From dd05ec37f762e1903c1d121346c9fbba7750155a Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Wed, 20 Aug 2025 15:04:25 +0200 Subject: [PATCH 01/59] feat: add initial project source code for TIC2WebSocket - Add Java-based WebSocket interface for Tele Information Client (TIC) - Implement real-time communication and data exchange capabilities - Include Maven build configuration and project structure - Add README in French and English - Include license files and contribution guidelines This commit establishes the foundation for the TIC2WebSocket application, a generic interface for accessing TIC data through WebSocket API. --- .gitignore | 8 + LICENSES/Apache-2.0.txt | 73 + README.fr.md | 120 ++ README.md | 120 ++ pom.xml | 344 +++++ src/assembly/all.xml | 102 ++ src/assembly/bin.xml | 85 ++ src/assembly/doc.xml | 30 + src/assembly/src.xml | 31 + .../config/TIC2WebSocketConfiguration.json | 29 + .../TIC2WebSocketConfiguration.json.license | 6 + .../message/EventOnError.json | 26 + .../message/EventOnError.json.license | 6 + .../message/EventOnTICData.json | 26 + .../message/EventOnTICData.json.license | 6 + .../message/RequestGetAvailableTICs.json | 14 + .../RequestGetAvailableTICs.json.license | 6 + .../message/RequestGetModemsInfo.json | 14 + .../message/RequestGetModemsInfo.json.license | 6 + .../message/RequestReadTIC.json | 22 + .../message/RequestReadTIC.json.license | 6 + .../message/RequestSubscribeTIC.json | 22 + .../message/RequestSubscribeTIC.json.license | 6 + .../message/RequestUnsubscribeTIC.json | 22 + .../RequestUnsubscribeTIC.json.license | 6 + .../message/ResponseGetAvailableTICs.json | 34 + .../ResponseGetAvailableTICs.json.license | 6 + .../message/ResponseGetModemsInfo.json | 34 + .../ResponseGetModemsInfo.json.license | 6 + .../message/ResponseReadTIC.json | 34 + .../message/ResponseReadTIC.json.license | 6 + .../message/ResponseSubscribeTIC.json | 26 + .../message/ResponseSubscribeTIC.json.license | 6 + .../message/ResponseUnsubscribeTIC.json | 26 + .../ResponseUnsubscribeTIC.json.license | 6 + .../tic/core/TICCoreError.json | 30 + .../tic/core/TICCoreError.json.license | 6 + .../tic/core/TICCoreFrame.json | 30 + .../tic/core/TICCoreFrame.json.license | 6 + .../tic/core/TICIdentifier.json | 25 + .../tic/core/TICIdentifier.json.license | 6 + src/main/java/enedis/lab/codec/Codec.java | 36 + .../java/enedis/lab/codec/CodecException.java | 169 +++ .../java/enedis/lab/io/PlugSubscriber.java | 31 + src/main/java/enedis/lab/io/PortFinder.java | 24 + .../java/enedis/lab/io/PortPlugNotifier.java | 214 +++ .../java/enedis/lab/io/channels/Channel.java | 110 ++ .../enedis/lab/io/channels/ChannelBase.java | 143 ++ .../lab/io/channels/ChannelConfiguration.java | 296 ++++ .../lab/io/channels/ChannelDirection.java | 22 + .../lab/io/channels/ChannelException.java | 194 +++ .../lab/io/channels/ChannelListener.java | 73 + .../lab/io/channels/ChannelPhysical.java | 227 +++ .../lab/io/channels/ChannelProtocol.java | 30 + .../enedis/lab/io/channels/ChannelStatus.java | 22 + .../ChannelSerialPortConfiguration.java | 432 ++++++ .../lab/io/channels/serialport/Parity.java | 25 + .../lab/io/datastreams/DataInputStream.java | 99 ++ .../enedis/lab/io/datastreams/DataStream.java | 38 + .../lab/io/datastreams/DataStreamBase.java | 373 +++++ .../datastreams/DataStreamConfiguration.java | 296 ++++ .../io/datastreams/DataStreamDirection.java | 21 + .../io/datastreams/DataStreamException.java | 205 +++ .../io/datastreams/DataStreamListener.java | 61 + .../lab/io/datastreams/DataStreamStatus.java | 23 + .../lab/io/datastreams/DataStreamType.java | 27 + .../lab/io/datastreams/DataStreamUser.java | 123 ++ .../io/serialport/SerialPortDescriptor.java | 455 ++++++ .../lab/io/serialport/SerialPortFinder.java | 120 ++ .../io/serialport/SerialPortFinderBase.java | 125 ++ .../serialport/SerialPortFinderForLinux.java | 703 +++++++++ .../SerialPortFinderForWindows.java | 455 ++++++ .../java/enedis/lab/io/tic/TICModemType.java | 48 + .../enedis/lab/io/tic/TICPortDescriptor.java | 291 ++++ .../java/enedis/lab/io/tic/TICPortFinder.java | 74 + .../enedis/lab/io/tic/TICPortFinderBase.java | 206 +++ .../lab/io/tic/TICPortPlugNotifier.java | 131 ++ .../enedis/lab/io/usb/USBPortDescriptor.java | 711 +++++++++ .../java/enedis/lab/io/usb/USBPortFinder.java | 73 + .../enedis/lab/io/usb/USBPortFinderBase.java | 201 +++ .../java/enedis/lab/protocol/tic/TICMode.java | 115 ++ .../tic/channels/ChannelTICSerialPort.java | 326 +++++ .../ChannelTICSerialPortConfiguration.java | 250 ++++ .../serialport/ChannelSerialPort.java | 823 +++++++++++ .../ChannelSerialPortErrorCode.java | 61 + .../tic/codec/CodecTICFrameHistoric.java | 172 +++ .../codec/CodecTICFrameHistoricDataSet.java | 198 +++ .../tic/codec/CodecTICFrameStandard.java | 156 ++ .../codec/CodecTICFrameStandardDataSet.java | 197 +++ .../lab/protocol/tic/codec/TICCodec.java | 295 ++++ .../tic/datastreams/TICInputStream.java | 241 ++++ .../datastreams/TICStreamConfiguration.java | 220 +++ .../lab/protocol/tic/frame/TICError.java | 90 ++ .../lab/protocol/tic/frame/TICFrame.java | 522 +++++++ .../protocol/tic/frame/TICFrameDataSet.java | 313 ++++ .../tic/frame/historic/TICFrameHistoric.java | 140 ++ .../historic/TICFrameHistoricDataSet.java | 190 +++ .../tic/frame/standard/TICException.java | 73 + .../tic/frame/standard/TICFrameStandard.java | 247 ++++ .../standard/TICFrameStandardDataSet.java | 352 +++++ .../java/enedis/lab/types/BytesArray.java | 1280 +++++++++++++++++ .../java/enedis/lab/types/DataArrayList.java | 392 +++++ .../java/enedis/lab/types/DataDictionary.java | 146 ++ .../lab/types/DataDictionaryException.java | 125 ++ src/main/java/enedis/lab/types/DataList.java | 57 + .../java/enedis/lab/types/ExceptionBase.java | 132 ++ .../types/configuration/Configuration.java | 104 ++ .../configuration/ConfigurationBase.java | 186 +++ .../configuration/ConfigurationException.java | 172 +++ .../datadictionary/DataDictionaryBase.java | 723 ++++++++++ .../types/datadictionary/KeyDescriptor.java | 66 + .../datadictionary/KeyDescriptorBase.java | 210 +++ .../KeyDescriptorDataDictionary.java | 152 ++ .../datadictionary/KeyDescriptorEnum.java | 142 ++ .../datadictionary/KeyDescriptorList.java | 233 +++ .../KeyDescriptorListMinMaxSize.java | 170 +++ .../KeyDescriptorLocalDateTime.java | 160 +++ .../datadictionary/KeyDescriptorNumber.java | 139 ++ .../KeyDescriptorNumberMinMax.java | 164 +++ .../datadictionary/KeyDescriptorString.java | 140 ++ .../java/enedis/lab/util/MinMaxChecker.java | 185 +++ .../java/enedis/lab/util/SystemError.java | 120 ++ src/main/java/enedis/lab/util/SystemLibC.java | 31 + .../java/enedis/lab/util/message/Event.java | 189 +++ .../java/enedis/lab/util/message/Message.java | 216 +++ .../enedis/lab/util/message/MessageType.java | 21 + .../java/enedis/lab/util/message/Request.java | 155 ++ .../enedis/lab/util/message/Response.java | 259 ++++ .../enedis/lab/util/message/ResponseBase.java | 189 +++ .../message/exception/MessageException.java | 112 ++ .../MessageInvalidContentException.java | 112 ++ .../MessageInvalidFormatException.java | 112 ++ .../MessageInvalidTypeException.java | 112 ++ .../MessageKeyNameDoesntExistException.java | 112 ++ .../MessageKeyTypeDoesntExistException.java | 112 ++ .../UnsupportedMessageException.java | 112 ++ .../factory/AbstractMessageFactory.java | 153 ++ .../message/factory/BasicMessageFactory.java | 118 ++ .../util/message/factory/EventFactory.java | 24 + .../util/message/factory/MessageFactory.java | 177 +++ .../util/message/factory/RequestFactory.java | 24 + .../util/message/factory/ResponseFactory.java | 24 + .../lab/util/task/FilteredNotifier.java | 113 ++ .../lab/util/task/FilteredNotifierBase.java | 302 ++++ .../java/enedis/lab/util/task/Notifier.java | 47 + .../enedis/lab/util/task/NotifierBase.java | 118 ++ .../java/enedis/lab/util/task/Subscriber.java | 16 + src/main/java/enedis/lab/util/task/Task.java | 31 + .../java/enedis/lab/util/task/TaskBase.java | 205 +++ .../enedis/lab/util/task/TaskPeriodic.java | 151 ++ .../task/TaskPeriodicWithSubscribers.java | 122 ++ src/main/java/enedis/lab/util/time/Time.java | 163 +++ .../tic/core/ReadNextFrameSubscriber.java | 104 ++ src/main/java/enedis/tic/core/TICCore.java | 107 ++ .../java/enedis/tic/core/TICCoreBase.java | 581 ++++++++ .../java/enedis/tic/core/TICCoreError.java | 288 ++++ .../enedis/tic/core/TICCoreErrorCode.java | 33 + .../enedis/tic/core/TICCoreException.java | 76 + .../java/enedis/tic/core/TICCoreFrame.java | 283 ++++ .../java/enedis/tic/core/TICCoreStream.java | 24 + .../enedis/tic/core/TICCoreStreamBase.java | 378 +++++ .../enedis/tic/core/TICCoreSubscriber.java | 32 + .../java/enedis/tic/core/TICIdentifier.java | 282 ++++ .../tic/service/TIC2WebSocketApplication.java | 424 ++++++ .../TIC2WebSocketApplicationErrorCode.java | 45 + .../tic/service/TIC2WebSocketCommandLine.java | 140 ++ .../service/client/TIC2WebSocketClient.java | 141 ++ .../client/TIC2WebSocketClientPool.java | 52 + .../client/TIC2WebSocketClientPoolBase.java | 140 ++ .../config/TIC2WebSocketConfiguration.java | 275 ++++ .../tic/service/endpoint/EventSender.java | 26 + .../TIC2WebSocketEndPointErrorCode.java | 57 + .../TIC2WebSocketEndPointException.java | 140 ++ .../tic/service/message/EventOnError.java | 192 +++ .../tic/service/message/EventOnTICData.java | 192 +++ .../message/RequestGetAvailableTICs.java | 128 ++ .../service/message/RequestGetModemsInfo.java | 123 ++ .../tic/service/message/RequestReadTIC.java | 189 +++ .../service/message/RequestSubscribeTIC.java | 190 +++ .../message/RequestUnsubscribeTIC.java | 190 +++ .../message/ResponseGetAvailableTICs.java | 197 +++ .../message/ResponseGetModemsInfo.java | 197 +++ .../tic/service/message/ResponseReadTIC.java | 196 +++ .../service/message/ResponseSubscribeTIC.java | 160 +++ .../message/ResponseUnsubscribeTIC.java | 160 +++ .../factory/TIC2WebSocketEventFactory.java | 28 + .../factory/TIC2WebSocketMessageFactory.java | 24 + .../factory/TIC2WebSocketRequestFactory.java | 34 + .../factory/TIC2WebSocketResponseFactory.java | 34 + .../TIC2WebSocketChannelInitializer.java | 121 ++ .../service/netty/TIC2WebSocketHandler.java | 322 +++++ .../service/netty/TIC2WebSocketServer.java | 190 +++ .../TIC2WebSocketRequestHandler.java | 27 + .../TIC2WebSocketRequestHandlerBase.java | 341 +++++ .../config/TIC2WebSocketConfiguration.json | 4 + .../TIC2WebSocketConfiguration.json.license | 6 + src/main/resources/log4j2-debug.xml | 45 + src/main/resources/log4j2-error.xml | 45 + src/main/resources/log4j2-fatal.xml | 45 + src/main/resources/log4j2-info.xml | 45 + src/main/resources/log4j2-off.xml | 45 + src/main/resources/log4j2-trace.xml | 45 + src/main/resources/log4j2-warn.xml | 45 + src/main/scripts/SerialPortPlugNotifier.bat | 14 + src/main/scripts/SerialPortPlugNotifier.sh | 14 + src/main/scripts/TIC2WebSocket.bat | 14 + src/main/scripts/TIC2WebSocket.sh | 14 + .../scripts/TIC2WebSocket_remote_debug.sh | 14 + src/main/scripts/TICPortDump.bat | 16 + src/main/scripts/TICPortDump.ps1 | 42 + src/main/scripts/TICPortDump.sh | 35 + src/main/scripts/TICPortPlugNotifier.bat | 14 + src/main/scripts/TICPortPlugNotifier.sh | 14 + src/main/scripts/setup_TIC2WebSocket.sh | 92 ++ .../java/enedis/lab/io/PortFinderMock.java | 73 + .../enedis/lab/io/tic/TICPortFinderMock.java | 102 ++ .../java/enedis/lab/mock/FunctionCall.java | 26 + .../java/enedis/tic/core/TICCoreBaseTest.java | 498 +++++++ .../tic/core/TICCoreReadNextFrameTask.java | 109 ++ .../enedis/tic/core/TICCoreStreamMock.java | 167 +++ .../tic/core/TICCoreSubscriberMock.java | 85 ++ .../tic/core/TICCoreSubscriberOnDataCall.java | 78 + .../core/TICCoreSubscriberOnErrorCall.java | 78 + .../enedis/tic/core/TICIdentifierTest.java | 187 +++ .../TIC2WebSocketEventFactoryTest.java | 75 + .../TIC2WebSocketMessageFactoryTest.java | 34 + .../TIC2WebSocketRequestFactoryTest.java | 239 +++ .../TIC2WebSocketResponseFactoryTest.java | 161 +++ .../netty/TIC2WebSocketServerTest.java | 39 + src/uml/TIC2WebSocket.vpp | Bin 0 -> 2255872 bytes src/uml/TIC2WebSocket.vpp.license | 6 + src/uml/TICCore.vpp | Bin 0 -> 2255872 bytes src/uml/TICCore.vpp.license | 6 + 233 files changed, 31575 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSES/Apache-2.0.txt create mode 100644 README.fr.md create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/assembly/all.xml create mode 100644 src/assembly/bin.xml create mode 100644 src/assembly/doc.xml create mode 100644 src/assembly/src.xml create mode 100644 src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json create mode 100644 src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license create mode 100644 src/dataDictionaryDescriptors/message/EventOnError.json create mode 100644 src/dataDictionaryDescriptors/message/EventOnError.json.license create mode 100644 src/dataDictionaryDescriptors/message/EventOnTICData.json create mode 100644 src/dataDictionaryDescriptors/message/EventOnTICData.json.license create mode 100644 src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json create mode 100644 src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license create mode 100644 src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json create mode 100644 src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license create mode 100644 src/dataDictionaryDescriptors/message/RequestReadTIC.json create mode 100644 src/dataDictionaryDescriptors/message/RequestReadTIC.json.license create mode 100644 src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json create mode 100644 src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license create mode 100644 src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json create mode 100644 src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license create mode 100644 src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json create mode 100644 src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license create mode 100644 src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json create mode 100644 src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license create mode 100644 src/dataDictionaryDescriptors/message/ResponseReadTIC.json create mode 100644 src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license create mode 100644 src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json create mode 100644 src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license create mode 100644 src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json create mode 100644 src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license create mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreError.json create mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license create mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json create mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license create mode 100644 src/dataDictionaryDescriptors/tic/core/TICIdentifier.json create mode 100644 src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license create mode 100644 src/main/java/enedis/lab/codec/Codec.java create mode 100644 src/main/java/enedis/lab/codec/CodecException.java create mode 100644 src/main/java/enedis/lab/io/PlugSubscriber.java create mode 100644 src/main/java/enedis/lab/io/PortFinder.java create mode 100644 src/main/java/enedis/lab/io/PortPlugNotifier.java create mode 100644 src/main/java/enedis/lab/io/channels/Channel.java create mode 100644 src/main/java/enedis/lab/io/channels/ChannelBase.java create mode 100644 src/main/java/enedis/lab/io/channels/ChannelConfiguration.java create mode 100644 src/main/java/enedis/lab/io/channels/ChannelDirection.java create mode 100644 src/main/java/enedis/lab/io/channels/ChannelException.java create mode 100644 src/main/java/enedis/lab/io/channels/ChannelListener.java create mode 100644 src/main/java/enedis/lab/io/channels/ChannelPhysical.java create mode 100644 src/main/java/enedis/lab/io/channels/ChannelProtocol.java create mode 100644 src/main/java/enedis/lab/io/channels/ChannelStatus.java create mode 100644 src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java create mode 100644 src/main/java/enedis/lab/io/channels/serialport/Parity.java create mode 100644 src/main/java/enedis/lab/io/datastreams/DataInputStream.java create mode 100644 src/main/java/enedis/lab/io/datastreams/DataStream.java create mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamBase.java create mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java create mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java create mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamException.java create mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamListener.java create mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java create mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamType.java create mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamUser.java create mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java create mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortFinder.java create mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java create mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java create mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java create mode 100644 src/main/java/enedis/lab/io/tic/TICModemType.java create mode 100644 src/main/java/enedis/lab/io/tic/TICPortDescriptor.java create mode 100644 src/main/java/enedis/lab/io/tic/TICPortFinder.java create mode 100644 src/main/java/enedis/lab/io/tic/TICPortFinderBase.java create mode 100644 src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java create mode 100644 src/main/java/enedis/lab/io/usb/USBPortDescriptor.java create mode 100644 src/main/java/enedis/lab/io/usb/USBPortFinder.java create mode 100644 src/main/java/enedis/lab/io/usb/USBPortFinderBase.java create mode 100644 src/main/java/enedis/lab/protocol/tic/TICMode.java create mode 100644 src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java create mode 100644 src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java create mode 100644 src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java create mode 100644 src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java create mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java create mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java create mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java create mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java create mode 100644 src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java create mode 100644 src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java create mode 100644 src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java create mode 100644 src/main/java/enedis/lab/protocol/tic/frame/TICError.java create mode 100644 src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java create mode 100644 src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java create mode 100644 src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java create mode 100644 src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java create mode 100644 src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java create mode 100644 src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java create mode 100644 src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java create mode 100644 src/main/java/enedis/lab/types/BytesArray.java create mode 100644 src/main/java/enedis/lab/types/DataArrayList.java create mode 100644 src/main/java/enedis/lab/types/DataDictionary.java create mode 100644 src/main/java/enedis/lab/types/DataDictionaryException.java create mode 100644 src/main/java/enedis/lab/types/DataList.java create mode 100644 src/main/java/enedis/lab/types/ExceptionBase.java create mode 100644 src/main/java/enedis/lab/types/configuration/Configuration.java create mode 100644 src/main/java/enedis/lab/types/configuration/ConfigurationBase.java create mode 100644 src/main/java/enedis/lab/types/configuration/ConfigurationException.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java create mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java create mode 100644 src/main/java/enedis/lab/util/MinMaxChecker.java create mode 100644 src/main/java/enedis/lab/util/SystemError.java create mode 100644 src/main/java/enedis/lab/util/SystemLibC.java create mode 100644 src/main/java/enedis/lab/util/message/Event.java create mode 100644 src/main/java/enedis/lab/util/message/Message.java create mode 100644 src/main/java/enedis/lab/util/message/MessageType.java create mode 100644 src/main/java/enedis/lab/util/message/Request.java create mode 100644 src/main/java/enedis/lab/util/message/Response.java create mode 100644 src/main/java/enedis/lab/util/message/ResponseBase.java create mode 100644 src/main/java/enedis/lab/util/message/exception/MessageException.java create mode 100644 src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java create mode 100644 src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java create mode 100644 src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java create mode 100644 src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java create mode 100644 src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java create mode 100644 src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java create mode 100644 src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java create mode 100644 src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java create mode 100644 src/main/java/enedis/lab/util/message/factory/EventFactory.java create mode 100644 src/main/java/enedis/lab/util/message/factory/MessageFactory.java create mode 100644 src/main/java/enedis/lab/util/message/factory/RequestFactory.java create mode 100644 src/main/java/enedis/lab/util/message/factory/ResponseFactory.java create mode 100644 src/main/java/enedis/lab/util/task/FilteredNotifier.java create mode 100644 src/main/java/enedis/lab/util/task/FilteredNotifierBase.java create mode 100644 src/main/java/enedis/lab/util/task/Notifier.java create mode 100644 src/main/java/enedis/lab/util/task/NotifierBase.java create mode 100644 src/main/java/enedis/lab/util/task/Subscriber.java create mode 100644 src/main/java/enedis/lab/util/task/Task.java create mode 100644 src/main/java/enedis/lab/util/task/TaskBase.java create mode 100644 src/main/java/enedis/lab/util/task/TaskPeriodic.java create mode 100644 src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java create mode 100644 src/main/java/enedis/lab/util/time/Time.java create mode 100644 src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java create mode 100644 src/main/java/enedis/tic/core/TICCore.java create mode 100644 src/main/java/enedis/tic/core/TICCoreBase.java create mode 100644 src/main/java/enedis/tic/core/TICCoreError.java create mode 100644 src/main/java/enedis/tic/core/TICCoreErrorCode.java create mode 100644 src/main/java/enedis/tic/core/TICCoreException.java create mode 100644 src/main/java/enedis/tic/core/TICCoreFrame.java create mode 100644 src/main/java/enedis/tic/core/TICCoreStream.java create mode 100644 src/main/java/enedis/tic/core/TICCoreStreamBase.java create mode 100644 src/main/java/enedis/tic/core/TICCoreSubscriber.java create mode 100644 src/main/java/enedis/tic/core/TICIdentifier.java create mode 100644 src/main/java/enedis/tic/service/TIC2WebSocketApplication.java create mode 100644 src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java create mode 100644 src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java create mode 100644 src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java create mode 100644 src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java create mode 100644 src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java create mode 100644 src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java create mode 100644 src/main/java/enedis/tic/service/endpoint/EventSender.java create mode 100644 src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java create mode 100644 src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java create mode 100644 src/main/java/enedis/tic/service/message/EventOnError.java create mode 100644 src/main/java/enedis/tic/service/message/EventOnTICData.java create mode 100644 src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java create mode 100644 src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java create mode 100644 src/main/java/enedis/tic/service/message/RequestReadTIC.java create mode 100644 src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java create mode 100644 src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java create mode 100644 src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java create mode 100644 src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java create mode 100644 src/main/java/enedis/tic/service/message/ResponseReadTIC.java create mode 100644 src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java create mode 100644 src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java create mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java create mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java create mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java create mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java create mode 100644 src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java create mode 100644 src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java create mode 100644 src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java create mode 100644 src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java create mode 100644 src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java create mode 100644 src/main/resources/config/TIC2WebSocketConfiguration.json create mode 100644 src/main/resources/config/TIC2WebSocketConfiguration.json.license create mode 100644 src/main/resources/log4j2-debug.xml create mode 100644 src/main/resources/log4j2-error.xml create mode 100644 src/main/resources/log4j2-fatal.xml create mode 100644 src/main/resources/log4j2-info.xml create mode 100644 src/main/resources/log4j2-off.xml create mode 100644 src/main/resources/log4j2-trace.xml create mode 100644 src/main/resources/log4j2-warn.xml create mode 100644 src/main/scripts/SerialPortPlugNotifier.bat create mode 100644 src/main/scripts/SerialPortPlugNotifier.sh create mode 100644 src/main/scripts/TIC2WebSocket.bat create mode 100644 src/main/scripts/TIC2WebSocket.sh create mode 100644 src/main/scripts/TIC2WebSocket_remote_debug.sh create mode 100644 src/main/scripts/TICPortDump.bat create mode 100644 src/main/scripts/TICPortDump.ps1 create mode 100644 src/main/scripts/TICPortDump.sh create mode 100644 src/main/scripts/TICPortPlugNotifier.bat create mode 100644 src/main/scripts/TICPortPlugNotifier.sh create mode 100644 src/main/scripts/setup_TIC2WebSocket.sh create mode 100644 src/test/java/enedis/lab/io/PortFinderMock.java create mode 100644 src/test/java/enedis/lab/io/tic/TICPortFinderMock.java create mode 100644 src/test/java/enedis/lab/mock/FunctionCall.java create mode 100644 src/test/java/enedis/tic/core/TICCoreBaseTest.java create mode 100644 src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java create mode 100644 src/test/java/enedis/tic/core/TICCoreStreamMock.java create mode 100644 src/test/java/enedis/tic/core/TICCoreSubscriberMock.java create mode 100644 src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java create mode 100644 src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java create mode 100644 src/test/java/enedis/tic/core/TICIdentifierTest.java create mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java create mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java create mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java create mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java create mode 100644 src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java create mode 100644 src/uml/TIC2WebSocket.vpp create mode 100644 src/uml/TIC2WebSocket.vpp.license create mode 100644 src/uml/TICCore.vpp create mode 100644 src/uml/TICCore.vpp.license diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29a63cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +src/main/resources/TIC2WebSocket.properties diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed 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. diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 0000000..f2e8934 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,120 @@ + + +# TIC2WebSocket +*Interface WebSocket pour les données TIC (Télé Information Client)* + +[![REUSE status](https://api.reuse.software/badge/git.fsfe.org/reuse/api)](https://api.reuse.software/info/git.fsfe.org/reuse/api) + +[🇫🇷 Français](README.fr.md) | [🇺🇸 English](README.md) + +## Sommaire + +* [Introduction](#introduction) +* [Installation](#installation) +* [Exemple d'utilisation](#usage_example) +* [Documentation](#documentation) +* [Contribuer](#contrib) +* [Support](#support) +* [Contributeurs](#contributors) + +## Introduction + +L'application **TIC2WebSocket** est utilisée comme interface générique pour accéder aux données TIC (Télé Information Client). + +TIC2WebSocket fournit une API basée sur WebSocket pour gérer les données TIC, permettant une communication en temps réel et un échange de données pour les informations de télémétrie client. + +## Installation + +### Prérequis + +Pour générer/installer l'application, vous avez besoin des prérequis suivants : + +- [Java](https://www.java.com/download/) +- [Maven](https://maven.apache.org/download.cgi) + +### Installation + +Pour installer l'application **TIC2WebSocket**, suivez ces étapes : + +#### Générer les fichiers cible +Lorsque vous êtes à la racine du projet, tapez simplement la commande suivante : + +```bash +mvn clean package +``` + +En conséquence, le répertoire *target* est créé, contenant le fichier .zip de sortie. + +#### Extraire le fichier .zip +Décompressez le fichier .zip dans le dossier de votre choix. + +🐧 **Linux** + +Extraire le zip +```bash +unzip target/TIC2WebSocket-*-bin -d /chemin/vers/votre/dossier +``` + +💻 **Windows** + +Extrayez `target/TIC2WebSocket-*-bin` dans le dossier de votre choix. + +### Démarrage de l'Application + +Pour démarrer l'application **TIC2WebSocket**, exécutez le script de lancement : + +🐧 **Linux** + +Extraire le zip +```bash +cd /chemin/vers/votre/dossier +./TIC2WebSocket.sh +``` + +💻 **Windows** + +Exécutez `TIC2WebSocket.bat` + +### Aide + +Ajoutez l'option `--help` pour obtenir des informations de base sur l'utilisation du lanceur. +```bash +./TIC2WebSocket.sh --help +``` + +## Documentation + +[TODO] + +## Contribuer ? + +![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) + +Consultez le guide [Getting started](GETTING_STARTED.md), qui documente comment installer et configurer l'environnement requis pour le développement + +Vous n'avez pas besoin d'être développeur pour contribuer, ni de faire beaucoup, vous pouvez simplement : +* Améliorer la documentation, +* Corriger une faute d'orthographe, +* [Signaler un bug](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose) +* [Demander une fonctionnalité](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose) +* [Nous donner des conseils ou des idées](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose), +* etc. + +Pour vous aider à démarrer, nous vous invitons à lire [Contributing](CONTRIBUTING.md), qui vous donne les règles et conventions de code à respecter + +Pour contribuer à cette documentation (README, CONTRIBUTING, etc.), nous nous conformons à la [CommonMark Spec](https://spec.commonmark.org/) + +## Contributeurs + +Contributeurs principaux : +* **Jehan BOUSCH** () +* **Mathieu SABARTHES** () + +Nous nous efforçons de fournir un environnement bienveillant et de soutenir toute [contribution](#contrib). diff --git a/README.md b/README.md new file mode 100644 index 0000000..fddcc9b --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ + + +# TIC2WebSocket +*Tele Information Client (TIC) WebSocket Interface* + +[![REUSE status](https://api.reuse.software/badge/git.fsfe.org/reuse/api)](https://api.reuse.software/info/git.fsfe.org/reuse/api) + +[🇫🇷 Français](README.fr.md) | [🇺🇸 English](README.md) + +## Summary + +* [Introduction](#introduction) +* [Installation](#installation) +* [Usage Example](#usage_example) +* [Documentation](#documentation) +* [Contributing](#contrib) +* [Support](#support) +* [Contributors](#contributors) + +## Introduction + +The **TIC2WebSocket** application is used as a generic interface for accessing the Tele Information Client (TIC). + +TIC2WebSocket provides a WebSocket-based API for managing and interfacing with TIC data, enabling real-time communication and data exchange for client telemetry information. + +## Installation + +### Prerequisites + +To generate/install the application, you need the following prerequisites: + +- [Java](https://www.java.com/download/) +- [Maven](https://maven.apache.org/download.cgi) + +### Installation + +To install the **TIC2WebSocket** application, follow these steps: + +#### Generate target files +When you are at the project root, simply type the following command: + +```bash +mvn clean package +``` + +As a result, the *target* directory is created, containing the output .zip file. + +#### Extract .zip file +Unzip the .zip file into the folder of your choice. + +🐧 **Linux** + +Extract zip +```bash +unzip target/TIC2WebSocket-*-bin -d /path/to/your/folder +``` + +💻 **Windows** + +Extract `target/TIC2WebSocket-*-bin` in the folder of your choice. + +### Starting the Application + +To start the **TIC2WebSocket** application, execute the launch script: + +🐧 **Linux** + +Extract zip +```bash +cd /path/to/your/folder +./TIC2WebSocket.sh +``` + +💻 **Windows** + +Run `TIC2WebSocket.bat` + +### Help + +Add `--help` option to get basic information on how to use the launcher. +```bash +./TIC2WebSocket.sh --help +``` + +## Documentation + +[TODO] + +## Contributing ? + +![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) + +See the [Getting started](GETTING_STARTED.md), which document how to install and setup the required environment for developing + +You don't need to be a developer to contribute, nor do much, you can simply: +* Enhance documentation, +* Correct a spelling, +* [Report a bug](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose) +* [Ask a feature](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose) +* [Give us advices or ideas](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose), +* etc. + +To help you start, we invite you to read [Contributing](CONTRIBUTING.md), which gives you rules and code conventions to respect + +To contribute to this documentation (README, CONTRIBUTING, etc.), we conform to the [CommonMark Spec](https://spec.commonmark.org/) + +## Contributors + +Core contributors : +* **Jehan BOUSCH** () +* **Mathieu SABARTHES** () + +We strive to provide a benevolent environment and support any [contribution](#contrib). diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..97debe9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,344 @@ + + + + 4.0.0 + + + fr.enedis + TIC2WebSocket + 1.0.0 + jar + ${project.artifactId} + + Tele Information Client (TIC) WebSocket Interface + + + Enedis + http://www.enedis.fr/ + + + + + + Apache-2.0 + LICENCES/Apache-2.0.txt + repo + Licence Apache 2.0. + + + + + + + Jehan BOUSCH + jehan-externe.bousch@enedis.fr + + Spécifieur + Architecte + Développeur + Testeur + + Europe/Paris + + ${team_pics_dir}/JB158FFN.png + + + + Mathieu SABARTHES + matthieu-externe.sabarthes@enedis.fr + + Spécifieur + Architecte + Développeur + Testeur + + Europe/Paris + + ${team_pics_dir}/MS4D7E9N.png + + + + + + + + + + + + + + + + + + + + + + + + + UTF-8 + UTF-8 + UTF-8 + ${project.basedir} + ${project.groupId} + ${project.artifactId} + ${project.name} + ${project.version} + ${project.description} + ./images/team + + + + + + junit + junit + 4.13.2 + test + + + org.json + json + 20231013 + + + org.apache.logging.log4j + log4j-core + 2.17.2 + + + io.github.java-native + jssc + 2.9.2 + + + org.apache.commons + commons-lang3 + 3.18.0 + + + net.java.dev.jna + jna-platform + 5.17.0 + + + org.usb4java + usb4java + 1.3.0 + + + info.picocli + picocli + 4.7.7 + + + io.netty + netty-all + 4.2.2.Final + + + + + + + + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-site-plugin + 3.7.1 + + fr + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.0.0 + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + generate-resources + + write-project-properties + + + ${project.basedir}/src/main/resources/${project.artifactId}.properties + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.1 + + + make-bin-package + package + + single + + + + ${project.basedir}/src/assembly/bin.xml + + + + + make-src-package + package + + single + + + + ${project.basedir}/src/assembly/src.xml + + + + + make-doc-package + site + + single + + + + ${project.basedir}/src/assembly/doc.xml + + + + + make-complete-package + site + + single + + + + ${project.basedir}/src/assembly/all.xml + + + + + + + com.github.jeluard + plantuml-maven-plugin + 1.2 + + + manuel_utilisateur-uml + + generate + + pre-site + + UTF-8 + + ${project.basedir}/src/site/manuel_utilisateur/resources/uml + + **/*.plantuml + + + ${project.basedir}/src/site/manuel_utilisateur/resources/images/uml + + + + architecture-uml + + generate + + pre-site + + UTF-8 + + ${project.basedir}/src/site/architecture/resources/uml + + **/*.plantuml + + + ${project.basedir}/src/site/architecture/resources/images/uml + + + + + + org.apache.maven.plugins + maven-pdf-plugin + 1.4 + + + manuel_utilisateur-pdf + pre-site + + pdf + + + ${project.basedir}/src/site/manuel_utilisateur + ${project.basedir}/src/site/manuel_utilisateur/pdf.xml + ${project.reporting.outputDirectory}/manuel_utilisateur + false + + + + architecture-pdf + pre-site + + pdf + + + ${project.basedir}/src/site/architecture + ${project.basedir}/src/site/architecture/pdf.xml + ${project.reporting.outputDirectory}/architecture + false + + + + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.0 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assembly/all.xml b/src/assembly/all.xml new file mode 100644 index 0000000..1e81400 --- /dev/null +++ b/src/assembly/all.xml @@ -0,0 +1,102 @@ + + + + all + + zip + tar.gz + + false + + + true + lib + false + + + + + CHANGELOG.md + CHANGELOG.md + + + LICENCE.txt + LICENCE.txt + + + ${project.build.scriptSourceDirectory}/${project.name}.bat + ${project.name}.bat + + + ${project.build.scriptSourceDirectory}/${project.name}.sh + ${project.name}.sh + 0755 + + + ${project.build.scriptSourceDirectory}/SerialPortPlugNotifier.bat + SerialPortPlugNotifier.bat + + + ${project.build.scriptSourceDirectory}/SerialPortPlugNotifier.sh + SerialPortPlugNotifier.sh + 0755 + + + ${project.build.scriptSourceDirectory}/TICPortPlugNotifier.bat + TICPortPlugNotifier.bat + + + ${project.build.scriptSourceDirectory}/TICPortPlugNotifier.sh + TICPortPlugNotifier.sh + 0755 + + + ${project.build.scriptSourceDirectory}/TICPortDump.bat + TICPortDump.bat + + + ${project.build.scriptSourceDirectory}/TICPortDump.ps1 + TICPortDump.ps1 + + + ${project.build.scriptSourceDirectory}/TICPortDump.sh + TICPortDump.sh + 0755 + + + + + ${basedir}/src/main/resources/config + var/config + true + + + ${project.basedir} + true + + **/*.log + **/.settings/** + **/var/** + **/${project.build.directory}/** + + + + ${project.build.directory}/site + doc + true + + **/*.log + + + + diff --git a/src/assembly/bin.xml b/src/assembly/bin.xml new file mode 100644 index 0000000..efe8df6 --- /dev/null +++ b/src/assembly/bin.xml @@ -0,0 +1,85 @@ + + + + + + bin + + zip + tar.gz + + false + + + true + lib + false + + + + + CHANGELOG.md + CHANGELOG.md + + + ${project.build.scriptSourceDirectory}/${project.name}.bat + ${project.name}.bat + + + ${project.build.scriptSourceDirectory}/${project.name}.sh + ${project.name}.sh + 0755 + + + ${project.build.scriptSourceDirectory}/SerialPortPlugNotifier.bat + SerialPortPlugNotifier.bat + + + ${project.build.scriptSourceDirectory}/SerialPortPlugNotifier.sh + SerialPortPlugNotifier.sh + 0755 + + + ${project.build.scriptSourceDirectory}/TICPortPlugNotifier.bat + TICPortPlugNotifier.bat + + + ${project.build.scriptSourceDirectory}/TICPortPlugNotifier.sh + TICPortPlugNotifier.sh + 0755 + + + ${project.build.scriptSourceDirectory}/TICPortDump.bat + TICPortDump.bat + + + ${project.build.scriptSourceDirectory}/TICPortDump.ps1 + TICPortDump.ps1 + + + ${project.build.scriptSourceDirectory}/TICPortDump.sh + TICPortDump.sh + 0755 + + + + + ${basedir}/src/main/resources/config + var/config + true + + + ${basedir}/LICENSES + + + \ No newline at end of file diff --git a/src/assembly/doc.xml b/src/assembly/doc.xml new file mode 100644 index 0000000..9a2fe16 --- /dev/null +++ b/src/assembly/doc.xml @@ -0,0 +1,30 @@ + + + + doc + + zip + tar.gz + + false + + + ${project.build.directory}/site + . + true + + **/*.log + + + + \ No newline at end of file diff --git a/src/assembly/src.xml b/src/assembly/src.xml new file mode 100644 index 0000000..9769f18 --- /dev/null +++ b/src/assembly/src.xml @@ -0,0 +1,31 @@ + + + + src + + zip + tar.gz + + false + + + ${project.basedir} + true + + **/*.log + **/var/** + **/${project.build.directory}/** + + + + diff --git a/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json b/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json new file mode 100644 index 0000000..f15c099 --- /dev/null +++ b/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json @@ -0,0 +1,29 @@ +{ + "package" : "enedis.tic.service.config", + "name": "TIC2WebSocketConfiguration", + "type": "ConfigurationBase", + "attributes": [ + { + "name": "serverPort", + "mandatory": true, + "type": "Number", + "constraint": "MinMax", + "min": 1, + "max": 65535 + }, + { + "name": "ticMode", + "mandatory": false, + "type": "Enum", + "enumType": "TICMode" + }, + { + "name": "ticPortNames", + "mandatory": false, + "type": "List", + "listType": "String", + "constraint": "MinMaxSize", + "min": 1 + } + ] +} diff --git a/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license b/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/EventOnError.json b/src/dataDictionaryDescriptors/message/EventOnError.json new file mode 100644 index 0000000..a00674f --- /dev/null +++ b/src/dataDictionaryDescriptors/message/EventOnError.json @@ -0,0 +1,26 @@ +{ + "package": "enedis.tic.service.message", + "name": "EventOnError", + "extends" : "Event", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"OnError\"", + "removeFromConstructor" : true + }, + { + "name": "dateTime", + "type": "LocalDateTime" + } + ], + "attributes": [ + { + "name": "data", + "mandatory" : false, + "type": "DataDictionary", + "dataDictionaryType": "TICCoreError" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/EventOnError.json.license b/src/dataDictionaryDescriptors/message/EventOnError.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/EventOnError.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/EventOnTICData.json b/src/dataDictionaryDescriptors/message/EventOnTICData.json new file mode 100644 index 0000000..180d5fd --- /dev/null +++ b/src/dataDictionaryDescriptors/message/EventOnTICData.json @@ -0,0 +1,26 @@ +{ + "package": "enedis.tic.service.message", + "name": "EventOnTICData", + "extends" : "Event", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"OnTICData\"", + "removeFromConstructor" : true + }, + { + "name": "dateTime", + "type": "LocalDateTime" + } + ], + "attributes": [ + { + "name": "data", + "mandatory" : true, + "type": "DataDictionary", + "dataDictionaryType": "TICCoreFrame" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/EventOnTICData.json.license b/src/dataDictionaryDescriptors/message/EventOnTICData.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/EventOnTICData.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json b/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json new file mode 100644 index 0000000..afa0795 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json @@ -0,0 +1,14 @@ +{ + "package": "enedis.tic.service.message", + "name": "RequestGetAvailableTICs", + "extends" : "Request", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"GetAvailableTICs\"", + "removeFromConstructor" : true + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license b/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json b/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json new file mode 100644 index 0000000..2846109 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json @@ -0,0 +1,14 @@ +{ + "package": "enedis.tic.service.message", + "name": "RequestGetModemsInfo", + "extends" : "Request", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"GetModemsInfo\"", + "removeFromConstructor" : true + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license b/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestReadTIC.json b/src/dataDictionaryDescriptors/message/RequestReadTIC.json new file mode 100644 index 0000000..30ca65a --- /dev/null +++ b/src/dataDictionaryDescriptors/message/RequestReadTIC.json @@ -0,0 +1,22 @@ +{ + "package": "enedis.tic.service.message", + "name": "RequestReadTIC", + "extends" : "Request", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"ReadTIC\"", + "removeFromConstructor" : true + } + ], + "attributes": [ + { + "name": "data", + "mandatory" : true, + "type": "DataDictionary", + "dataDictionaryType": "TICIdentifier" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestReadTIC.json.license b/src/dataDictionaryDescriptors/message/RequestReadTIC.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/RequestReadTIC.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json b/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json new file mode 100644 index 0000000..f97b2fb --- /dev/null +++ b/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json @@ -0,0 +1,22 @@ +{ + "package": "enedis.tic.service.message", + "name": "RequestSubscribeTIC", + "extends" : "Request", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"SubscribeTIC\"", + "removeFromConstructor" : true + } + ], + "attributes": [ + { + "name": "data", + "mandatory" : false, + "type": "List", + "listType": "TICIdentifier" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json b/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json new file mode 100644 index 0000000..fd33cd9 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json @@ -0,0 +1,22 @@ +{ + "package": "enedis.tic.service.message", + "name": "RequestUnsubscribeTIC", + "extends" : "Request", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"UnsubscribeTIC\"", + "removeFromConstructor" : true + } + ], + "attributes": [ + { + "name": "data", + "mandatory" : false, + "type": "List", + "listType": "TICIdentifier" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json b/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json new file mode 100644 index 0000000..cb53f58 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json @@ -0,0 +1,34 @@ +{ + "package": "enedis.tic.service.message", + "name": "ResponseGetAvailableTICs", + "extends" : "Response", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"GetAvailableTICs\"", + "removeFromConstructor" : true + }, + { + "name": "dateTime", + "type": "LocalDateTime" + }, + { + "name": "errorCode", + "type": "Number" + }, + { + "name": "errorMessage", + "type": "String" + } + ], + "attributes": [ + { + "name": "data", + "mandatory" : false, + "type": "List", + "listType": "TICIdentifier" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license b/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json b/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json new file mode 100644 index 0000000..dbc7c86 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json @@ -0,0 +1,34 @@ +{ + "package": "enedis.tic.service.message", + "name": "ResponseGetModemsInfo", + "extends" : "Response", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"GetModemsInfo\"", + "removeFromConstructor" : true + }, + { + "name": "dateTime", + "type": "LocalDateTime" + }, + { + "name": "errorCode", + "type": "Number" + }, + { + "name": "errorMessage", + "type": "String" + } + ], + "attributes": [ + { + "name": "data", + "mandatory" : false, + "type": "List", + "listType": "TICPortDescriptor" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license b/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseReadTIC.json b/src/dataDictionaryDescriptors/message/ResponseReadTIC.json new file mode 100644 index 0000000..9498969 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/ResponseReadTIC.json @@ -0,0 +1,34 @@ +{ + "package": "enedis.tic.service.message", + "name": "ResponseReadTIC", + "extends" : "Response", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"ReadTIC\"", + "removeFromConstructor" : true + }, + { + "name": "dateTime", + "type": "LocalDateTime" + }, + { + "name": "errorCode", + "type": "Number" + }, + { + "name": "errorMessage", + "type": "String" + } + ], + "attributes": [ + { + "name": "data", + "mandatory" : false, + "type": "DataDictionary", + "dataDictionaryType": "TICCoreFrame" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license b/src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json b/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json new file mode 100644 index 0000000..7de0ca3 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json @@ -0,0 +1,26 @@ +{ + "package": "enedis.tic.service.message", + "name": "ResponseSubscribeTIC", + "extends" : "Response", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"SubscribeTIC\"", + "removeFromConstructor" : true + }, + { + "name": "dateTime", + "type": "LocalDateTime" + }, + { + "name": "errorCode", + "type": "Number" + }, + { + "name": "errorMessage", + "type": "String" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json b/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json new file mode 100644 index 0000000..834ed75 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json @@ -0,0 +1,26 @@ +{ + "package": "enedis.tic.service.message", + "name": "ResponseUnsubscribeTIC", + "extends" : "Response", + "type": "DataDictionaryBase", + "superAttributes": [ + { + "name": "name", + "type": "String", + "acceptedValues": "\"UnsubscribeTIC\"", + "removeFromConstructor" : true + }, + { + "name": "dateTime", + "type": "LocalDateTime" + }, + { + "name": "errorCode", + "type": "Number" + }, + { + "name": "errorMessage", + "type": "String" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreError.json b/src/dataDictionaryDescriptors/tic/core/TICCoreError.json new file mode 100644 index 0000000..04569e7 --- /dev/null +++ b/src/dataDictionaryDescriptors/tic/core/TICCoreError.json @@ -0,0 +1,30 @@ +{ + "package" : "enedis.tic.core", + "name": "TICCoreError", + "type": "DataDictionaryBase", + "attributes": [ + { + "name": "identifier", + "mandatory": true, + "type": "DataDictionary", + "dataDictionaryType": "TICIdentifier" + }, + { + "name": "errorCode", + "mandatory": true, + "type": "Number" + }, + { + "name": "errorMessage", + "mandatory": true, + "type": "String", + "emptyAllow": false + }, + { + "name": "data", + "mandatory": false, + "type": "DataDictionary", + "dataDictionaryType": "DataDictionaryBase" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license b/src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json b/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json new file mode 100644 index 0000000..73b606a --- /dev/null +++ b/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json @@ -0,0 +1,30 @@ +{ + "package" : "enedis.tic.core", + "name": "TICCoreFrame", + "type": "DataDictionaryBase", + "attributes": [ + { + "name": "identifier", + "mandatory": true, + "type": "DataDictionary", + "dataDictionaryType": "TICIdentifier" + }, + { + "name": "mode", + "mandatory": true, + "type": "Enum", + "enumType": "TICMode" + }, + { + "name": "captureDateTime", + "mandatory": true, + "type": "LocalDateTime" + }, + { + "name": "content", + "mandatory": true, + "type": "DataDictionary", + "dataDictionaryType": "DataDictionaryBase" + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license b/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json b/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json new file mode 100644 index 0000000..fbd5bda --- /dev/null +++ b/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json @@ -0,0 +1,25 @@ +{ + "package" : "enedis.tic.core", + "name": "TICIdentifier", + "type": "DataDictionaryBase", + "attributes": [ + { + "name": "portId", + "mandatory": false, + "type": "String", + "emptyAllow": false + }, + { + "name": "portName", + "mandatory": false, + "type": "String", + "emptyAllow": false + }, + { + "name": "serialNumber", + "mandatory": false, + "type": "String", + "emptyAllow": false + } + ] +} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license b/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/main/java/enedis/lab/codec/Codec.java b/src/main/java/enedis/lab/codec/Codec.java new file mode 100644 index 0000000..e7932f8 --- /dev/null +++ b/src/main/java/enedis/lab/codec/Codec.java @@ -0,0 +1,36 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.codec; + +/** + * Codec interface + * + * @param + * @param + * + */ +public interface Codec +{ + /** + * Decode type K to type T + * + * @param object + * @return instance of T + * @throws CodecException + */ + public T decode(K object) throws CodecException; + + /** + * Encode type T to type K + * + * @param object + * @return instance of K + * @throws CodecException + */ + public K encode(T object) throws CodecException; +} diff --git a/src/main/java/enedis/lab/codec/CodecException.java b/src/main/java/enedis/lab/codec/CodecException.java new file mode 100644 index 0000000..678524d --- /dev/null +++ b/src/main/java/enedis/lab/codec/CodecException.java @@ -0,0 +1,169 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.codec; + +/** + * Codec exception + */ +public class CodecException extends Exception +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = 6029680699870915485L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Object data; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public CodecException() + { + super(); + } + + /** + * Constructor + * + * @param message + * @param cause + */ + public CodecException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor + * + * @param message + */ + public CodecException(String message) + { + super(message); + } + + /** + * Constructor + * + * @param message + * @param data + */ + public CodecException(String message, Object data) + { + super(message); + this.data = data; + } + + /** + * Constructor + * + * @param cause + */ + public CodecException(Throwable cause) + { + super(cause); + } + + /** + * Raise CodecException invalid value + * + * @param info + * @throws CodecException + */ + public static void raiseInvalidValue(String info) throws CodecException + { + throw new CodecException("Invalid value : " + info); + } + + /** + * Raise CodecException missing value + * + * @param value + * @throws CodecException + */ + public static void raiseMissingValue(String value) throws CodecException + { + throw new CodecException("Missing value : " + value); + } + + /** + * Raise CodecException inconsistency + * + * @param info + * @throws CodecException + */ + public static void raiseInconsistency(String info) throws CodecException + { + throw new CodecException("Inconsistency : " + info); + } + + /** + * Get data + * + * @return data + */ + public Object getData() + { + return this.data; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/io/PlugSubscriber.java b/src/main/java/enedis/lab/io/PlugSubscriber.java new file mode 100644 index 0000000..41f007a --- /dev/null +++ b/src/main/java/enedis/lab/io/PlugSubscriber.java @@ -0,0 +1,31 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io; + +import enedis.lab.util.task.Subscriber; + +/** + * Interface used to be notified when something has been plugged or unplugged + * + * @param the information class associated with plugged or unplugged notification + */ +public interface PlugSubscriber extends Subscriber +{ + /** + * Notification when something has been plugged + * + * @param info the information on what has been plugged + */ + public void onPlugged(T info); + /** + * Notification when something has been unplugged + * + * @param info the information on what has been unplugged + */ + public void onUnplugged(T info); +} diff --git a/src/main/java/enedis/lab/io/PortFinder.java b/src/main/java/enedis/lab/io/PortFinder.java new file mode 100644 index 0000000..eda1e69 --- /dev/null +++ b/src/main/java/enedis/lab/io/PortFinder.java @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io; + +import enedis.lab.types.DataList; + +/** + * Interface used to find all port descriptor + * @param the port descriptor class + */ +public interface PortFinder +{ + /** + * Find all port descriptor + * + * @return The port descriptor list found + */ + public DataList findAll(); +} diff --git a/src/main/java/enedis/lab/io/PortPlugNotifier.java b/src/main/java/enedis/lab/io/PortPlugNotifier.java new file mode 100644 index 0000000..9b4a8fd --- /dev/null +++ b/src/main/java/enedis/lab/io/PortPlugNotifier.java @@ -0,0 +1,214 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io; + +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicReference; + +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataList; +import enedis.lab.util.task.TaskPeriodicWithSubscribers; +import enedis.lab.util.time.Time; + +/** + * Class used to notify when a port has been plugged or unplugged + * + * @param + * the port finder interface + * @param + * the port descriptor class + */ +public class PortPlugNotifier, T> extends TaskPeriodicWithSubscribers> +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Program writing on the output stream when a port has been plugged or unplugged + * + * @param + * the port finder interface + * @param + * the port descriptor class + * @param notifier + * the port plug notifier instance + * @param subscriber + * the port plus subscriber instance + */ + public static , T> void main(PortPlugNotifier notifier, PlugSubscriber subscriber) + { + /* 1. Add program shutdown hook when CTRL+C is pressed to exit program properly */ + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() + { + @Override + public void run() + { + notifier.unsubscribe(subscriber); + notifier.stop(); + } + })); + /* 4. Add a subscriber to notification service */ + notifier.subscribe(subscriber); + /* 5. Start the notification service */ + notifier.start(); + /* 6. Execute forever loop until CTRL+C is pressed */ + while (notifier.isRunning()) + { + Time.sleep(1000); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private AtomicReference finder = new AtomicReference(); + private DataList descriptors = new DataArrayList(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor with all parameters + * + * @param period + * the period (in milliseconds) used to look for plugged or unplugged serial port + * @param finder + * the serial port finder interface used to find all serial port descriptors + */ + public PortPlugNotifier(long period, F finder) + { + super(period); + this.setFinder(finder); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set the finder + * + * @param finder + * the serial port finder interface used to find all serial port descriptors + * @throws IllegalArgumentException + * if finder is null + */ + public void setFinder(F finder) + { + if (finder == null) + { + throw new IllegalArgumentException("Cannot set null finder"); + } + this.finder.set(finder); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void process() + { + DataList newDescriptors = this.finder.get().findAll(); + + this.checkAndNotifyIfUnplugged(newDescriptors); + this.checkAndNotifyIfPlugged(newDescriptors); + this.updateDescriptors(newDescriptors); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void checkAndNotifyIfUnplugged(DataList newDescriptors) + { + Iterator it = this.descriptors.iterator(); + + while (it.hasNext()) + { + T descriptor = it.next(); + if (!newDescriptors.contains(descriptor)) + { + this.notifyOnUnplugged(descriptor); + } + } + } + + private void checkAndNotifyIfPlugged(DataList newDescriptors) + { + Iterator it = newDescriptors.iterator(); + + while (it.hasNext()) + { + T newDescriptor = it.next(); + if (!this.descriptors.contains(newDescriptor)) + { + this.notifyOnPlugged(newDescriptor); + } + } + } + + private void updateDescriptors(DataList newDescriptors) + { + synchronized (this.descriptors) + { + this.descriptors.clear(); + this.descriptors.addAll(newDescriptors); + } + } + + private void notifyOnPlugged(T info) + { + for (PlugSubscriber subscriber : this.notifier.getSubscribers()) + { + subscriber.onPlugged(info); + } + } + + private void notifyOnUnplugged(T info) + { + for (PlugSubscriber subscriber : this.notifier.getSubscribers()) + { + subscriber.onUnplugged(info); + } + } +} diff --git a/src/main/java/enedis/lab/io/channels/Channel.java b/src/main/java/enedis/lab/io/channels/Channel.java new file mode 100644 index 0000000..45ee2ec --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/Channel.java @@ -0,0 +1,110 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import enedis.lab.util.task.Notifier; +import enedis.lab.util.task.Task; + +/** + * Channel interface + */ +public interface Channel extends Task, Notifier +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Setup the channel from a configuration + * + * @param configuration + * the channel configuration + * + * @throws ChannelException + */ + public void setup(ChannelConfiguration configuration) throws ChannelException; + + /** + * Read data on the channel + * + * @return bytes + * @throws ChannelException + */ + public byte[] read() throws ChannelException; + + /** + * Write data on the channel + * + * @param data + * @throws ChannelException + */ + public void write(byte[] data) throws ChannelException; + + /** + * Get the channel name + * + * @return the channel's name + */ + public String getName(); + + /** + * Get the channel protocol + * + * @return the protocol used by the channel + */ + public ChannelProtocol getProtocol(); + + /** + * Get the channel direction + * + * @return the direction of the channel + */ + public ChannelDirection getDirection(); + + /** + * Get channel status + * + * @return the channel's status + */ + public ChannelStatus getStatus(); + + /** + * Get channel configuration + * + * @return channel configuration + */ + public ChannelConfiguration getConfiguration(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // none + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // none +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelBase.java b/src/main/java/enedis/lab/io/channels/ChannelBase.java new file mode 100644 index 0000000..140687a --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/ChannelBase.java @@ -0,0 +1,143 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.task.TaskPeriodic; + +/** + * Channel base + */ +public abstract class ChannelBase extends TaskPeriodic implements Channel +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected Logger logger; + protected ChannelConfiguration configuration; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor + * + * @param configuration + * configuration used to set the channel + * @throws ChannelException + */ + protected ChannelBase(ChannelConfiguration configuration) throws ChannelException + { + this.logger = LogManager.getLogger(this.getClass()); + this.setup(configuration); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Channel + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void setup(ChannelConfiguration configuration) throws ChannelException + { + /* 1. Check configuration */ + if (configuration == null) + { + ChannelException.raiseInvalidConfiguration("null"); + } + /* 2. Copy configuration */ + ChannelConfiguration configurationBackUp = null; + try + { + if (this.configuration == null) + { + this.configuration = (ChannelConfiguration) configuration.clone(); + } + else + { + configurationBackUp = (ChannelConfiguration) this.configuration.clone(); + this.configuration.copy(configuration); + } + } + catch (DataDictionaryException exception) + { + ChannelException.raiseInvalidConfiguration(exception.getMessage()); + } + /* 3. Set up from internal configuration */ + try + { + this.setup(); + } + catch (Exception exception) + { + /* 4. Restore internal configuration */ + if (configurationBackUp != null) + { + try + { + this.configuration.copy(configurationBackUp); + } + catch (DataDictionaryException dataDictionaryException) + { + this.logger.error("", dataDictionaryException); + } + } + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setup() throws ChannelException + { + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java b/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java new file mode 100644 index 0000000..18fac19 --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java @@ -0,0 +1,296 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.configuration.ConfigurationBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * ChannelConfiguration class + * + * Generated + */ +public class ChannelConfiguration extends ConfigurationBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_NAME = "name"; + protected static final String KEY_PROTOCOL = "protocol"; + protected static final String KEY_DIRECTION = "direction"; + protected static final String KEY_ALIAS = "alias"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorString kName; + protected KeyDescriptorEnum kProtocol; + protected KeyDescriptorEnum kDirection; + protected KeyDescriptorString kAlias; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected ChannelConfiguration() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ChannelConfiguration(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ChannelConfiguration(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * + * Constructor setting configuration name/file and parameters to default values + * + * @param name + * the configuration name + * @param file + * the configuration file + */ + public ChannelConfiguration(String name, File file) + { + this(); + this.init(name, file); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param protocol + * @param direction + * @param alias + * @throws DataDictionaryException + */ + public ChannelConfiguration(String name, ChannelProtocol protocol, ChannelDirection direction, String alias) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setProtocol(protocol); + this.setDirection(direction); + this.setAlias(alias); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// ConfigurationBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get name + * + * @return the name + */ + public String getName() + { + return (String) this.data.get(KEY_NAME); + } + + /** + * Get protocol + * + * @return the protocol + */ + public ChannelProtocol getProtocol() + { + return (ChannelProtocol) this.data.get(KEY_PROTOCOL); + } + + /** + * Get direction + * + * @return the direction + */ + public ChannelDirection getDirection() + { + return (ChannelDirection) this.data.get(KEY_DIRECTION); + } + + /** + * Get alias + * + * @return the alias + */ + public String getAlias() + { + return (String) this.data.get(KEY_ALIAS); + } + + /** + * Set name + * + * @param name + * @throws DataDictionaryException + */ + public void setName(String name) throws DataDictionaryException + { + this.setName((Object) name); + } + + /** + * Set protocol + * + * @param protocol + * @throws DataDictionaryException + */ + public void setProtocol(ChannelProtocol protocol) throws DataDictionaryException + { + this.setProtocol((Object) protocol); + } + + /** + * Set direction + * + * @param direction + * @throws DataDictionaryException + */ + public void setDirection(ChannelDirection direction) throws DataDictionaryException + { + this.setDirection((Object) direction); + } + + /** + * Set alias + * + * @param alias + * @throws DataDictionaryException + */ + public void setAlias(String alias) throws DataDictionaryException + { + this.setAlias((Object) alias); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setName(Object name) throws DataDictionaryException + { + this.data.put(KEY_NAME, this.kName.convert(name)); + } + + protected void setProtocol(Object protocol) throws DataDictionaryException + { + this.data.put(KEY_PROTOCOL, this.kProtocol.convert(protocol)); + } + + protected void setDirection(Object direction) throws DataDictionaryException + { + this.data.put(KEY_DIRECTION, this.kDirection.convert(direction)); + } + + protected void setAlias(Object alias) throws DataDictionaryException + { + this.data.put(KEY_ALIAS, this.kAlias.convert(alias)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kName = new KeyDescriptorString(KEY_NAME, true, false); + this.keys.add(this.kName); + + this.kProtocol = new KeyDescriptorEnum(KEY_PROTOCOL, true, ChannelProtocol.class); + this.keys.add(this.kProtocol); + + this.kDirection = new KeyDescriptorEnum(KEY_DIRECTION, true, ChannelDirection.class); + this.keys.add(this.kDirection); + + this.kAlias = new KeyDescriptorString(KEY_ALIAS, false, false); + this.keys.add(this.kAlias); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelDirection.java b/src/main/java/enedis/lab/io/channels/ChannelDirection.java new file mode 100644 index 0000000..95398f3 --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/ChannelDirection.java @@ -0,0 +1,22 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +/** + * Enumeration of channel direction + * + */ +public enum ChannelDirection +{ + /** Reception */ + RX, + /** Sending */ + TX, + /** Reception and sending */ + RXTX +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelException.java b/src/main/java/enedis/lab/io/channels/ChannelException.java new file mode 100644 index 0000000..18ff7c0 --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/ChannelException.java @@ -0,0 +1,194 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import enedis.lab.types.ExceptionBase; + +/** + * Class used for the channel exceptions + */ +public class ChannelException extends ExceptionBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -3507876436535833098L; + + /** Invalid channel error code */ + public static final int ERRCODE_INVALID_CHANNEL = -5; + /** Already exists error code */ + public static final int ERRCODE_ALREADY_EXISTS = -6; + /** Internal error error code */ + public static final int ERRCODE_INTERNAL_ERROR = -7; + /** Channel not ready error code */ + public static final int ERRCODE_CHANNEL_NOT_READY = -8; + /** Invalid configuration type error code */ + public static final int ERRCODE_INVALID_CONFIGURATION_TYPE = -9; + /** Invalid configuration error code */ + public static final int ERRCODE_INVALID_CONFIGURATION = -10; + /** Operation denied error code */ + public static final int ERRCODE_OPERATION_DENIED = -11; + /** Channel doesn't exist error code */ + public static final int ERRCODE_CHANNEL_DOESNT_EXIST = -12; + /** Unexpected error code */ + public static final int ERRCODE_UNEXPECTED = -99; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @param channelType + * @throws ChannelException + */ + public static void raiseUnhandledChannelType(String channelType) throws ChannelException + { + throw new ChannelException(ERRCODE_INVALID_CHANNEL, channelType + " channel is not handled"); + } + + /** + * @param channelType + * @param expectedChannelType + * @throws ChannelException + */ + public static void raiseConflictingChannelType(String channelType, String expectedChannelType) throws ChannelException + { + throw new ChannelException(ERRCODE_INVALID_CHANNEL, channelType + " channel is conflicting with " + expectedChannelType); + } + + /** + * @param info + * @throws ChannelException + */ + public static void raiseInternalError(String info) throws ChannelException + { + throw new ChannelException(ERRCODE_INTERNAL_ERROR, info); + } + + /** + * @param info + * @throws ChannelException + */ + public static void raiseChannelNotReady(String info) throws ChannelException + { + throw new ChannelException(ERRCODE_CHANNEL_NOT_READY, info); + } + + /** + * @param configuration + * @param expected_configuration_name + * @throws ChannelException + */ + public static void raiseInvalidConfigurationType(ChannelConfiguration configuration, String expected_configuration_name) throws ChannelException + { + throw new ChannelException(ERRCODE_INVALID_CONFIGURATION_TYPE, + "Configuration " + configuration.getClass().getSimpleName() + " is not a valid type (" + expected_configuration_name + " expected)"); + } + + /** + * @param info + * @throws ChannelException + */ + public static void raiseInvalidConfiguration(String info) throws ChannelException + { + throw new ChannelException(ERRCODE_INVALID_CONFIGURATION, "Configuration is invalid (" + info + ")"); + } + + /** + * @param operation + * @throws ChannelException + */ + public static void raiseOperationDenied(String operation) throws ChannelException + { + throw new ChannelException(ERRCODE_OPERATION_DENIED, "Operation \'" + operation + "\' is not allowed"); + } + + /** + * @param info + * @throws ChannelException + */ + public static void raiseUnexpectedError(String info) throws ChannelException + { + throw new ChannelException(ERRCODE_UNEXPECTED, "Unexpected error occurs : " + info); + } + + /** + * @param channelName + * @throws ChannelException + */ + public static void raiseAlreadyExists(String channelName) throws ChannelException + { + throw new ChannelException(ERRCODE_ALREADY_EXISTS, "Channel " + channelName + " already exists"); + } + + /** + * @param channelName + * @throws ChannelException + */ + public static void raiseDoesntExist(String channelName) throws ChannelException + { + throw new ChannelException(ERRCODE_CHANNEL_DOESNT_EXIST, "Channel " + channelName + " doesn't exist"); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @param code + * @param info + */ + public ChannelException(int code, String info) + { + super(code, info); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelListener.java b/src/main/java/enedis/lab/io/channels/ChannelListener.java new file mode 100644 index 0000000..e31a37d --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/ChannelListener.java @@ -0,0 +1,73 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import enedis.lab.types.DataDictionary; +import enedis.lab.util.task.Subscriber; + +/** + * Channel listener + */ +public interface ChannelListener extends Subscriber +{ + /** + * On data read + * + * @param channelName + * the channel name + * @param data + * the bytes read + */ + public void onDataRead(String channelName, byte[] data); + + /** + * On data written + * + * @param channelName + * the channel name + * @param data + * the bytes written + */ + public void onDataWritten(String channelName, byte[] data); + + /** + * On status changed + * + * @param channelName + * the channel name + * @param status + * the new status + */ + public void onStatusChanged(String channelName, ChannelStatus status); + + /** + * On error detected + * + * @param channelName + * the channel name + * @param errorCode + * the error code + * @param errorMessage + * the error message + */ + public void onErrorDetected(String channelName, int errorCode, String errorMessage); + + /** + * On error detected + * + * @param channelName + * the channel name + * @param errorCode + * the error code + * @param errorMessage + * the error message + * @param data + * the data associated with error message + */ + public void onErrorDetected(String channelName, int errorCode, String errorMessage, DataDictionary data); +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelPhysical.java b/src/main/java/enedis/lab/io/channels/ChannelPhysical.java new file mode 100644 index 0000000..5295653 --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/ChannelPhysical.java @@ -0,0 +1,227 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import java.util.Collection; + +import enedis.lab.util.task.NotifierBase; + +/** + * Channel physical + */ +public abstract class ChannelPhysical extends ChannelBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private NotifierBase notifier; + protected ChannelStatus status; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor + * + * @param configuration + * configuration used to set the channel + * @throws ChannelException + */ + protected ChannelPhysical(ChannelConfiguration configuration) throws ChannelException + { + super(configuration); + this.init(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Channel + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void subscribe(ChannelListener listener) + { + this.notifier.subscribe(listener); + } + + @Override + public void unsubscribe(ChannelListener listener) + { + this.notifier.unsubscribe(listener); + } + + @Override + public boolean hasSubscriber(ChannelListener subscriber) + { + return this.notifier.hasSubscriber(subscriber); + } + + @Override + public Collection getSubscribers() + { + return this.notifier.getSubscribers(); + } + + @Override + public String getName() + { + return this.configuration.getName(); + } + + @Override + public ChannelProtocol getProtocol() + { + return this.configuration.getProtocol(); + } + + @Override + public ChannelDirection getDirection() + { + return this.configuration.getDirection(); + } + + @Override + public ChannelStatus getStatus() + { + return this.status; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ChannelConfiguration getConfiguration() + { + return this.configuration; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set the channel status + * + * @param status + * channel status + */ + protected void setStatus(ChannelStatus status) + { + this.setStatus(status, true); + } + + /** + * Set the channel status. If 'notify' argument is false, no channel listener will be notified about a new status, + * otherwise, the will + * + * @param status + * channel status + * @param notify + * if 'true', the channel's listener will be notified, else it will not + */ + protected void setStatus(ChannelStatus status, boolean notify) + { + if (status != this.status) + { + this.status = status; + if (status == ChannelStatus.ERROR) + { + this.logger.error("Channel " + this.getName() + " new status : " + status.name()); + } + else + { + this.logger.info("Channel " + this.getName() + " new status : " + status.name()); + } + if (true == notify) + { + this.notifyOnStatusChanged(); + } + } + } + + protected void notifyOnDataRead(byte[] data) + { + if (data != null) + { + for (ChannelListener subscriber : this.notifier.getSubscribers()) + { + subscriber.onDataRead(this.getName(), data); + } + } + } + + protected void notifyOnDataWritten(byte[] data) + { + if (data != null) + { + for (ChannelListener subscriber : this.notifier.getSubscribers()) + { + subscriber.onDataWritten(this.getName(), data); + } + } + } + + protected void notifyOnStatusChanged() + { + for (ChannelListener subscriber : this.notifier.getSubscribers()) + { + subscriber.onStatusChanged(this.getName(), this.status); + } + } + + protected void notifyOnErrorDetected(int errorCode, String errorMessage) + { + for (ChannelListener subscriber : this.notifier.getSubscribers()) + { + subscriber.onErrorDetected(this.getName(), errorCode, errorMessage); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void init() + { + this.status = ChannelStatus.STOPPED; + this.notifier = new NotifierBase(); + } +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java b/src/main/java/enedis/lab/io/channels/ChannelProtocol.java new file mode 100644 index 0000000..82206e5 --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/ChannelProtocol.java @@ -0,0 +1,30 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +/** + * Enumeration of channel protocol + * + */ +public enum ChannelProtocol +{ + /** File */ + FILE, + /** Serial port */ + SERIAL_PORT, + /** RTU modbus */ + MODBUS_RTU, + /** TCP modbus */ + MODBUS_TCP, + /** modbus stub */ + MODBUS_STUB, + /** TCP */ + TCP, + /** UDP */ + UDP, +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelStatus.java b/src/main/java/enedis/lab/io/channels/ChannelStatus.java new file mode 100644 index 0000000..acae4c5 --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/ChannelStatus.java @@ -0,0 +1,22 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +/** + * Enumeration of channel status + * + */ +public enum ChannelStatus +{ + /** Stopped */ + STOPPED, + /** Started */ + STARTED, + /** Error */ + ERROR +} diff --git a/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java b/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java new file mode 100644 index 0000000..de7e171 --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java @@ -0,0 +1,432 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels.serialport; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.io.channels.ChannelConfiguration; +import enedis.lab.io.channels.ChannelDirection; +import enedis.lab.io.channels.ChannelProtocol; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import enedis.lab.types.datadictionary.KeyDescriptorNumber; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * ChannelSerialPortConfiguration class + * + * Generated + */ +public class ChannelSerialPortConfiguration extends ChannelConfiguration +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_PORT_ID = "portId"; + protected static final String KEY_PORT_NAME = "portName"; + protected static final String KEY_BAUDRATE = "baudrate"; + protected static final String KEY_PARITY = "parity"; + protected static final String KEY_DATA_BITS = "dataBits"; + protected static final String KEY_STOP_BITS = "stopBits"; + protected static final String KEY_SYNC_READ_TIMEOUT = "syncReadTimeout"; + + private static final ChannelProtocol PROTOCOL_ACCEPTED_VALUE = ChannelProtocol.SERIAL_PORT; + private static final ChannelDirection DIRECTION_ACCEPTED_VALUE = ChannelDirection.RXTX; + private static final Number[] BAUDRATE_ACCEPTED_VALUES = { 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1843200, + 3686400 }; + private static final Number[] DATA_BITS_ACCEPTED_VALUES = { 5, 6, 7, 8 }; + private static final Number[] STOP_BITS_ACCEPTED_VALUES = { 1.0d, 1.5d, 2.0d }; + private static final Number SYNC_READ_TIMEOUT_DEFAULT_VALUE = 10000; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorString kPortId; + protected KeyDescriptorString kPortName; + protected KeyDescriptorNumber kBaudrate; + protected KeyDescriptorEnum kParity; + protected KeyDescriptorNumber kDataBits; + protected KeyDescriptorNumber kStopBits; + protected KeyDescriptorNumber kSyncReadTimeout; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected ChannelSerialPortConfiguration() + { + super(); + this.loadKeyDescriptors(); + + this.kProtocol.setAcceptedValues(PROTOCOL_ACCEPTED_VALUE); + this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUE); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ChannelSerialPortConfiguration(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ChannelSerialPortConfiguration(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * + * Constructor setting configuration name/file and parameters to default values + * + * @param name + * the configuration name + * @param file + * the configuration file + */ + public ChannelSerialPortConfiguration(String name, File file) + { + this(); + this.init(name, file); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param alias + * @param portId + * @param portName + * @param baudrate + * @param parity + * @param dataBits + * @param stopBits + * @param syncReadTimeout + * @throws DataDictionaryException + */ + public ChannelSerialPortConfiguration(String name, String alias, String portId, String portName, Number baudrate, Parity parity, Number dataBits, Number stopBits, + Number syncReadTimeout) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setAlias(alias); + this.setPortId(portId); + this.setPortName(portName); + this.setBaudrate(baudrate); + this.setParity(parity); + this.setDataBits(dataBits); + this.setStopBits(stopBits); + this.setSyncReadTimeout(syncReadTimeout); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// ChannelConfiguration + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_PORT_ID) && !this.exists(KEY_PORT_NAME)) + { + throw new DataDictionaryException("Neither " + KEY_PORT_ID + " nor " + KEY_PORT_NAME + " is defined"); + } + if (!this.exists(KEY_PROTOCOL)) + { + this.setProtocol(PROTOCOL_ACCEPTED_VALUE); + } + if (!this.exists(KEY_DIRECTION)) + { + this.setDirection(DIRECTION_ACCEPTED_VALUE); + } + if (!this.exists(KEY_SYNC_READ_TIMEOUT)) + { + this.setSyncReadTimeout(SYNC_READ_TIMEOUT_DEFAULT_VALUE); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get port id + * + * @return the port id + */ + public String getPortId() + { + return (String) this.data.get(KEY_PORT_ID); + } + + /** + * Get port name + * + * @return the port name + */ + public String getPortName() + { + return (String) this.data.get(KEY_PORT_NAME); + } + + /** + * Get baudrate + * + * @return the baudrate + */ + public Number getBaudrate() + { + return (Number) this.data.get(KEY_BAUDRATE); + } + + /** + * Get parity + * + * @return the parity + */ + public Parity getParity() + { + return (Parity) this.data.get(KEY_PARITY); + } + + /** + * Get data bits + * + * @return the data bits + */ + public Number getDataBits() + { + return (Number) this.data.get(KEY_DATA_BITS); + } + + /** + * Get stop bits + * + * @return the stop bits + */ + public Number getStopBits() + { + return (Number) this.data.get(KEY_STOP_BITS); + } + + /** + * Get sync read timeout + * + * @return the sync read timeout + */ + public Number getSyncReadTimeout() + { + return (Number) this.data.get(KEY_SYNC_READ_TIMEOUT); + } + + /** + * Set port id + * + * @param portId + * @throws DataDictionaryException + */ + public void setPortId(String portId) throws DataDictionaryException + { + this.setPortId((Object) portId); + } + + /** + * Set port name + * + * @param portName + * @throws DataDictionaryException + */ + public void setPortName(String portName) throws DataDictionaryException + { + this.setPortName((Object) portName); + } + + /** + * Set baudrate + * + * @param baudrate + * @throws DataDictionaryException + */ + public void setBaudrate(Number baudrate) throws DataDictionaryException + { + this.setBaudrate((Object) baudrate); + } + + /** + * Set parity + * + * @param parity + * @throws DataDictionaryException + */ + public void setParity(Parity parity) throws DataDictionaryException + { + this.setParity((Object) parity); + } + + /** + * Set data bits + * + * @param dataBits + * @throws DataDictionaryException + */ + public void setDataBits(Number dataBits) throws DataDictionaryException + { + this.setDataBits((Object) dataBits); + } + + /** + * Set stop bits + * + * @param stopBits + * @throws DataDictionaryException + */ + public void setStopBits(Number stopBits) throws DataDictionaryException + { + this.setStopBits((Object) stopBits); + } + + /** + * Set sync read timeout + * + * @param syncReadTimeout + * @throws DataDictionaryException + */ + public void setSyncReadTimeout(Number syncReadTimeout) throws DataDictionaryException + { + this.setSyncReadTimeout((Object) syncReadTimeout); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setPortId(Object portId) throws DataDictionaryException + { + this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); + } + + protected void setPortName(Object portName) throws DataDictionaryException + { + this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); + } + + protected void setBaudrate(Object baudrate) throws DataDictionaryException + { + this.data.put(KEY_BAUDRATE, this.kBaudrate.convert(baudrate)); + } + + protected void setParity(Object parity) throws DataDictionaryException + { + this.data.put(KEY_PARITY, this.kParity.convert(parity)); + } + + protected void setDataBits(Object dataBits) throws DataDictionaryException + { + this.data.put(KEY_DATA_BITS, this.kDataBits.convert(dataBits)); + } + + protected void setStopBits(Object stopBits) throws DataDictionaryException + { + this.data.put(KEY_STOP_BITS, this.kStopBits.convert(stopBits)); + } + + protected void setSyncReadTimeout(Object syncReadTimeout) throws DataDictionaryException + { + this.data.put(KEY_SYNC_READ_TIMEOUT, this.kSyncReadTimeout.convert(syncReadTimeout)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); + this.keys.add(this.kPortId); + + this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); + this.keys.add(this.kPortName); + + this.kBaudrate = new KeyDescriptorNumber(KEY_BAUDRATE, true); + this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); + this.keys.add(this.kBaudrate); + + this.kParity = new KeyDescriptorEnum(KEY_PARITY, true, Parity.class); + this.keys.add(this.kParity); + + this.kDataBits = new KeyDescriptorNumber(KEY_DATA_BITS, true); + this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUES); + this.keys.add(this.kDataBits); + + this.kStopBits = new KeyDescriptorNumber(KEY_STOP_BITS, true); + this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUES); + this.keys.add(this.kStopBits); + + this.kSyncReadTimeout = new KeyDescriptorNumber(KEY_SYNC_READ_TIMEOUT, false); + this.keys.add(this.kSyncReadTimeout); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/channels/serialport/Parity.java b/src/main/java/enedis/lab/io/channels/serialport/Parity.java new file mode 100644 index 0000000..b5caaf4 --- /dev/null +++ b/src/main/java/enedis/lab/io/channels/serialport/Parity.java @@ -0,0 +1,25 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels.serialport; + +/** + * Parity enum + */ +public enum Parity +{ + /** None */ + NONE, + /** Even */ + EVEN, + /** Odd */ + ODD, + /** Mark */ + MARK, + /** Space */ + SPACE +} \ No newline at end of file diff --git a/src/main/java/enedis/lab/io/datastreams/DataInputStream.java b/src/main/java/enedis/lab/io/datastreams/DataInputStream.java new file mode 100644 index 0000000..430d8bd --- /dev/null +++ b/src/main/java/enedis/lab/io/datastreams/DataInputStream.java @@ -0,0 +1,99 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +import enedis.lab.types.DataDictionary; + +/** + * Data input stream + */ +public abstract class DataInputStream extends DataStreamBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor using configuration + * + * @param configuration + * @throws DataStreamException + */ + public DataInputStream(DataStreamConfiguration configuration) throws DataStreamException + { + super(configuration); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataStream + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public final void write(DataDictionary data) throws DataStreamException + { + DataStreamException.raiseOperationDenied("write"); + } + + @Override + public void setup(DataStreamConfiguration configuration) throws DataStreamException + { + super.setup(configuration); + + if (DataStreamDirection.INPUT != configuration.getDirection()) + { + DataStreamException.raiseInvalidConfiguration(DataStreamConfiguration.KEY_DIRECTION + "=" + configuration.getDirection().name() + "(expected " + DataStreamDirection.INPUT.name() + ")"); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStream.java b/src/main/java/enedis/lab/io/datastreams/DataStream.java new file mode 100644 index 0000000..fb89041 --- /dev/null +++ b/src/main/java/enedis/lab/io/datastreams/DataStream.java @@ -0,0 +1,38 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +/** + * DataStream interface + */ +public interface DataStream extends DataStreamUser +{ + /** + * Open the stream + * + * @throws DataStreamException + */ + public void open() throws DataStreamException; + + /** + * Close the stream + * + * @throws DataStreamException + */ + public void close() throws DataStreamException; + + /** + * Setup the stream from a configuration + * + * @param configuration + * configuration given for the stream + * @throws DataStreamException + * if an error occurs + */ + public void setup(DataStreamConfiguration configuration) throws DataStreamException; +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java b/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java new file mode 100644 index 0000000..578325f --- /dev/null +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java @@ -0,0 +1,373 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +import java.util.Collection; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import enedis.lab.io.channels.Channel; +import enedis.lab.io.channels.ChannelListener; +import enedis.lab.io.channels.ChannelStatus; +import enedis.lab.types.DataDictionary; +import enedis.lab.util.task.NotifierBase; + +/** + * DataStream Base + */ +public abstract class DataStreamBase implements DataStream, ChannelListener +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected NotifierBase notifier; + protected DataStreamConfiguration configuration; + private DataStreamStatus status; + protected Channel channel; + protected Logger logger; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor + * + * @param configuration + * configuration used to set the stream + * @throws DataStreamException + */ + public DataStreamBase(DataStreamConfiguration configuration) throws DataStreamException + { + this.init(configuration); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// ChannelListener + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onStatusChanged(String channelName, ChannelStatus channelStatus) + { + DataStreamStatus status_cpy = this.status; + + switch (channelStatus) + { + case STOPPED: + { + if (DataStreamStatus.CLOSED != this.status) + { + this.setStatus(DataStreamStatus.ERROR); + } + break; + } + case STARTED: + { + if (DataStreamStatus.ERROR == this.status) + { + this.setStatus(DataStreamStatus.OPENED); + } + break; + } + case ERROR: + { + this.setStatus(DataStreamStatus.ERROR); + break; + } + + default: + { + + } + } + + if (!this.notifier.getSubscribers().isEmpty() && (status_cpy != this.status)) + { + this.notifyOnStatusChanged(this.status); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataStream + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void open() throws DataStreamException + { + String channelName = this.configuration.getChannelName(); + + if (null != channelName) + { + if (null != this.channel) + { + this.channel.subscribe(this); + this.setStatus(DataStreamStatus.OPENED); + this.logger.info("Stream " + this.getName() + " start"); + } + else + { + this.setStatus(DataStreamStatus.ERROR); + DataStreamException.raiseChannelError(channelName, "channel does not exist"); + } + } + } + + @Override + public void close() throws DataStreamException + { + String channelName = this.configuration.getChannelName(); + + if (null != channelName) + { + if (null != this.channel) + { + this.channel.unsubscribe(this); + this.setStatus(DataStreamStatus.CLOSED); + this.logger.info("Service " + this.getName() + " stop"); + } + else + { + this.setStatus(DataStreamStatus.ERROR); + DataStreamException.raiseChannelError(channelName, "channel does not exist"); + } + } + } + + @Override + public void setup(DataStreamConfiguration configuration) throws DataStreamException + { + if (this.status == DataStreamStatus.OPENED) + { + DataStreamException.raiseInternalError("Cannot setup stream " + this.getName() + " (already open)"); + } + this.configuration = configuration; + } + + @Override + public void subscribe(DataStreamListener listener) + { + this.notifier.subscribe(listener); + } + + @Override + public void unsubscribe(DataStreamListener listener) + { + this.notifier.unsubscribe(listener); + } + + @Override + public boolean hasSubscriber(DataStreamListener subscriber) + { + return this.notifier.hasSubscriber(subscriber); + } + + @Override + public Collection getSubscribers() + { + return this.notifier.getSubscribers(); + } + + @Override + public String getName() + { + return this.configuration.getName(); + } + + @Override + public DataStreamConfiguration getConfiguration() + { + return (DataStreamConfiguration) this.configuration.clone(); + } + + @Override + public DataStreamDirection getDirection() + { + return this.configuration.getDirection(); + } + + @Override + public DataStreamType getType() + { + return this.configuration.getType(); + } + + @Override + public DataStreamStatus getStatus() + { + return this.status; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set the channel reference + * + * @param channel + */ + public void setChannel(Channel channel) + { + this.channel = channel; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Notify onDataReceived listeners + * + * @param data + * new data received + */ + protected void notifyOnDataReceived(DataDictionary data) + { + if (data != null) + { + for (DataStreamListener listener : this.notifier.getSubscribers()) + { + listener.onDataReceived(this.getName(), data); + } + } + } + + /** + * Notify onDataSent listeners + * + * @param data + * new data sent + */ + protected void notifyOnDataSent(DataDictionary data) + { + if (data != null) + { + for (DataStreamListener listener : this.notifier.getSubscribers()) + { + listener.onDataSent(this.getName(), data); + } + } + } + + /** + * Notify onStatusChanged listeners + * + * @param newStatus + * new status + */ + protected void notifyOnStatusChanged(DataStreamStatus newStatus) + { + if (newStatus != null) + { + for (DataStreamListener listener : this.notifier.getSubscribers()) + { + listener.onStatusChanged(this.getName(), newStatus); + } + } + } + + /** + * Notify onErrorDetected listeners + * + * @param errorCode + * the error code + * @param errorMessage + * the error message + * @param data + * the data associated with the error + */ + protected void notifyOnErrorDetected(int errorCode, String errorMessage, DataDictionary data) + { + for (DataStreamListener listener : this.notifier.getSubscribers()) + { + listener.onErrorDetected(this.getName(), errorCode, errorMessage, data); + } + } + + /** + * Set the channel status + * + * @param status + * channel status + */ + protected void setStatus(DataStreamStatus status) + { + this.setStatus(status, true); + } + + /** + * Set the channel status. If 'notify' argument is false, no channel listener will be notified about a new status, + * otherwise, the will + * + * @param status + * channel status + * @param notify + * if 'true', the channel's listener will be notified, else it will not + */ + protected void setStatus(DataStreamStatus status, boolean notify) + { + if (status != this.status) + { + this.status = status; + + if (true == notify) + { + this.notifyOnStatusChanged(this.status); + } + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void init(DataStreamConfiguration configuration) throws DataStreamException + { + this.notifier = new NotifierBase(); + this.status = DataStreamStatus.UNKNOWN; + this.configuration = configuration; + this.logger = LogManager.getLogger(this.getClass()); + this.setup(configuration); + } +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java b/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java new file mode 100644 index 0000000..ddd6069 --- /dev/null +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java @@ -0,0 +1,296 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.configuration.ConfigurationBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * DataStreamConfiguration class + * + * Generated + */ +public class DataStreamConfiguration extends ConfigurationBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_NAME = "name"; + protected static final String KEY_TYPE = "type"; + protected static final String KEY_DIRECTION = "direction"; + protected static final String KEY_CHANNEL_NAME = "channelName"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorString kName; + protected KeyDescriptorEnum kType; + protected KeyDescriptorEnum kDirection; + protected KeyDescriptorString kChannelName; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected DataStreamConfiguration() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public DataStreamConfiguration(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public DataStreamConfiguration(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * + * Constructor setting configuration name/file and parameters to default values + * + * @param name + * the configuration name + * @param file + * the configuration file + */ + public DataStreamConfiguration(String name, File file) + { + this(); + this.init(name, file); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param type + * @param direction + * @param channelName + * @throws DataDictionaryException + */ + public DataStreamConfiguration(String name, DataStreamType type, DataStreamDirection direction, String channelName) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setType(type); + this.setDirection(direction); + this.setChannelName(channelName); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// ConfigurationBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get name + * + * @return the name + */ + public String getName() + { + return (String) this.data.get(KEY_NAME); + } + + /** + * Get type + * + * @return the type + */ + public DataStreamType getType() + { + return (DataStreamType) this.data.get(KEY_TYPE); + } + + /** + * Get direction + * + * @return the direction + */ + public DataStreamDirection getDirection() + { + return (DataStreamDirection) this.data.get(KEY_DIRECTION); + } + + /** + * Get channel name + * + * @return the channel name + */ + public String getChannelName() + { + return (String) this.data.get(KEY_CHANNEL_NAME); + } + + /** + * Set name + * + * @param name + * @throws DataDictionaryException + */ + public void setName(String name) throws DataDictionaryException + { + this.setName((Object) name); + } + + /** + * Set type + * + * @param type + * @throws DataDictionaryException + */ + public void setType(DataStreamType type) throws DataDictionaryException + { + this.setType((Object) type); + } + + /** + * Set direction + * + * @param direction + * @throws DataDictionaryException + */ + public void setDirection(DataStreamDirection direction) throws DataDictionaryException + { + this.setDirection((Object) direction); + } + + /** + * Set channel name + * + * @param channelName + * @throws DataDictionaryException + */ + public void setChannelName(String channelName) throws DataDictionaryException + { + this.setChannelName((Object) channelName); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setName(Object name) throws DataDictionaryException + { + this.data.put(KEY_NAME, this.kName.convert(name)); + } + + protected void setType(Object type) throws DataDictionaryException + { + this.data.put(KEY_TYPE, this.kType.convert(type)); + } + + protected void setDirection(Object direction) throws DataDictionaryException + { + this.data.put(KEY_DIRECTION, this.kDirection.convert(direction)); + } + + protected void setChannelName(Object channelName) throws DataDictionaryException + { + this.data.put(KEY_CHANNEL_NAME, this.kChannelName.convert(channelName)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kName = new KeyDescriptorString(KEY_NAME, true, false); + this.keys.add(this.kName); + + this.kType = new KeyDescriptorEnum(KEY_TYPE, true, DataStreamType.class); + this.keys.add(this.kType); + + this.kDirection = new KeyDescriptorEnum(KEY_DIRECTION, true, DataStreamDirection.class); + this.keys.add(this.kDirection); + + this.kChannelName = new KeyDescriptorString(KEY_CHANNEL_NAME, false, false); + this.keys.add(this.kChannelName); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java b/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java new file mode 100644 index 0000000..66e83e7 --- /dev/null +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java @@ -0,0 +1,21 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +/** + * DataStream direction + */ +public enum DataStreamDirection +{ + /** Input dataStream */ + INPUT, + /** Output dataStream */ + OUTPUT, + /** Inoutput dataStream */ + INOUTPUT; +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamException.java b/src/main/java/enedis/lab/io/datastreams/DataStreamException.java new file mode 100644 index 0000000..1410e0f --- /dev/null +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamException.java @@ -0,0 +1,205 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +/** + * Specific exception for data stream + */ +public class DataStreamException extends Exception +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = 4079445021346677107L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Raise channel error exception + * + * @param channelName + * @param info + * @throws DataStreamException + */ + public static void raiseChannelError(String channelName, String info) throws DataStreamException + { + throw new DataStreamException("Error occurs on channel " + channelName + " (" + info + ")"); + } + + /** + * Raise unhandle type exception + * + * @param type + * @throws DataStreamException + */ + public static void raiseUnhandledType(String type) throws DataStreamException + { + throw new DataStreamException(type + " datastream is not handled"); + } + + /** + * Raise internal error exception + * + * @param info + * @throws DataStreamException + */ + public static void raiseInternalError(String info) throws DataStreamException + { + throw new DataStreamException(info); + } + + /** + * Raise invalid configuration exception + * + * @param configuration + * @param expected_configuration_name + * @throws DataStreamException + */ + public static void raiseInvalidConfiguration(DataStreamConfiguration configuration, String expected_configuration_name) throws DataStreamException + { + throw new DataStreamException("Configuration " + configuration.getClass().getSimpleName() + " is not valid (" + expected_configuration_name + " expected)"); + } + + /** + * Raise invalid configuration exceotion + * + * @param info + * @throws DataStreamException + */ + public static void raiseInvalidConfiguration(String info) throws DataStreamException + { + throw new DataStreamException(info); + } + + /** + * Raise operation denied excpetion + * + * @param operation + * @throws DataStreamException + */ + public static void raiseOperationDenied(String operation) throws DataStreamException + { + throw new DataStreamException("Operation \'" + operation + "\' is not allowed"); + } + + /** + * Raise unexpected error exception + * + * @param info + * @throws DataStreamException + */ + public static void raiseUnexpectedError(String info) throws DataStreamException + { + throw new DataStreamException("Unexpected error occurs : " + info); + } + + /** + * Raise already exists exception + * + * @param dataStream + * @throws DataStreamException + */ + public static void raiseAlreadyExists(String dataStream) throws DataStreamException + { + throw new DataStreamException("DataStream " + dataStream + " already exists"); + } + + /** + * Raise creation failed exception + * + * @param dataStream + * @param info + * @throws DataStreamException + */ + public static void raiseCreationFailed(String dataStream, String info) throws DataStreamException + { + throw new DataStreamException("Creation of dataStream " + dataStream + " creation failed : " + info); + } + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Super class constructor + * + * @param message + * @param cause + */ + public DataStreamException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Super class constructor + * + * @param message + */ + public DataStreamException(String message) + { + super(message); + } + + /** + * Super class constructor + * + * @param cause + */ + public DataStreamException(Throwable cause) + { + super(cause); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java b/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java new file mode 100644 index 0000000..f9094ef --- /dev/null +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java @@ -0,0 +1,61 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +import enedis.lab.types.DataDictionary; +import enedis.lab.util.task.Subscriber; + +/** + * Data stream listener + */ +public interface DataStreamListener extends Subscriber +{ + /** + * Function call when data has been received + * + * @param dataStreamName + * the stream name + * @param data + * the data received + */ + public void onDataReceived(String dataStreamName, DataDictionary data); + + /** + * Function call when data has been sent + * + * @param dataStreamName + * the stream name + * @param data + * the data sent + */ + public void onDataSent(String dataStreamName, DataDictionary data); + + /** + * Function call when data stream status changed + * + * @param dataStreamName + * the stream name + * @param status + * the new status + */ + public void onStatusChanged(String dataStreamName, DataStreamStatus status); + + /** + * On error detected + * + * @param dataStreamName + * the stream name + * @param errorCode + * the error code + * @param errorMessage + * the error message + * @param data + * the data associated with error + */ + public void onErrorDetected(String dataStreamName, int errorCode, String errorMessage, DataDictionary data); +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java b/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java new file mode 100644 index 0000000..282be74 --- /dev/null +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java @@ -0,0 +1,23 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +/** + * DataStream status + */ +public enum DataStreamStatus +{ + /** Unknown, default status */ + UNKNOWN, + /** Closed */ + CLOSED, + /** Opened */ + OPENED, + /** Error */ + ERROR +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamType.java b/src/main/java/enedis/lab/io/datastreams/DataStreamType.java new file mode 100644 index 0000000..9d778d7 --- /dev/null +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamType.java @@ -0,0 +1,27 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +/** + * DataStream type + */ +public enum DataStreamType +{ + /** raw stream */ + RAW, + /** TIC stream */ + TIC, + /** IEC61850 stream */ + IEC61850, + /** Slave modbus stream */ + MODBUS_SLAVE, + /** Master modbus stream */ + MODBUS_MASTER, + /** LEM DC product */ + LEM_DC; +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java b/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java new file mode 100644 index 0000000..6e0e865 --- /dev/null +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java @@ -0,0 +1,123 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +import enedis.lab.types.DataDictionary; +import enedis.lab.util.task.Notifier; + +/** + * DataStream interface + */ +public interface DataStreamUser extends Notifier +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * read data on the stream + * + * @return data read + * @throws DataStreamException + */ + public DataDictionary read() throws DataStreamException; + + /** + * write data on the stream + * + * @param data + * @throws DataStreamException + */ + public void write(DataDictionary data) throws DataStreamException; + + /** + * Get data stream name + * + * @return the stream name + */ + public String getName(); + + /** + * Get stream configuration + * + * @return stream configuration + */ + public DataStreamConfiguration getConfiguration(); + + /** + * Get stream direction + * + * @return stream direction + */ + public DataStreamDirection getDirection(); + + /** + * Get stream type + * + * @return stream type + */ + public DataStreamType getType(); + + /** + * Get stream status + * + * @return stream status + */ + public DataStreamStatus getStatus(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java b/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java new file mode 100644 index 0000000..3c30eee --- /dev/null +++ b/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java @@ -0,0 +1,455 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.serialport; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorNumberMinMax; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * Class used to get the following serial port informations:
+ * - port unique identifier
+ * - port name used to open serial port
+ * - port description
+ * - USB device product identifier
+ * - USB device vendor identifier
+ * - USB device product name
+ * - USB device manufacturer
+ * - USB device serial number + */ +public class SerialPortDescriptor extends DataDictionaryBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_PORT_ID = "portId"; + protected static final String KEY_PORT_NAME = "portName"; + protected static final String KEY_DESCRIPTION = "description"; + protected static final String KEY_PRODUCT_ID = "productId"; + protected static final String KEY_VENDOR_ID = "vendorId"; + protected static final String KEY_PRODUCT_NAME = "productName"; + protected static final String KEY_MANUFACTURER = "manufacturer"; + protected static final String KEY_SERIAL_NUMBER = "serialNumber"; + + private static final Number PRODUCT_ID_MIN = 0; + private static final Number PRODUCT_ID_MAX = 65535; + private static final Number VENDOR_ID_MIN = 0; + private static final Number VENDOR_ID_MAX = 65535; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorString kPortId; + protected KeyDescriptorString kPortName; + protected KeyDescriptorString kDescription; + protected KeyDescriptorNumberMinMax kProductId; + protected KeyDescriptorNumberMinMax kVendorId; + protected KeyDescriptorString kProductName; + protected KeyDescriptorString kManufacturer; + protected KeyDescriptorString kSerialNumber; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Empty constructor + */ + public SerialPortDescriptor() + { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public SerialPortDescriptor(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using data dictionary + * + * @param other + * @throws DataDictionaryException + */ + public SerialPortDescriptor(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param portId + * the port unique identifier + * @param portName + * the port name used to open serial port + * @param description + * the port description + * @param productId + * the USB device product identifier + * @param vendorId + * the USB device vendor identifier + * @param productName + * the USB device product name + * @param manufacturer + * the USB device manufacturer + * @param serialNumber + * the USB device serial number + * @throws DataDictionaryException + * if any parameters is invalid + */ + public SerialPortDescriptor(String portId, String portName, String description, Number productId, Number vendorId, String productName, String manufacturer, String serialNumber) + throws DataDictionaryException + { + this(); + + this.setPortId(portId); + this.setPortName(portName); + this.setDescription(description); + this.setProductId(productId); + this.setVendorId(vendorId); + this.setProductName(productName); + this.setManufacturer(manufacturer); + this.setSerialNumber(serialNumber); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get port id + * + * @return the port unique identifier + */ + public String getPortId() + { + return (String) this.data.get(KEY_PORT_ID); + } + + /** + * Get port name + * + * @return the port name used to open serial port + */ + public String getPortName() + { + return (String) this.data.get(KEY_PORT_NAME); + } + + /** + * Get description + * + * @return the port description + */ + public String getDescription() + { + return (String) this.data.get(KEY_DESCRIPTION); + } + + /** + * Get product id + * + * @return the USB device product identifier + */ + public Number getProductId() + { + return (Number) this.data.get(KEY_PRODUCT_ID); + } + + /** + * Get vendor id + * + * @return the USB device vendor identifier + */ + public Number getVendorId() + { + return (Number) this.data.get(KEY_VENDOR_ID); + } + + /** + * Get product name + * + * @return the USB device product name + */ + public String getProductName() + { + return (String) this.data.get(KEY_PRODUCT_NAME); + } + + /** + * Get manufacturer + * + * @return the USB device manufacturer + */ + public String getManufacturer() + { + return (String) this.data.get(KEY_MANUFACTURER); + } + + /** + * Get serial number + * + * @return the USB device serial number + */ + public String getSerialNumber() + { + return (String) this.data.get(KEY_SERIAL_NUMBER); + } + + /** + * Set port id + * + * @param portId + * the port unique identifier + * @throws DataDictionaryException + * if portId is empty + */ + public void setPortId(String portId) throws DataDictionaryException + { + this.setPortId((Object) portId); + } + + /** + * Set port name + * + * @param portName + * the port name used to open serial port + * @throws DataDictionaryException + * if portName is empty + */ + public void setPortName(String portName) throws DataDictionaryException + { + this.setPortName((Object) portName); + } + + /** + * Set description + * + * @param description + * the port description + * @throws DataDictionaryException + * if description is empty + */ + public void setDescription(String description) throws DataDictionaryException + { + this.setDescription((Object) description); + } + + /** + * Set product id + * + * @param productId + * the USB device product identifier + * @throws DataDictionaryException + * if productId is out of range [0-65535] + */ + public void setProductId(Number productId) throws DataDictionaryException + { + this.setProductId((Object) productId); + } + + /** + * Set vendor id + * + * @param vendorId + * the USB device vendor identifier + * @throws DataDictionaryException + * if vendorId is out of range [0-65535] + */ + public void setVendorId(Number vendorId) throws DataDictionaryException + { + this.setVendorId((Object) vendorId); + } + + /** + * Set product name + * + * @param productName + * the USB device product name + * @throws DataDictionaryException + * if productName is empty + */ + public void setProductName(String productName) throws DataDictionaryException + { + this.setProductName((Object) productName); + } + + /** + * Set manufacturer + * + * @param manufacturer + * the USB device manufacturer + * @throws DataDictionaryException + * if manufacturer is empty + */ + public void setManufacturer(String manufacturer) throws DataDictionaryException + { + this.setManufacturer((Object) manufacturer); + } + + /** + * Set serial number + * + * @param serialNumber + * the USB device serial number + * @throws DataDictionaryException + * if serialNumber is empty + */ + public void setSerialNumber(String serialNumber) throws DataDictionaryException + { + this.setSerialNumber((Object) serialNumber); + } + + /** + * Check if serial port is native (not USB) + * + * @return true if native port, false otherwise + */ + public boolean isNative() + { + return this.getPortName() != null && !this.getPortName().isEmpty() && this.getPortId() == null; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setPortId(Object portId) throws DataDictionaryException + { + this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); + } + + protected void setPortName(Object portName) throws DataDictionaryException + { + this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); + } + + protected void setDescription(Object description) throws DataDictionaryException + { + this.data.put(KEY_DESCRIPTION, this.kDescription.convert(description)); + } + + protected void setProductId(Object productId) throws DataDictionaryException + { + this.data.put(KEY_PRODUCT_ID, this.kProductId.convert(productId)); + } + + protected void setVendorId(Object vendorId) throws DataDictionaryException + { + this.data.put(KEY_VENDOR_ID, this.kVendorId.convert(vendorId)); + } + + protected void setProductName(Object productName) throws DataDictionaryException + { + this.data.put(KEY_PRODUCT_NAME, this.kProductName.convert(productName)); + } + + protected void setManufacturer(Object manufacturer) throws DataDictionaryException + { + this.data.put(KEY_MANUFACTURER, this.kManufacturer.convert(manufacturer)); + } + + protected void setSerialNumber(Object serialNumber) throws DataDictionaryException + { + this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); + this.keys.add(this.kPortId); + + this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); + this.keys.add(this.kPortName); + + this.kDescription = new KeyDescriptorString(KEY_DESCRIPTION, false, false); + this.keys.add(this.kDescription); + + this.kProductId = new KeyDescriptorNumberMinMax(KEY_PRODUCT_ID, false, PRODUCT_ID_MIN, PRODUCT_ID_MAX); + this.keys.add(this.kProductId); + + this.kVendorId = new KeyDescriptorNumberMinMax(KEY_VENDOR_ID, false, VENDOR_ID_MIN, VENDOR_ID_MAX); + this.keys.add(this.kVendorId); + + this.kProductName = new KeyDescriptorString(KEY_PRODUCT_NAME, false, false); + this.keys.add(this.kProductName); + + this.kManufacturer = new KeyDescriptorString(KEY_MANUFACTURER, false, false); + this.keys.add(this.kManufacturer); + + this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, false); + this.keys.add(this.kSerialNumber); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java new file mode 100644 index 0000000..4090a4b --- /dev/null +++ b/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java @@ -0,0 +1,120 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.serialport; + +import enedis.lab.io.PortFinder; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataList; + +/** + * Interface used to find all serial port descriptor + */ +public interface SerialPortFinder extends PortFinder +{ + /** + * Find serial port descriptor matching with port id + * + * @param portId + * the unique port identifier desired + * + * @return Serial port descriptor found, or null if nothing matches with portId + */ + public default SerialPortDescriptor findByPortId(String portId) + { + return this.findAll().stream().filter(k -> (k.getPortId() != null) ? k.getPortId().equals(portId) : portId == null).findFirst().orElse(null); + } + + /** + * Find serial port descriptor matching with port name + * + * @param portName + * the port name desired + * + * @return Serial port descriptor found, or null if nothing matches with portName + */ + public default SerialPortDescriptor findByPortName(String portName) + { + return this.findAll().stream().filter(k -> (k.getPortName() != null) ? k.getPortName().equals(portName) : portName == null).findFirst().orElse(null); + } + + /** + * Find native serial port (not USB) descriptor matching with port name + * + * @param portName + * the port name desired + * + * @return Serial port descriptor found, or null if nothing matches with portName + */ + public default SerialPortDescriptor findNative(String portName) + { + return this.findAll().stream().filter(k -> k.isNative() && k.getPortName().equals(portName)).findFirst().orElse(null); + } + + /** + * Find serial port descriptor matching with port id (having priority) or port name + * + * @param portId + * the unique port identifier desired + * @param portName + * the port name desired + * + * @return Serial port descriptor found, or null if nothing matches with portName + */ + public default SerialPortDescriptor findByPortIdOrPortName(String portId, String portName) + { + SerialPortDescriptor descriptor; + + if (portId != null) + { + descriptor = this.findByPortId(portId); + } + else if (portName != null) + { + descriptor = this.findByPortName(portName); + } + else + { + descriptor = null; + } + + return descriptor; + } + + /** + * Find serial port descriptor matching with pid/vid + * + * @param productId + * the USB device product identifier + * @param vendorId + * the USB device vendor identifier + * + * @return Serial port descriptor list found + */ + public default DataList findByProductIdAndVendorId(Number productId, Number vendorId) + { + DataList descriptorList = new DataArrayList(); + + for (SerialPortDescriptor descriptor : this.findAll()) + { + if (descriptor.getProductId() == null && productId != null) + { + continue; + } + if (descriptor.getVendorId() == null && vendorId != null) + { + continue; + } + if (descriptor.getProductId().equals(productId) && descriptor.getVendorId().equals(vendorId)) + { + descriptorList.add(descriptor); + } + } + + return descriptorList; + } +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java new file mode 100644 index 0000000..9b9ba0f --- /dev/null +++ b/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java @@ -0,0 +1,125 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.serialport; + +import org.apache.commons.lang3.SystemUtils; + +import enedis.lab.types.DataList; + +/** + * Class used to find all serial port descriptor for any operating system + */ +public class SerialPortFinderBase implements SerialPortFinder +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Program writing the serial port descriptor list (JSON format) on the output stream + * + * @param args + * not used + */ + public static void main(String[] args) + { + DataList descriptors = getInstance().findAll(); + + System.out.println(descriptors.toString(2)); + } + + /** + * Get instance + * + * @return Unique instance + */ + public static SerialPortFinder getInstance() + { + if (instance == null) + { + if (SystemUtils.IS_OS_WINDOWS) + { + instance = SerialPortFinderForWindows.getInstance(); + } + else if (SystemUtils.IS_OS_LINUX) + { + instance = SerialPortFinderForLinux.getInstance(); + } + else + { + throw new RuntimeException("Operating system " + SystemUtils.OS_NAME + " is not handled by " + SerialPortFinderBase.class.getName()); + } + } + + return instance; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static SerialPortFinder instance; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private SerialPortFinderBase() + { + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// SerialPortFinder + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public DataList findAll() + { + return instance.findAll(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java new file mode 100644 index 0000000..f8c774a --- /dev/null +++ b/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java @@ -0,0 +1,703 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.serialport; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.sun.jna.platform.linux.Udev; +import com.sun.jna.platform.linux.Udev.UdevContext; +import com.sun.jna.platform.linux.Udev.UdevDevice; +import com.sun.jna.platform.linux.Udev.UdevEnumerate; +import com.sun.jna.platform.linux.Udev.UdevListEntry; + +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; + +/** + * Class used to find all serial port descriptor for Linux + */ +public class SerialPortFinderForLinux implements SerialPortFinder +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static String MOXA_IDENTIFIER = "ttyr"; + private static Pattern MOXA_PATTERN = Pattern.compile("(.*)" + MOXA_IDENTIFIER + "(\\d+)"); + private static String MOXA_FORMAT = MOXA_IDENTIFIER + "%02d"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get instance + * + * @return Unique instance + */ + public static SerialPortFinderForLinux getInstance() + { + if (instance == null) + { + instance = new SerialPortFinderForLinux(); + } + + return instance; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static SerialPortFinderForLinux instance; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private SerialPortFinderForLinux() + { + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// SerialPortFinder + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public DataList findAll() + { + DataList serialPortDescriptorList = new DataArrayList(); + + serialPortDescriptorList = availablePortsByUdev(); + if (serialPortDescriptorList.isEmpty()) + { + serialPortDescriptorList = availablePortsBySysfs(); + } + if (serialPortDescriptorList.isEmpty()) + { + serialPortDescriptorList = availablePortsByFiltersOfDevices(); + } + + return serialPortDescriptorList; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static DataList availablePortsByUdev() + { + UdevContext udev = Udev.INSTANCE.udev_new(); + UdevEnumerate enumerate = Udev.INSTANCE.udev_enumerate_new(udev); + Udev.INSTANCE.udev_enumerate_add_match_subsystem(enumerate, "tty"); + Udev.INSTANCE.udev_enumerate_scan_devices(enumerate); + + DataList serialPortDescriptorList = new DataArrayList(); + + for (UdevListEntry dev_list_entry = Udev.INSTANCE.udev_enumerate_get_list_entry(enumerate); dev_list_entry != null; dev_list_entry = dev_list_entry.getNext()) + { + UdevDevice device = Udev.INSTANCE.udev_device_new_from_syspath(udev, Udev.INSTANCE.udev_list_entry_get_name(dev_list_entry)); + if (device == null) + { + break; + } + SerialPortDescriptor serialPortDescriptor; + + String location = deviceLocation(device); + String portName = deviceName(device); + + UdevDevice parentdev = Udev.INSTANCE.udev_device_get_parent(device); + + if (parentdev != null) + { + String driverName = deviceDriver(parentdev); + if (isSerial8250Driver(driverName) && !isValidSerial8250(location)) + { + Udev.INSTANCE.udev_device_unref(device); + continue; + } + String description = deviceDescription(device); + String portId = devicePortId(device); + String manufacturer = deviceManufacturer(device); + String serialNumber = deviceSerialNumber(device); + Number vendorIdentifier = deviceVendorIdentifier(device); + Number productIdentifier = deviceProductIdentifier(device); + String productName = deviceProductName(device); + try + { + serialPortDescriptor = new SerialPortDescriptor(portId, location, description, productIdentifier, vendorIdentifier, productName, manufacturer, serialNumber); + } + catch (DataDictionaryException e) + { + e.printStackTrace(); + Udev.INSTANCE.udev_device_unref(device); + continue; + } + } + else + { + if (!isRfcommDevice(portName) && !isVirtualNullModemDevice(portName) && !isGadgetDevice(portName) && !isMoxaDevice(portName)) + { + Udev.INSTANCE.udev_device_unref(device); + continue; + } + try + { + serialPortDescriptor = new SerialPortDescriptor(null, location, null, null, null, null, null, null); + } + catch (DataDictionaryException e) + { + e.printStackTrace(); + Udev.INSTANCE.udev_device_unref(device); + continue; + } + } + serialPortDescriptorList.add(serialPortDescriptor); + Udev.INSTANCE.udev_device_unref(device); + } + Udev.INSTANCE.udev_enumerate_unref(enumerate); + Udev.INSTANCE.udev_unref(udev); + + return serialPortDescriptorList; + } + + private static DataList availablePortsBySysfs() + { + DataList serialPortDescriptorList = new DataArrayList(); + File ttySysClassDir = new File("/sys/class/tty"); + + if (!(ttySysClassDir.exists() && ttySysClassDir.canRead())) + { + return serialPortDescriptorList; + } + FileFilter filter = new FileFilter() + { + @Override + public boolean accept(File pathname) + { + + return pathname.isDirectory() && !pathname.getName().equals(".") && !pathname.getName().equals(".."); + } + }; + File[] fileInfos = ttySysClassDir.listFiles(filter); + for (File fileInfo : fileInfos) + { + if (!Files.isSymbolicLink(fileInfo.toPath())) + { + continue; + } + File targetDir = fileInfo.getAbsoluteFile(); + SerialPortDescriptor serialPortDescriptor; + + String portName = deviceName(targetDir); + if (portName == null) + { + continue; + } + String driverName = deviceDriver(targetDir); + if (driverName == null) + { + if (!isRfcommDevice(portName) && !isVirtualNullModemDevice(portName) && !isGadgetDevice(portName) && !isMoxaDevice(portName)) + { + continue; + } + } + + String device = portNameToSystemLocation(portName); + if (isSerial8250Driver(driverName) && !isValidSerial8250(device)) + { + continue; + } + String description = null; + String manufacturer = null; + String serialNumber = null; + Number vendorIdentifier = null; + Number productIdentifier = null; + String productName = null; + do + { + if (description == null) + { + description = deviceDescription(targetDir); + } + if (manufacturer == null) + { + manufacturer = deviceManufacturer(targetDir); + } + if (serialNumber == null) + { + serialNumber = deviceSerialNumber(targetDir); + } + if (vendorIdentifier == null) + { + vendorIdentifier = deviceVendorIdentifier(targetDir); + } + if (productIdentifier == null) + { + productIdentifier = deviceProductIdentifier(targetDir); + } + if (productName == null) + { + productName = deviceProductName(targetDir); + } + if (description != null || manufacturer != null || serialNumber != null || vendorIdentifier != null || productIdentifier != null || productName != null) + { + break; + } + targetDir = targetDir.getParentFile(); + } while (targetDir != null); + try + { + serialPortDescriptor = new SerialPortDescriptor(null, portName, description, productIdentifier, vendorIdentifier, productName, manufacturer, serialNumber); + } + catch (DataDictionaryException e) + { + e.printStackTrace(); + continue; + } + serialPortDescriptorList.add(serialPortDescriptor); + } + + return serialPortDescriptorList; + } + + private static DataList availablePortsByFiltersOfDevices() + { + DataList serialPortDescriptorList = new DataArrayList(); + + List deviceFilePaths = filteredDeviceFilePaths(); + for (String deviceFilePath : deviceFilePaths) + { + SerialPortDescriptor serialPortDescriptor; + String portName = portNameFromSystemLocation(deviceFilePath); + try + { + serialPortDescriptor = new SerialPortDescriptor(null, portName, null, null, null, null, null, null); + } + catch (DataDictionaryException e) + { + e.printStackTrace(); + continue; + } + serialPortDescriptorList.add(serialPortDescriptor); + } + + return serialPortDescriptorList; + } + + private static List filteredDeviceFilePaths() + { + List deviceFileNameFilterList = new ArrayList(); + + // Linux + deviceFileNameFilterList.add("ttyS*"); // Standard UART 8250 and etc. + deviceFileNameFilterList.add("ttyO*"); // OMAP UART 8250 and etc. + deviceFileNameFilterList.add("ttyUSB*"); // Usb/serial converters PL2303 and etc. + deviceFileNameFilterList.add("ttyACM*"); // CDC_ACM converters (i.e. Mobile Phones). + deviceFileNameFilterList.add("ttyGS*"); // Gadget serial device (i.e. Mobile Phones with gadget serial driver). + deviceFileNameFilterList.add("ttyMI*"); // MOXA pci/serial converters. + deviceFileNameFilterList.add("ttymxc*"); // Motorola IMX serial ports (i.e. Freescale i.MX). + deviceFileNameFilterList.add("ttyAMA*"); // AMBA serial device for embedded platform on ARM (i.e. Raspberry Pi). + deviceFileNameFilterList.add("ttyTHS*"); // Serial device for embedded platform on ARM (i.e. Tegra Jetson TK1). + deviceFileNameFilterList.add("rfcomm*"); // Bluetooth serial device. + deviceFileNameFilterList.add("ircomm*"); // IrDA serial device. + deviceFileNameFilterList.add("tnt*"); // Virtual tty0tty serial device. + deviceFileNameFilterList.add(MOXA_IDENTIFIER + "*"); // MOXA ethernet device. + // FreeBSD + deviceFileNameFilterList.add("cu*"); + // QNX + deviceFileNameFilterList.add("ser*"); + + List result = new ArrayList(); + + File deviceDir = new File("/dev"); + if (deviceDir.exists()) + { + FileFilter deviceFileNameFilter = new FileFilter() + { + @Override + public boolean accept(File pathname) + { + Path path = pathname.toPath(); + + return deviceFileNameFilterList.contains(pathname.getName()) && pathname.isFile() && !Files.isSymbolicLink(path); + } + }; + File[] deviceFileInfos = deviceDir.listFiles(deviceFileNameFilter); + for (File deviceFileInfo : deviceFileInfos) + { + String deviceAbsoluteFilePath = deviceFileInfo.getAbsolutePath(); + // it is a quick workaround to skip the non-serial devices (FreeBSD) + if (deviceAbsoluteFilePath.endsWith(".init") || deviceAbsoluteFilePath.endsWith(".lock")) + { + continue; + } + if (!result.contains(deviceAbsoluteFilePath)) + { + result.add(deviceAbsoluteFilePath); + } + } + } + + return result; + } + + private static boolean isSerial8250Driver(String driverName) + { + return (driverName == "serial8250"); + } + + private static boolean isValidSerial8250(String systemLocation) + { + return false; + } + + private static boolean isRfcommDevice(String portName) + { + if (!portName.startsWith("rfcomm")) + { + return false; + } + try + { + String number = portName.substring(6); + int portNumber = Integer.parseInt(number); + if (portNumber < 0 || portNumber > 255) + { + return false; + } + } + catch (Exception e) + { + return false; + } + return true; + } + + // provided by the tty0tty driver + private static boolean isVirtualNullModemDevice(String portName) + { + return portName.startsWith("tnt"); + } + + // provided by the g_serial driver + private static boolean isGadgetDevice(String portName) + { + return portName.startsWith("ttyGS"); + } + + // provided by the MOXA driver + private static boolean isMoxaDevice(String portName) + { + return portName.startsWith(MOXA_IDENTIFIER); + } + + private static String devicePortId(UdevDevice dev) + { + return deviceProperty(dev, "ID_PATH"); + } + + private static String deviceDescription(UdevDevice dev) + { + String description = deviceProperty(dev, "ID_MODEL_FROM_DATABASE"); + + return (description != null) ? description.replace('_', ' ') : null; + } + + private static String deviceDescription(File targetDir) + { + return deviceProperty(new File(targetDir, "product").getAbsolutePath()); + } + + private static String deviceManufacturer(UdevDevice dev) + { + String manufacturer = deviceProperty(dev, "ID_VENDOR"); + + return (manufacturer != null) ? manufacturer.replace('_', ' ') : null; + } + + private static String deviceManufacturer(File targetDir) + { + return deviceProperty(new File(targetDir, "manufacturer").getAbsolutePath()); + } + + private static Number deviceProductIdentifier(UdevDevice dev) + { + Number identifierValue; + try + { + String indentifierText = deviceProperty(dev, "ID_MODEL_ID"); + identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + } + catch (Exception e) + { + identifierValue = null; + } + return identifierValue; + } + + private static String deviceProductName(UdevDevice dev) + { + return deviceProperty(dev, "ID_MODEL"); + } + + private static String deviceProductName(File targetDir) + { + return deviceProperty(new File(targetDir, "product").getAbsolutePath()); + } + + private static Number deviceProductIdentifier(File targetDir) + { + String indentifierText = deviceProperty(new File(targetDir, "idProduct").getAbsolutePath()); + + if (indentifierText == null) + { + indentifierText = deviceProperty(new File(targetDir, "device").getAbsolutePath()); + } + Number identifierValue; + try + { + identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + } + catch (Exception e) + { + identifierValue = null; + } + return identifierValue; + } + + private static Number deviceVendorIdentifier(File targetDir) + { + String indentifierText = deviceProperty(new File(targetDir, "idVendor").getAbsolutePath()); + + if (indentifierText == null) + { + indentifierText = deviceProperty(new File(targetDir, "vendor").getAbsolutePath()); + } + Number identifierValue; + try + { + identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + } + catch (Exception e) + { + identifierValue = null; + } + return identifierValue; + } + + private static String deviceSerialNumber(File targetDir) + { + return deviceProperty(new File(targetDir, "serial").getAbsolutePath()); + } + + private static Number deviceVendorIdentifier(UdevDevice dev) + { + Number identifierValue; + try + { + String indentifierText = deviceProperty(dev, "ID_VENDOR_ID"); + identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + } + catch (Exception e) + { + identifierValue = null; + } + return identifierValue; + } + + private static String deviceSerialNumber(UdevDevice dev) + { + return deviceProperty(dev, "ID_SERIAL_SHORT"); + } + + private static String deviceProperty(UdevDevice dev, String name) + { + return Udev.INSTANCE.udev_device_get_property_value(dev, name); + } + + private static String deviceProperty(String targetFilePath) + { + FileInputStream f; + + try + { + f = new FileInputStream(targetFilePath); + } + catch (FileNotFoundException e) + { + e.printStackTrace(); + return null; + } + String text; + try + { + int available = f.available(); + byte[] buffer = new byte[available]; + f.read(buffer); + text = new String(buffer); + } + catch (IOException e) + { + e.printStackTrace(); + return null; + } + finally + { + try + { + f.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + return text.trim(); + } + + private static String deviceDriver(UdevDevice dev) + { + return null; + } + + private static String deviceDriver(File targetDir) + { + File deviceDir = new File(targetDir, "device"); + + return ueventProperty(deviceDir, "DRIVER="); + } + + private static String deviceName(UdevDevice dev) + { + return Udev.INSTANCE.udev_device_get_sysname(dev); + } + + static String deviceName(File targetDir) + { + return ueventProperty(targetDir, "DEVNAME="); + } + + private static String deviceLocation(UdevDevice dev) + { + String location = Udev.INSTANCE.udev_device_get_devnode(dev); + // Patch for MOXA device + // Udev prints location as /dev/ttyrN but only /dev/ttyr0N exists (with N belongs to [1-4]) + // So we are renaming location from /dev/ttyrN to /dev/ttyr0N + Matcher matcher = MOXA_PATTERN.matcher(location); + + if (matcher.matches()) + { + String prefix = ""; + String indexValue; + if (matcher.groupCount() == 2) + { + prefix = matcher.group(1); + indexValue = matcher.group(2); + } + else + { + indexValue = matcher.group(2); + } + int index = Integer.parseUnsignedInt(indexValue); + location = String.format("%s" + MOXA_FORMAT, prefix, index); + } + + return location; + } + + private static String portNameToSystemLocation(String source) + { + return (source.startsWith("/") || source.startsWith("./") || source.startsWith("../")) ? source : (("/dev/") + source); + } + + private static String portNameFromSystemLocation(String source) + { + return source.startsWith("/dev/") ? source.substring(5) : source; + } + + @SuppressWarnings("resource") + private static String ueventProperty(File targetDir, String pattern) + { + FileInputStream f; + + try + { + f = new FileInputStream(new File(targetDir, "uevent")); + } + catch (FileNotFoundException e) + { + e.printStackTrace(); + return null; + } + String content; + try + { + int available = f.available(); + byte[] buffer = new byte[available]; + f.read(buffer); + content = new String(buffer); + } + catch (IOException e) + { + e.printStackTrace(); + return null; + } + int firstbound = content.indexOf(pattern); + if (firstbound == -1) + { + return null; + } + int lastbound = content.indexOf('\n', firstbound); + + return content.substring(firstbound, lastbound); + } +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java new file mode 100644 index 0000000..6f66a58 --- /dev/null +++ b/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java @@ -0,0 +1,455 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.serialport; + +import static com.sun.jna.platform.win32.SetupApi.DICS_FLAG_GLOBAL; +import static com.sun.jna.platform.win32.SetupApi.DIGCF_DEVICEINTERFACE; +import static com.sun.jna.platform.win32.SetupApi.DIGCF_PRESENT; +import static com.sun.jna.platform.win32.SetupApi.DIREG_DEV; +import static com.sun.jna.platform.win32.SetupApi.GUID_DEVINTERFACE_COMPORT; +import static com.sun.jna.platform.win32.SetupApi.SPDRP_DEVICEDESC; +import static com.sun.jna.platform.win32.WinBase.INVALID_HANDLE_VALUE; +import static com.sun.jna.platform.win32.WinDef.MAX_PATH; +import static com.sun.jna.platform.win32.WinError.ERROR_SUCCESS; +import static com.sun.jna.platform.win32.WinNT.KEY_QUERY_VALUE; +import static com.sun.jna.platform.win32.WinNT.KEY_READ; +import static com.sun.jna.platform.win32.WinNT.REG_DWORD; +import static com.sun.jna.platform.win32.WinNT.REG_SZ; +import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE; + +import java.util.ArrayList; +import java.util.List; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; +import com.sun.jna.platform.win32.Advapi32; +import com.sun.jna.platform.win32.Advapi32Util; +import com.sun.jna.platform.win32.Cfgmgr32; +import com.sun.jna.platform.win32.Cfgmgr32Util; +import com.sun.jna.platform.win32.Guid.GUID; +import com.sun.jna.platform.win32.SetupApi; +import com.sun.jna.platform.win32.SetupApi.SP_DEVINFO_DATA; +import com.sun.jna.platform.win32.Win32Exception; +import com.sun.jna.platform.win32.WinNT.HANDLE; +import com.sun.jna.platform.win32.WinReg.HKEY; +import com.sun.jna.ptr.IntByReference; + +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; + +/** + * Class used to find all serial port descriptor for Windows + */ +public class SerialPortFinderForWindows implements SerialPortFinder +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final int SPDRP_MFG = 0x0000000B; + private static final int SPDRP_ADDRESS = 0x0000001C; + private static final int PROPERTY_MAX_SIZE = 1024; + + private static final GUID GUID_DEVCLASS_PORTS = new GUID("{4d36e978-e325-11ce-bfc1-08002be10318}"); + private static final GUID GUID_DEVCLASS_MODEM = new GUID("{4d36e96d-e325-11ce-bfc1-08002be10318}"); + private static final GUID GUID_DEVINTERFACE_MODEM = new GUID("{2c7089aa-2e0e-11d1-b114-00c04fc2aae4}"); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get instance + * + * @return Unique instance + */ + public static SerialPortFinderForWindows getInstance() + { + if(instance == null) + { + instance = new SerialPortFinderForWindows(); + } + + return instance; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static SerialPortFinderForWindows instance; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private SerialPortFinderForWindows() + { + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// SerialPortFinder + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public DataList findAll() + { + final class SetupToken + { + public GUID guid; + public int flags; + + public SetupToken(GUID guid, int flags) + { + super(); + this.guid = guid; + this.flags = flags; + } + } + + SetupToken[] setupTokens = new SetupToken[] { new SetupToken(GUID_DEVCLASS_PORTS, DIGCF_PRESENT), new SetupToken(GUID_DEVCLASS_MODEM, DIGCF_PRESENT), + new SetupToken(GUID_DEVINTERFACE_COMPORT, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE), new SetupToken(GUID_DEVINTERFACE_MODEM, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE) }; + + DataList serialPortDescriptorList = new DataArrayList(); + + for (int i = 0; i < setupTokens.length; ++i) + { + HANDLE deviceInfoSet = SetupApi.INSTANCE.SetupDiGetClassDevs(setupTokens[i].guid, null, null, setupTokens[i].flags); + if (deviceInfoSet == INVALID_HANDLE_VALUE) + { + return serialPortDescriptorList; + } + SP_DEVINFO_DATA deviceInfoData = new SP_DEVINFO_DATA(); + int index = 0; + while (SetupApi.INSTANCE.SetupDiEnumDeviceInfo(deviceInfoSet, index++, deviceInfoData)) + { + SerialPortDescriptor serialPortDescriptor; + + String portName = devicePortName(deviceInfoSet, deviceInfoData); + if (portName == null || portName.isEmpty() || portName.contains("LPT")) + { + continue; + } + if (anyOfPorts(serialPortDescriptorList, portName)) + { + continue; + } + String description = deviceDescription(deviceInfoSet, deviceInfoData); + String address = deviceAddress(deviceInfoSet, deviceInfoData); + String manufacturer = deviceManufacturer(deviceInfoSet, deviceInfoData); + String instanceIdentifier = deviceInstanceIdentifier(deviceInfoData.DevInst); + String serialNumber = deviceSerialNumber(instanceIdentifier, deviceInfoData.DevInst); + Number vendorIdentifier = deviceVendorIdentifier(instanceIdentifier); + Number productIdentifier = deviceProductIdentifier(instanceIdentifier); + try + { + serialPortDescriptor = new SerialPortDescriptor(address, portName, description, productIdentifier, vendorIdentifier, null, manufacturer, serialNumber); + } + catch (DataDictionaryException e) + { + e.printStackTrace(System.err); + continue; + } + serialPortDescriptorList.add(serialPortDescriptor); + + } + SetupApi.INSTANCE.SetupDiDestroyDeviceInfoList(deviceInfoSet); + } + + List portNames = portNamesFromHardwareDeviceMap(); + for (String portName : portNames) + { + if (!anyOfPorts(serialPortDescriptorList, portName)) + { + try + { + SerialPortDescriptor serialPortDescriptor = new SerialPortDescriptor(null, portName, null, null, null, null, null, null); + serialPortDescriptorList.add(serialPortDescriptor); + } + catch (DataDictionaryException e) + { + e.printStackTrace(System.err); + } + } + } + + return serialPortDescriptorList; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static boolean anyOfPorts(DataList descriptors, String portName) + { + if (descriptors == null) + { + return false; + } + for (SerialPortDescriptor descriptor : descriptors) + { + if (descriptor.getPortName() == null) + { + if (portName != null) + { + continue; + } + else + { + return true; + } + } + if (descriptor.getPortName().equals(portName)) + { + return true; + } + } + + return false; + } + + private static String devicePortName(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) + { + HKEY key = SetupApi.INSTANCE.SetupDiOpenDevRegKey(deviceInfoSet, deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); + + if (key == INVALID_HANDLE_VALUE) + { + return null; + } + String[] keyTokens = { "PortName", "PortNumber" }; + String portName = null; + for (int i = 0; i < keyTokens.length && portName == null; i++) + { + portName = Advapi32Util.registryGetStringValue(key, keyTokens[i]); + } + + return portName; + } + + private static String deviceDescription(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) + { + return (String) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_DEVICEDESC); + } + + private static String deviceManufacturer(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) + { + return (String) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_MFG); + } + + private static String deviceAddress(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) + { + Integer address = (Integer) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_ADDRESS); + return (address != null) ? address.toString() : null; + } + + private static Number deviceVendorIdentifier(String instanceIdentifier) + { + final int vendorIdentifierSize = 4; + + Number result = parseDeviceIdentifier(instanceIdentifier, "VID_", vendorIdentifierSize); + if (result == null) + { + result = parseDeviceIdentifier(instanceIdentifier, "VEN_", vendorIdentifierSize); + } + + return result; + } + + private static Number deviceProductIdentifier(String instanceIdentifier) + { + final int productIdentifierSize = 4; + + Number result = parseDeviceIdentifier(instanceIdentifier, "PID_", productIdentifierSize); + if (result == null) + { + result = parseDeviceIdentifier(instanceIdentifier, "DEV_", productIdentifierSize); + } + + return result; + } + + private static String deviceInstanceIdentifier(int deviceInstanceNumber) + { + return Cfgmgr32Util.CM_Get_Device_ID(deviceInstanceNumber); + } + + private static String deviceSerialNumber(String instanceIdentifier, int deviceInstanceNumber) + { + for (;;) + { + String serialNumber = parseDeviceSerialNumber(instanceIdentifier); + if (serialNumber != null) + { + return serialNumber; + } + deviceInstanceNumber = parentDeviceInstanceNumber(deviceInstanceNumber); + if (deviceInstanceNumber == 0) + { + break; + } + instanceIdentifier = deviceInstanceIdentifier(deviceInstanceNumber); + if (instanceIdentifier.isEmpty()) + { + break; + } + } + + return null; + } + + private static Object deviceRegistryProperty(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData, int property) + { + Object propertyValue = null; + IntByReference dataType = new IntByReference(0); + Pointer outputBuffer = new Memory(PROPERTY_MAX_SIZE); + IntByReference bytesRequired = new IntByReference(PROPERTY_MAX_SIZE); + + if (SetupApi.INSTANCE.SetupDiGetDeviceRegistryProperty(deviceInfoSet, deviceInfoData, property, dataType, outputBuffer, bytesRequired.getValue(), bytesRequired)) + { + switch (dataType.getValue()) + { + case REG_DWORD: + propertyValue = outputBuffer.getInt(0); + break; + case REG_SZ: + propertyValue = outputBuffer.getWideString(0); + break; + } + } + + return propertyValue; + } + + private static int parentDeviceInstanceNumber(int childDeviceInstanceNumber) + { + IntByReference parentInstanceNumber = new IntByReference(0); + if (Cfgmgr32.INSTANCE.CM_Get_Parent(parentInstanceNumber, childDeviceInstanceNumber, 0) != Cfgmgr32.CR_SUCCESS) + { + return 0; + } + return parentInstanceNumber.getValue(); + } + + private static String parseDeviceSerialNumber(String instanceIdentifier) + { + int firstbound = instanceIdentifier.lastIndexOf('\\'); + int lastbound = instanceIdentifier.indexOf('_', firstbound); + if (instanceIdentifier.startsWith("USB\\")) + { + if (lastbound != instanceIdentifier.length() - 3) + lastbound = instanceIdentifier.length(); + int ampersand = instanceIdentifier.indexOf('&', firstbound); + if (ampersand != -1 && ampersand < lastbound) + return null; + } + else if (instanceIdentifier.startsWith("FTDIBUS\\")) + { + firstbound = instanceIdentifier.lastIndexOf('+'); + lastbound = instanceIdentifier.indexOf('\\', firstbound); + if (lastbound == -1) + return null; + } + else + { + return null; + } + + return instanceIdentifier.substring(firstbound + 1, lastbound); + } + + private static Number parseDeviceIdentifier(String instanceIdentifier, String identifierPrefix, int identifierSize) + { + int index = instanceIdentifier.indexOf(identifierPrefix); + + if (index == -1) + { + return null; + } + String indentifierText = instanceIdentifier.substring(index + identifierPrefix.length(), index + identifierPrefix.length() + identifierSize); + Number identifierValue; + try + { + identifierValue = Integer.parseInt(indentifierText, 16); + } + catch (Exception e) + { + identifierValue = null; + } + + return identifierValue; + } + + private static List portNamesFromHardwareDeviceMap() + { + List result = new ArrayList(); + HKEY hKey = null; + try + { + hKey = Advapi32Util.registryGetKey(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM", KEY_QUERY_VALUE).getValue(); + } + catch(Win32Exception exception) + { + return result; + } + int index = 0; + for (;;) + { + // This is a maximum length of value name, see: + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724872%28v=vs.85%29.aspx + IntByReference requiredValueNameChars = new IntByReference(16383); + char[] outputValueName = new char[requiredValueNameChars.getValue()]; + Pointer outputBuffer = new Memory(MAX_PATH); + IntByReference bytesRequired = new IntByReference(MAX_PATH); + int ret = Advapi32.INSTANCE.RegEnumValue(hKey, index, outputValueName, requiredValueNameChars, null, null, outputBuffer, bytesRequired); + if (ret == ERROR_SUCCESS) + { + result.add(outputBuffer.getWideString(0)); + ++index; + } + else + { + break; + } + } + Advapi32.INSTANCE.RegCloseKey(hKey); + + return result; + } +} diff --git a/src/main/java/enedis/lab/io/tic/TICModemType.java b/src/main/java/enedis/lab/io/tic/TICModemType.java new file mode 100644 index 0000000..b1f3688 --- /dev/null +++ b/src/main/java/enedis/lab/io/tic/TICModemType.java @@ -0,0 +1,48 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +/** + * TIC Modem type + */ +public enum TICModemType +{ + /** Modem michaud */ + MICHAUD(0x6001, 0x0403), + /** Télé info */ + TELEINFO(0x6015, 0x0403); + + private int productId; + private int vendorId; + + TICModemType(int productId, int vendorId) + { + this.productId = productId; + this.vendorId = vendorId; + } + + /** + * Get product id + * + * @return product id + */ + public int getProductId() + { + return this.productId; + } + + /** + * Get vendor id + * + * @return vendor id + */ + public int getVendorId() + { + return this.vendorId; + } +} diff --git a/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java b/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java new file mode 100644 index 0000000..675a073 --- /dev/null +++ b/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java @@ -0,0 +1,291 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.io.serialport.SerialPortDescriptor; +import enedis.lab.io.usb.USBPortDescriptor; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; + +/** + * TICPortDescriptor class + * + * Generated + */ +public class TICPortDescriptor extends SerialPortDescriptor +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_MODEM_TYPE = "modemType"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kModemType; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected TICPortDescriptor() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICPortDescriptor(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICPortDescriptor(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param portId + * @param portName + * @param description + * @param productName + * @param manufacturer + * @param serialNumber + * @param modemType + * @throws DataDictionaryException + */ + public TICPortDescriptor(String portId, String portName, String description, String productName, String manufacturer, String serialNumber, TICModemType modemType) + throws DataDictionaryException + { + this(); + + this.setPortId(portId); + this.setPortName(portName); + this.setDescription(description); + this.setProductName(productName); + this.setManufacturer(manufacturer); + this.setSerialNumber(serialNumber); + this.setModemType(modemType); + + this.checkAndUpdate(); + } + + /** + * Constructor setting parameters to specific values + * + * @param serialPortDescriptor + * @param modemType + * @throws DataDictionaryException + */ + public TICPortDescriptor(SerialPortDescriptor serialPortDescriptor, TICModemType modemType) throws DataDictionaryException + { + this(); + + this.checkProductId(serialPortDescriptor.getProductId(), modemType); + this.checkVendorId(serialPortDescriptor.getVendorId(), modemType); + + this.setPortId(serialPortDescriptor.getPortId()); + this.setPortName(serialPortDescriptor.getPortName()); + this.setDescription(serialPortDescriptor.getDescription()); + this.setProductId(serialPortDescriptor.getProductId()); + this.setVendorId(serialPortDescriptor.getVendorId()); + this.setProductName(serialPortDescriptor.getProductName()); + this.setManufacturer(serialPortDescriptor.getManufacturer()); + this.setSerialNumber(serialPortDescriptor.getSerialNumber()); + this.setModemType(modemType); + + this.checkAndUpdate(); + } + + /** + * Constructor setting parameters to specific values + * + * @param usbPortDescriptor + * @param modemType + * @throws DataDictionaryException + */ + public TICPortDescriptor(USBPortDescriptor usbPortDescriptor, TICModemType modemType) throws DataDictionaryException + { + this(); + + this.checkProductId(usbPortDescriptor.getIdProduct(), modemType); + this.checkVendorId(usbPortDescriptor.getIdVendor(), modemType); + + this.setProductId(usbPortDescriptor.getIdProduct()); + this.setVendorId(usbPortDescriptor.getIdVendor()); + this.setProductName(usbPortDescriptor.getProduct()); + this.setManufacturer(usbPortDescriptor.getManufacturer()); + this.setSerialNumber(usbPortDescriptor.getSerialNumber()); + this.setModemType(modemType); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// SerialPortDescriptor + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (this.exists(KEY_MODEM_TYPE)) + { + if (this.getModemType() != null) + { + this.setProductId(this.getModemType().getProductId()); + this.setVendorId(this.getModemType().getVendorId()); + } + else + { + this.setProductId(null); + this.setVendorId(null); + } + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get modem type + * + * @return the modem type + */ + public TICModemType getModemType() + { + return (TICModemType) this.data.get(KEY_MODEM_TYPE); + } + + /** + * Set modem type + * + * @param modemType + * @throws DataDictionaryException + */ + public void setModemType(TICModemType modemType) throws DataDictionaryException + { + this.setModemType((Object) modemType); + if (modemType != null) + { + this.setProductId(modemType.getProductId()); + this.setVendorId(modemType.getVendorId()); + } + else + { + this.setProductId(null); + this.setVendorId(null); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setModemType(Object modemType) throws DataDictionaryException + { + if (modemType == null) + { + this.data.put(KEY_MODEM_TYPE, null); + } + else + { + this.data.put(KEY_MODEM_TYPE, this.kModemType.convert(modemType)); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void checkProductId(Number productId, TICModemType modemType) throws DataDictionaryException + { + if (modemType != null && productId != null && productId.intValue() != modemType.getProductId()) + { + throw new DataDictionaryException("TIC modem productId is inconsistent with the given one"); + } + } + + private void checkVendorId(Number vendorId, TICModemType modemType) throws DataDictionaryException + { + if (modemType != null && vendorId != null && vendorId.intValue() != modemType.getVendorId()) + { + throw new DataDictionaryException("TIC modem vendorId is inconsistent with the given one"); + } + } + + private void loadKeyDescriptors() + { + try + { + this.kModemType = new KeyDescriptorEnum(KEY_MODEM_TYPE, false, TICModemType.class); + this.keys.add(this.kModemType); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/tic/TICPortFinder.java b/src/main/java/enedis/lab/io/tic/TICPortFinder.java new file mode 100644 index 0000000..370121b --- /dev/null +++ b/src/main/java/enedis/lab/io/tic/TICPortFinder.java @@ -0,0 +1,74 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +import enedis.lab.io.PortFinder; + +/** + * Interface used to find all TIC port descriptor + */ +public interface TICPortFinder extends PortFinder +{ + /** + * Find TIC port descriptor matching with port id + * + * @param portId + * the unique port identifier desired + * + * @return TIC port descriptor found, or null if nothing matches with portId + */ + public default TICPortDescriptor findByPortId(String portId) + { + return this.findAll().stream().filter(p -> (p.getPortId() != null) ? p.getPortId().equals(portId) : portId == null).findFirst().orElse(null); + } + + /** + * Find TIC port descriptor matching with port name + * + * @param portName + * the port name desired + * + * @return TIC port descriptor found, or null if nothing matches with portName + */ + public default TICPortDescriptor findByPortName(String portName) + { + return this.findAll().stream().filter(p -> (p.getPortName() != null) ? p.getPortName().equals(portName) : portName == null).findFirst().orElse(null); + } + + /** + * Find native TIC port (not USB) descriptor matching with port name + * + * @param portName + * the port name desired + * + * @return TIC port descriptor found, or null if nothing matches with portName + */ + public TICPortDescriptor findNative(String portName); + + /** + * Find TIC port descriptor matching with port id or port name + * + * @param portId + * the unique port identifier desired + * @param portName + * the port name desired + * + * @return TIC port descriptor found, or null if nothing matches with portName + */ + public default TICPortDescriptor findByPortIdOrPortName(String portId, String portName) + { + TICPortDescriptor descriptor = this.findByPortId(portId); + + if (descriptor == null) + { + descriptor = this.findByPortName(portId); + } + + return descriptor; + } +} diff --git a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java b/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java new file mode 100644 index 0000000..d4777c1 --- /dev/null +++ b/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java @@ -0,0 +1,206 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +import java.util.List; + +import enedis.lab.io.serialport.SerialPortDescriptor; +import enedis.lab.io.serialport.SerialPortFinder; +import enedis.lab.io.serialport.SerialPortFinderBase; +import enedis.lab.io.usb.USBPortDescriptor; +import enedis.lab.io.usb.USBPortFinder; +import enedis.lab.io.usb.USBPortFinderBase; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; + +/** + * Class used to find all TIC port descriptor + */ +public class TICPortFinderBase implements TICPortFinder +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Program writing the TIC port descriptor list (JSON format) on the output stream + * + * @param args + * not used + */ + public static void main(String[] args) + { + DataList descriptors = getInstance().findAll(); + + System.out.println(descriptors.toString(2)); + } + + /** + * Get instance + * + * @return Unique instance + */ + public static TICPortFinderBase getInstance() + { + if (instance == null) + { + instance = new TICPortFinderBase(); + } + + return instance; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static TICPortFinderBase instance; + + private SerialPortFinder serialPortFinder; + private USBPortFinder usbPortFinder; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private TICPortFinderBase() + { + this(SerialPortFinderBase.getInstance(), USBPortFinderBase.getInstance()); + } + + /** + * Constructor with finder parameters + * + * @param serialPortFinder + * the serial port finder interface + * @param usbPortFinder + * the USB port finder interface + */ + public TICPortFinderBase(SerialPortFinder serialPortFinder, USBPortFinder usbPortFinder) + { + if (serialPortFinder == null) + { + throw new IllegalArgumentException("Cannot set null serial port finder"); + } + this.serialPortFinder = serialPortFinder; + if (usbPortFinder == null) + { + throw new IllegalArgumentException("Cannot set null USB port finder"); + } + this.usbPortFinder = usbPortFinder; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICPortFinder + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public DataList findAll() + { + DataList ticSerialPort = new DataArrayList(); + + for (TICModemType modemType : TICModemType.values()) + { + List tmpSerialPort = this.serialPortFinder.findByProductIdAndVendorId(modemType.getProductId(), modemType.getVendorId()); + + if (tmpSerialPort.isEmpty()) + { + List tmpUSBPort = this.usbPortFinder.findByProductIdAndVendorId(modemType.getProductId(), modemType.getVendorId()); + + for (USBPortDescriptor upd : tmpUSBPort) + { + try + { + TICPortDescriptor tic = new TICPortDescriptor(upd, modemType); + ticSerialPort.add(tic); + } + catch (DataDictionaryException e) + { + } + } + } + else + { + for (SerialPortDescriptor spd : tmpSerialPort) + { + try + { + TICPortDescriptor tic = new TICPortDescriptor(spd, modemType); + ticSerialPort.add(tic); + } + catch (DataDictionaryException e) + { + } + } + } + } + + return ticSerialPort; + } + + @Override + public TICPortDescriptor findNative(String portName) + { + TICPortDescriptor ticPortDescriptor = null; + SerialPortDescriptor serialPortDescriptor = this.serialPortFinder.findNative(portName); + + if (serialPortDescriptor != null) + { + try + { + ticPortDescriptor = new TICPortDescriptor(serialPortDescriptor, null); + } + catch (DataDictionaryException e) + { + } + } + + return ticPortDescriptor; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java b/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java new file mode 100644 index 0000000..d16edc8 --- /dev/null +++ b/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java @@ -0,0 +1,131 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +import org.json.JSONObject; + +import enedis.lab.io.PlugSubscriber; +import enedis.lab.io.PortPlugNotifier; + +/** + * Class used to notify when a TIC port has been plugged or unplugged + */ +public class TICPortPlugNotifier extends PortPlugNotifier +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final int DEFAULT_JSON_INDENTATION = 2; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Program writing on the output stream when an TIC port has been plugged or unplugged + * + * @param args + * not used + */ + public static void main(String[] args) + { + /* 1. Create notification service */ + TICPortPlugNotifier notifier = new TICPortPlugNotifier(); + /* 2. Create subscriber to print when an TIC port has been plugged or unplugged */ + PlugSubscriber subscriber = new PlugSubscriber() + { + @Override + public void onPlugged(TICPortDescriptor descriptor) + { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(notifier.getClass().getSimpleName() + ".onPlugged", descriptor.toJSON()); + System.out.println(jsonObject.toString(DEFAULT_JSON_INDENTATION) + "\n"); + + } + @Override + public void onUnplugged(TICPortDescriptor descriptor) + { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(notifier.getClass().getSimpleName() + ".onUnplugged", descriptor.toJSON()); + System.out.println(jsonObject.toString(DEFAULT_JSON_INDENTATION) + "\n"); + } + }; + /* 3. Run program printing when an TIC port has been plugged or unplugged until CTRL+C is pressed */ + PortPlugNotifier.main(notifier, subscriber); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor with default parameter + * + * @see #DEFAULT_PERIOD + * @see TICPortFinderBase + */ + public TICPortPlugNotifier() + { + this(DEFAULT_PERIOD,TICPortFinderBase.getInstance()); + } + + /** + * Constructor with all parameters + * + * @param period the period (in milliseconds) used to look for plugged or unplugged TIC port + * @param finder the TIC port finder interface used to find all TIC port descriptors + */ + public TICPortPlugNotifier(long period,TICPortFinder finder) + { + super(period,finder); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Runnable + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java b/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java new file mode 100644 index 0000000..b3f71fb --- /dev/null +++ b/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java @@ -0,0 +1,711 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.usb; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorNumber; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * USBPortDescriptor class + * + * Generated + */ +public class USBPortDescriptor extends DataDictionaryBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_BCD_DEVICE = "bcdDevice"; + protected static final String KEY_BCD_USB = "bcdUSB"; + protected static final String KEY_B_DESCRIPTOR_TYPE = "bDescriptorType"; + protected static final String KEY_B_DEVICE_CLASS = "bDeviceClass"; + protected static final String KEY_B_DEVICE_PROTOCOL = "bDeviceProtocol"; + protected static final String KEY_B_DEVICE_SUB_CLASS = "bDeviceSubClass"; + protected static final String KEY_B_LENGTH = "bLength"; + protected static final String KEY_B_MAX_PACKET_SIZE0 = "bMaxPacketSize0"; + protected static final String KEY_B_NUM_CONFIGURATIONS = "bNumConfigurations"; + protected static final String KEY_ID_PRODUCT = "idProduct"; + protected static final String KEY_ID_VENDOR = "idVendor"; + protected static final String KEY_I_MANUFACTURER = "iManufacturer"; + protected static final String KEY_I_PRODUCT = "iProduct"; + protected static final String KEY_I_SERIAL_NUMBER = "iSerialNumber"; + protected static final String KEY_MANUFACTURER = "manufacturer"; + protected static final String KEY_PRODUCT = "product"; + protected static final String KEY_SERIAL_NUMBER = "serialNumber"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorNumber kBcdDevice; + protected KeyDescriptorNumber kBcdUSB; + protected KeyDescriptorNumber kBDescriptorType; + protected KeyDescriptorNumber kBDeviceClass; + protected KeyDescriptorNumber kBDeviceProtocol; + protected KeyDescriptorNumber kBDeviceSubClass; + protected KeyDescriptorNumber kBLength; + protected KeyDescriptorNumber kBMaxPacketSize0; + protected KeyDescriptorNumber kBNumConfigurations; + protected KeyDescriptorNumber kIdProduct; + protected KeyDescriptorNumber kIdVendor; + protected KeyDescriptorNumber kIManufacturer; + protected KeyDescriptorNumber kIProduct; + protected KeyDescriptorNumber kISerialNumber; + protected KeyDescriptorString kManufacturer; + protected KeyDescriptorString kProduct; + protected KeyDescriptorString kSerialNumber; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected USBPortDescriptor() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public USBPortDescriptor(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public USBPortDescriptor(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param bcdDevice + * @param bcdUSB + * @param bDescriptorType + * @param bDeviceClass + * @param bDeviceProtocol + * @param bDeviceSubClass + * @param bLength + * @param bMaxPacketSize0 + * @param bNumConfigurations + * @param idProduct + * @param idVendor + * @param iManufacturer + * @param iProduct + * @param iSerialNumber + * @param manufacturer + * @param product + * @param serialNumber + * @throws DataDictionaryException + */ + public USBPortDescriptor(Number bcdDevice, Number bcdUSB, Number bDescriptorType, Number bDeviceClass, Number bDeviceProtocol, Number bDeviceSubClass, Number bLength, + Number bMaxPacketSize0, Number bNumConfigurations, Number idProduct, Number idVendor, Number iManufacturer, Number iProduct, Number iSerialNumber, String manufacturer, + String product, String serialNumber) throws DataDictionaryException + { + this(); + + this.setBcdDevice(bcdDevice); + this.setBcdUSB(bcdUSB); + this.setBDescriptorType(bDescriptorType); + this.setBDeviceClass(bDeviceClass); + this.setBDeviceProtocol(bDeviceProtocol); + this.setBDeviceSubClass(bDeviceSubClass); + this.setBLength(bLength); + this.setBMaxPacketSize0(bMaxPacketSize0); + this.setBNumConfigurations(bNumConfigurations); + this.setIdProduct(idProduct); + this.setIdVendor(idVendor); + this.setIManufacturer(iManufacturer); + this.setIProduct(iProduct); + this.setISerialNumber(iSerialNumber); + this.setManufacturer(manufacturer); + this.setProduct(product); + this.setSerialNumber(serialNumber); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get bcd device + * + * @return the bcd device + */ + public Number getBcdDevice() + { + return (Number) this.data.get(KEY_BCD_DEVICE); + } + + /** + * Get bcd u s b + * + * @return the bcd u s b + */ + public Number getBcdUSB() + { + return (Number) this.data.get(KEY_BCD_USB); + } + + /** + * Get b descriptor type + * + * @return the b descriptor type + */ + public Number getBDescriptorType() + { + return (Number) this.data.get(KEY_B_DESCRIPTOR_TYPE); + } + + /** + * Get b device class + * + * @return the b device class + */ + public Number getBDeviceClass() + { + return (Number) this.data.get(KEY_B_DEVICE_CLASS); + } + + /** + * Get b device protocol + * + * @return the b device protocol + */ + public Number getBDeviceProtocol() + { + return (Number) this.data.get(KEY_B_DEVICE_PROTOCOL); + } + + /** + * Get b device sub class + * + * @return the b device sub class + */ + public Number getBDeviceSubClass() + { + return (Number) this.data.get(KEY_B_DEVICE_SUB_CLASS); + } + + /** + * Get b length + * + * @return the b length + */ + public Number getBLength() + { + return (Number) this.data.get(KEY_B_LENGTH); + } + + /** + * Get b max packet size0 + * + * @return the b max packet size0 + */ + public Number getBMaxPacketSize0() + { + return (Number) this.data.get(KEY_B_MAX_PACKET_SIZE0); + } + + /** + * Get b num configurations + * + * @return the b num configurations + */ + public Number getBNumConfigurations() + { + return (Number) this.data.get(KEY_B_NUM_CONFIGURATIONS); + } + + /** + * Get id product + * + * @return the id product + */ + public Number getIdProduct() + { + return (Number) this.data.get(KEY_ID_PRODUCT); + } + + /** + * Get id vendor + * + * @return the id vendor + */ + public Number getIdVendor() + { + return (Number) this.data.get(KEY_ID_VENDOR); + } + + /** + * Get i manufacturer + * + * @return the i manufacturer + */ + public Number getIManufacturer() + { + return (Number) this.data.get(KEY_I_MANUFACTURER); + } + + /** + * Get i product + * + * @return the i product + */ + public Number getIProduct() + { + return (Number) this.data.get(KEY_I_PRODUCT); + } + + /** + * Get i serial number + * + * @return the i serial number + */ + public Number getISerialNumber() + { + return (Number) this.data.get(KEY_I_SERIAL_NUMBER); + } + + /** + * Get manufacturer + * + * @return the manufacturer + */ + public String getManufacturer() + { + return (String) this.data.get(KEY_MANUFACTURER); + } + + /** + * Get product + * + * @return the product + */ + public String getProduct() + { + return (String) this.data.get(KEY_PRODUCT); + } + + /** + * Get serial number + * + * @return the serial number + */ + public String getSerialNumber() + { + return (String) this.data.get(KEY_SERIAL_NUMBER); + } + + /** + * Set bcd device + * + * @param bcdDevice + * @throws DataDictionaryException + */ + public void setBcdDevice(Number bcdDevice) throws DataDictionaryException + { + this.setBcdDevice((Object) bcdDevice); + } + + /** + * Set bcd u s b + * + * @param bcdUSB + * @throws DataDictionaryException + */ + public void setBcdUSB(Number bcdUSB) throws DataDictionaryException + { + this.setBcdUSB((Object) bcdUSB); + } + + /** + * Set b descriptor type + * + * @param bDescriptorType + * @throws DataDictionaryException + */ + public void setBDescriptorType(Number bDescriptorType) throws DataDictionaryException + { + this.setBDescriptorType((Object) bDescriptorType); + } + + /** + * Set b device class + * + * @param bDeviceClass + * @throws DataDictionaryException + */ + public void setBDeviceClass(Number bDeviceClass) throws DataDictionaryException + { + this.setBDeviceClass((Object) bDeviceClass); + } + + /** + * Set b device protocol + * + * @param bDeviceProtocol + * @throws DataDictionaryException + */ + public void setBDeviceProtocol(Number bDeviceProtocol) throws DataDictionaryException + { + this.setBDeviceProtocol((Object) bDeviceProtocol); + } + + /** + * Set b device sub class + * + * @param bDeviceSubClass + * @throws DataDictionaryException + */ + public void setBDeviceSubClass(Number bDeviceSubClass) throws DataDictionaryException + { + this.setBDeviceSubClass((Object) bDeviceSubClass); + } + + /** + * Set b length + * + * @param bLength + * @throws DataDictionaryException + */ + public void setBLength(Number bLength) throws DataDictionaryException + { + this.setBLength((Object) bLength); + } + + /** + * Set b max packet size0 + * + * @param bMaxPacketSize0 + * @throws DataDictionaryException + */ + public void setBMaxPacketSize0(Number bMaxPacketSize0) throws DataDictionaryException + { + this.setBMaxPacketSize0((Object) bMaxPacketSize0); + } + + /** + * Set b num configurations + * + * @param bNumConfigurations + * @throws DataDictionaryException + */ + public void setBNumConfigurations(Number bNumConfigurations) throws DataDictionaryException + { + this.setBNumConfigurations((Object) bNumConfigurations); + } + + /** + * Set id product + * + * @param idProduct + * @throws DataDictionaryException + */ + public void setIdProduct(Number idProduct) throws DataDictionaryException + { + this.setIdProduct((Object) idProduct); + } + + /** + * Set id vendor + * + * @param idVendor + * @throws DataDictionaryException + */ + public void setIdVendor(Number idVendor) throws DataDictionaryException + { + this.setIdVendor((Object) idVendor); + } + + /** + * Set i manufacturer + * + * @param iManufacturer + * @throws DataDictionaryException + */ + public void setIManufacturer(Number iManufacturer) throws DataDictionaryException + { + this.setIManufacturer((Object) iManufacturer); + } + + /** + * Set i product + * + * @param iProduct + * @throws DataDictionaryException + */ + public void setIProduct(Number iProduct) throws DataDictionaryException + { + this.setIProduct((Object) iProduct); + } + + /** + * Set i serial number + * + * @param iSerialNumber + * @throws DataDictionaryException + */ + public void setISerialNumber(Number iSerialNumber) throws DataDictionaryException + { + this.setISerialNumber((Object) iSerialNumber); + } + + /** + * Set manufacturer + * + * @param manufacturer + * @throws DataDictionaryException + */ + public void setManufacturer(String manufacturer) throws DataDictionaryException + { + this.setManufacturer((Object) manufacturer); + } + + /** + * Set product + * + * @param product + * @throws DataDictionaryException + */ + public void setProduct(String product) throws DataDictionaryException + { + this.setProduct((Object) product); + } + + /** + * Set serial number + * + * @param serialNumber + * @throws DataDictionaryException + */ + public void setSerialNumber(String serialNumber) throws DataDictionaryException + { + this.setSerialNumber((Object) serialNumber); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setBcdDevice(Object bcdDevice) throws DataDictionaryException + { + this.data.put(KEY_BCD_DEVICE, this.kBcdDevice.convert(bcdDevice)); + } + + protected void setBcdUSB(Object bcdUSB) throws DataDictionaryException + { + this.data.put(KEY_BCD_USB, this.kBcdUSB.convert(bcdUSB)); + } + + protected void setBDescriptorType(Object bDescriptorType) throws DataDictionaryException + { + this.data.put(KEY_B_DESCRIPTOR_TYPE, this.kBDescriptorType.convert(bDescriptorType)); + } + + protected void setBDeviceClass(Object bDeviceClass) throws DataDictionaryException + { + this.data.put(KEY_B_DEVICE_CLASS, this.kBDeviceClass.convert(bDeviceClass)); + } + + protected void setBDeviceProtocol(Object bDeviceProtocol) throws DataDictionaryException + { + this.data.put(KEY_B_DEVICE_PROTOCOL, this.kBDeviceProtocol.convert(bDeviceProtocol)); + } + + protected void setBDeviceSubClass(Object bDeviceSubClass) throws DataDictionaryException + { + this.data.put(KEY_B_DEVICE_SUB_CLASS, this.kBDeviceSubClass.convert(bDeviceSubClass)); + } + + protected void setBLength(Object bLength) throws DataDictionaryException + { + this.data.put(KEY_B_LENGTH, this.kBLength.convert(bLength)); + } + + protected void setBMaxPacketSize0(Object bMaxPacketSize0) throws DataDictionaryException + { + this.data.put(KEY_B_MAX_PACKET_SIZE0, this.kBMaxPacketSize0.convert(bMaxPacketSize0)); + } + + protected void setBNumConfigurations(Object bNumConfigurations) throws DataDictionaryException + { + this.data.put(KEY_B_NUM_CONFIGURATIONS, this.kBNumConfigurations.convert(bNumConfigurations)); + } + + protected void setIdProduct(Object idProduct) throws DataDictionaryException + { + this.data.put(KEY_ID_PRODUCT, this.kIdProduct.convert(idProduct)); + } + + protected void setIdVendor(Object idVendor) throws DataDictionaryException + { + this.data.put(KEY_ID_VENDOR, this.kIdVendor.convert(idVendor)); + } + + protected void setIManufacturer(Object iManufacturer) throws DataDictionaryException + { + this.data.put(KEY_I_MANUFACTURER, this.kIManufacturer.convert(iManufacturer)); + } + + protected void setIProduct(Object iProduct) throws DataDictionaryException + { + this.data.put(KEY_I_PRODUCT, this.kIProduct.convert(iProduct)); + } + + protected void setISerialNumber(Object iSerialNumber) throws DataDictionaryException + { + this.data.put(KEY_I_SERIAL_NUMBER, this.kISerialNumber.convert(iSerialNumber)); + } + + protected void setManufacturer(Object manufacturer) throws DataDictionaryException + { + this.data.put(KEY_MANUFACTURER, this.kManufacturer.convert(manufacturer)); + } + + protected void setProduct(Object product) throws DataDictionaryException + { + this.data.put(KEY_PRODUCT, this.kProduct.convert(product)); + } + + protected void setSerialNumber(Object serialNumber) throws DataDictionaryException + { + this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kBcdDevice = new KeyDescriptorNumber(KEY_BCD_DEVICE, true); + this.keys.add(this.kBcdDevice); + + this.kBcdUSB = new KeyDescriptorNumber(KEY_BCD_USB, true); + this.keys.add(this.kBcdUSB); + + this.kBDescriptorType = new KeyDescriptorNumber(KEY_B_DESCRIPTOR_TYPE, true); + this.keys.add(this.kBDescriptorType); + + this.kBDeviceClass = new KeyDescriptorNumber(KEY_B_DEVICE_CLASS, true); + this.keys.add(this.kBDeviceClass); + + this.kBDeviceProtocol = new KeyDescriptorNumber(KEY_B_DEVICE_PROTOCOL, true); + this.keys.add(this.kBDeviceProtocol); + + this.kBDeviceSubClass = new KeyDescriptorNumber(KEY_B_DEVICE_SUB_CLASS, true); + this.keys.add(this.kBDeviceSubClass); + + this.kBLength = new KeyDescriptorNumber(KEY_B_LENGTH, true); + this.keys.add(this.kBLength); + + this.kBMaxPacketSize0 = new KeyDescriptorNumber(KEY_B_MAX_PACKET_SIZE0, true); + this.keys.add(this.kBMaxPacketSize0); + + this.kBNumConfigurations = new KeyDescriptorNumber(KEY_B_NUM_CONFIGURATIONS, true); + this.keys.add(this.kBNumConfigurations); + + this.kIdProduct = new KeyDescriptorNumber(KEY_ID_PRODUCT, true); + this.keys.add(this.kIdProduct); + + this.kIdVendor = new KeyDescriptorNumber(KEY_ID_VENDOR, true); + this.keys.add(this.kIdVendor); + + this.kIManufacturer = new KeyDescriptorNumber(KEY_I_MANUFACTURER, true); + this.keys.add(this.kIManufacturer); + + this.kIProduct = new KeyDescriptorNumber(KEY_I_PRODUCT, true); + this.keys.add(this.kIProduct); + + this.kISerialNumber = new KeyDescriptorNumber(KEY_I_SERIAL_NUMBER, true); + this.keys.add(this.kISerialNumber); + + this.kManufacturer = new KeyDescriptorString(KEY_MANUFACTURER, false, true); + this.keys.add(this.kManufacturer); + + this.kProduct = new KeyDescriptorString(KEY_PRODUCT, false, true); + this.keys.add(this.kProduct); + + this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, true); + this.keys.add(this.kSerialNumber); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/usb/USBPortFinder.java b/src/main/java/enedis/lab/io/usb/USBPortFinder.java new file mode 100644 index 0000000..391e3e2 --- /dev/null +++ b/src/main/java/enedis/lab/io/usb/USBPortFinder.java @@ -0,0 +1,73 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.usb; + +import java.util.stream.Collectors; + +import enedis.lab.io.PortFinder; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataList; + +/** + * Interface used to find all USB port descriptor + */ +public interface USBPortFinder extends PortFinder +{ + /** + * Find USB device with the given product id + * + * @param idProduct the USB product identifier + * @return the USB descriptor data list of all USB device connected with the given product id + */ + public default DataList findByProductId(int idProduct) + { + DataList descriptors = new DataArrayList(); + // @formatter:off + descriptors.addAll(this.findAll().stream() + .filter(d -> d.getIdProduct().intValue() == idProduct) + .collect(Collectors.toList())); + // @formatter:on + return descriptors; + } + + /** + * Find USB device with the given vendor id + * + * @param idVendor the USB vendor identifier + * @return the USB descriptor data list of all USB device connected with the given vendor id + */ + public default DataList findByVendorId(int idVendor) + { + DataList descriptors = new DataArrayList(); + // @formatter:off + descriptors.addAll(this.findAll().stream() + .filter(d -> d.getIdVendor().intValue() == idVendor) + .collect(Collectors.toList())); + // @formatter:on + return descriptors; + } + + /** + * Find USB device with the given product id and vendor id + * + * @param idProduct the USB product identifier + * @param idVendor the USB vendor identifier + * @return the USB descriptor data list of all USB device connected with the given product id and vendor id + */ + public default DataList findByProductIdAndVendorId(int idProduct, int idVendor) + { + DataList descriptors = new DataArrayList(); + // @formatter:off + descriptors.addAll(this.findAll().stream() + .filter(d -> d.getIdProduct().intValue() == idProduct) + .filter(d -> d.getIdVendor().intValue() == idVendor) + .collect(Collectors.toList())); + // @formatter:on + return descriptors; + } +} diff --git a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java b/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java new file mode 100644 index 0000000..712626c --- /dev/null +++ b/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java @@ -0,0 +1,201 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.usb; + +import org.usb4java.Context; +import org.usb4java.Device; +import org.usb4java.DeviceDescriptor; +import org.usb4java.DeviceHandle; +import org.usb4java.DeviceList; +import org.usb4java.LibUsb; + +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; + +/** + * Class used to find all USB port descriptor + */ +public class USBPortFinderBase implements USBPortFinder +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Program writing the USB port descriptor list (JSON format) on the output stream + * + * @param args + * not used + */ + public static void main(String[] args) + { + DataList descriptors = getInstance().findAll(); + + System.out.println(descriptors.toString(2)); + } + + /** + * Get instance + * + * @return Unique instance + */ + public static USBPortFinderBase getInstance() + { + if (instance == null) + { + instance = new USBPortFinderBase(); + } + + return instance; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static USBPortFinderBase instance; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private USBPortFinderBase() + { + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// USBPortFinder + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public DataList findAll() + { + DataList usbPortList = new DataArrayList(); + + Context context = new Context(); + DeviceList deviceList = new DeviceList(); + + // Initialize USB library access + int result = LibUsb.init(context); + if (result != LibUsb.SUCCESS) + { + return usbPortList; + } + + // Get available USB device list + result = LibUsb.getDeviceList(context, deviceList); + if (result < 0) + { + return usbPortList; + } + + // For each USB device + for (Device device : deviceList) + { + DeviceDescriptor descriptor = new DeviceDescriptor(); + + // Get USB device descriptor + result = LibUsb.getDeviceDescriptor(device, descriptor); + if (result != LibUsb.SUCCESS) + { + continue; + } + String productName = null; + String manufacturer = null; + String serialNumber = null; + // If USB device has string descriptors + if (descriptor.idProduct() != 0) + { + // Get string descriptors for USB device + DeviceHandle handle = new DeviceHandle(); + result = LibUsb.open(device, handle); + if (result == LibUsb.SUCCESS) + { + productName = LibUsb.getStringDescriptor(handle, descriptor.iProduct()); + manufacturer = LibUsb.getStringDescriptor(handle, descriptor.iManufacturer()); + serialNumber = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber()); + LibUsb.close(handle); + } + } + + try + { + // @formatter:off + USBPortDescriptor usbPort = new USBPortDescriptor( + descriptor.bcdDevice() & 0xFFFF, + descriptor.bcdUSB() & 0xFFFF, + descriptor.bDescriptorType() & 0xFF, + descriptor.bDeviceClass() & 0xFF, + descriptor.bDeviceProtocol() & 0xFF, + descriptor.bDeviceSubClass() & 0xFF, + descriptor.bLength() & 0xFF, + descriptor.bMaxPacketSize0() & 0xFF, + descriptor.bNumConfigurations() & 0xFF, + descriptor.idProduct() & 0xFFFF, + descriptor.idVendor() & 0xFFFF, + descriptor.iManufacturer() & 0xFF, + descriptor.iProduct() & 0xFF, + descriptor.iSerialNumber() & 0xFF, + manufacturer, + productName, + serialNumber); + // @formatter:on + usbPortList.add(usbPort); + } + catch (DataDictionaryException e) + { + } + } + + // Finalize USB library access + LibUsb.freeDeviceList(deviceList, true); + LibUsb.exit(context); + return usbPortList; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/protocol/tic/TICMode.java b/src/main/java/enedis/lab/protocol/tic/TICMode.java new file mode 100644 index 0000000..5bbc299 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/TICMode.java @@ -0,0 +1,115 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic; + +import java.util.Arrays; + +/** + * TIC mode used available + */ + +import enedis.lab.protocol.tic.frame.TICError; +import enedis.lab.protocol.tic.frame.standard.TICException; + +/** + * TIC Mode + * + */ +public enum TICMode +{ + /** Unknown mode */ + UNKNOWN, + /** Standard mode */ + STANDARD, + /** Historic mode */ + HISTORIC, + /** Auto mode */ + AUTO; + + /** Historic separator */ + public static final char HISTORIC_SEPARATOR = ' '; + /** Standard separator */ + public static final char STANDARD_SEPARATOR = '\t'; + /** Historic buffer start */ + public static final byte[] HISTORIC_BUFFER_START = { 2, 10, 65, 68, 67, 79 }; + /** Standard buffer start */ + public static final byte[] STANDARD_BUFFER_START = { 2, 10, 65, 68, 83, 67 }; + + /** + * Find mode from Frame Buffer + * + * @param frameBuffer + * @return TICMode + * @throws TICException + */ + public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) throws TICException + { + byte[] frameBufferStart = new byte[HISTORIC_BUFFER_START.length]; + if (frameBuffer.length < frameBufferStart.length) + { + throw new TICException("Tic frame read 0x" + bytesToHex(frameBuffer) + " too short to determine TIC Mode !", + TICError.TIC_READER_FRAME_DECODE_FAILED); + } + System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); + if (Arrays.equals(frameBufferStart, HISTORIC_BUFFER_START)) + { + return HISTORIC; + } + else + { + if (STANDARD_BUFFER_START.length != HISTORIC_BUFFER_START.length) + { + frameBufferStart = new byte[STANDARD_BUFFER_START.length]; + System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); + } + if (Arrays.equals(frameBufferStart, STANDARD_BUFFER_START)) + { + return STANDARD; + } + return null; + } + } + + /** + * Find Mode frome Group Buffer + * + * @param groupBuffer + * @return TICMode + */ + public static TICMode findModeFromGroupBuffer(byte[] groupBuffer) + { + for (int i = 0; i < groupBuffer.length; i++) + { + if (groupBuffer[i] == HISTORIC_SEPARATOR) + { + return HISTORIC; + } + else if (groupBuffer[i] == STANDARD_SEPARATOR) + { + return STANDARD; + } + } + return null; + } + + /** + * Convert byte array to hexadecimal string (replacement for + * DatatypeConverter.printHexBinary) + * + * @param bytes the byte array to convert + * @return hexadecimal string representation + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02X", b)); + } + return result.toString(); + } + +} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java new file mode 100644 index 0000000..468c112 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java @@ -0,0 +1,326 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.channels; + +import java.util.Arrays; + +import enedis.lab.io.channels.ChannelConfiguration; +import enedis.lab.io.channels.ChannelException; +import enedis.lab.io.channels.serialport.ChannelSerialPort; +import enedis.lab.io.channels.serialport.ChannelSerialPortErrorCode; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.BytesArray; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.time.Time; + +/** + * Channel TIC SerialPort + */ +public class ChannelTICSerialPort extends ChannelSerialPort +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final byte START_PATTERN = (byte) 0x02; + private static final byte END_PATTERN = (byte) 0x03; + private static final int RECEIVE_DATA_POLLING_PERIOD = 100; + private static final byte[] HISTORIC_BUFFER_START = { 2, 10, 65, 68, 67, 79 }; + private static final byte[] STANDARD_BUFFER_START = { 2, 10, 65, 68, 83, 67 }; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private TICMode currentMode; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor: create an instance from a SerialPortConfiguration + * + * @param configuration + * @throws ChannelException + */ + public ChannelTICSerialPort(ChannelTICSerialPortConfiguration configuration) throws ChannelException + { + super(configuration); + this.currentMode = null; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Channel + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void setup(ChannelConfiguration configuration) throws ChannelException + { + if (configuration == null) + { + ChannelException.raiseInvalidConfiguration("null"); + } + if (!(configuration instanceof ChannelTICSerialPortConfiguration)) + { + ChannelException.raiseInvalidConfigurationType(configuration, ChannelTICSerialPortConfiguration.class.getSimpleName()); + } + super.setup(configuration); + } + + @Override + public ChannelTICSerialPortConfiguration getConfiguration() + { + return (ChannelTICSerialPortConfiguration) this.configuration; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get mode + * + * @return TIC Mode + */ + public TICMode getMode() + { + return (this.getCurrentMode() != null) ? this.getCurrentMode() : this.getSelectedMode(); + } + + /** + * Get selected mode + * + * @return selected Mode + */ + public TICMode getSelectedMode() + { + return this.getConfiguration().getTicMode(); + } + + /** + * Get current mode + * + * @return current Mode + */ + public TICMode getCurrentMode() + { + return this.currentMode; + } + + @Override + public byte[] read() throws ChannelException + { + long beginTime = System.currentTimeMillis(); + long elapsedTime = 0; + long timeout = this.getSyncReadTimeout(); + BytesArray buffer = new BytesArray(); + byte[] ticFrame = null; + int startOfFrame = -1, endOfFrame = -1; + + while (elapsedTime < timeout || timeout == 0) + { + if (this.available() > 0) + { + buffer.addAll(super.read(1)); + if (startOfFrame == -1) + { + startOfFrame = buffer.indexOf(START_PATTERN); + if (startOfFrame != -1) + { + endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); + if (endOfFrame != -1) + { + ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); + break; + } + } + } + else + { + endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); + if (endOfFrame != -1) + { + ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); + break; + } + } + } + else + { + Time.sleep(RECEIVE_DATA_POLLING_PERIOD); + elapsedTime = (System.currentTimeMillis() - beginTime); + } + } + + return ticFrame; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void receiveData() + { + if (this.currentMode == null) + { + if (this.autoDetectMode() == null) + { + this.onReadTimeout(); + return; + } + } + try + { + byte[] ticFrame = this.read(); + + if (ticFrame == null) + { + this.onReadTimeout(); + } + else + { + this.notifyOnDataRead(ticFrame); + } + } + catch (ChannelException e) + { + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), "TIC read failed: " + e.getErrorInfo()); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void onReadTimeout() + { + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_TIMEOUT.getCode(ERROR_CODE_OFFSET), "TIC read timeout"); + this.configurePort(); + this.currentMode = null; + } + + private void configurePort() + { + this.closePort(); + this.openPort(); + try + { + this.flush(); + } + catch (ChannelException e) + { + } + } + + private void setSelectedMode(TICMode ticMode) + { + try + { + this.getConfiguration().setTicMode(ticMode); + } + catch (DataDictionaryException e) + { + this.logger.error("Cannot set TIC mode " + ticMode, e); + } + } + + private boolean checkAndUpdateMode(TICMode ticMode) + { + boolean result = false; + this.setSelectedMode(ticMode); + this.configurePort(); + try + { + byte[] ticFrame = this.read(); + if (ticFrame != null && ticFrame.length > HISTORIC_BUFFER_START.length) + { + byte[] ticFrameStart = new byte[HISTORIC_BUFFER_START.length]; + System.arraycopy(ticFrame, 0, ticFrameStart, 0, ticFrameStart.length); + if (ticMode == TICMode.HISTORIC) + { + if (Arrays.equals(ticFrameStart, HISTORIC_BUFFER_START)) + { + result = true; + } + } + else if (ticMode == TICMode.STANDARD) + { + if (Arrays.equals(ticFrameStart, STANDARD_BUFFER_START)) + { + result = true; + } + } + } + } + catch (ChannelException e) + { + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), "TIC read failed: " + e.getErrorInfo()); + } + if (result) + { + this.currentMode = ticMode; + } + this.setSelectedMode(TICMode.AUTO); + + return result; + } + + private TICMode autoDetectMode() + { + if (this.getSelectedMode() != TICMode.AUTO) + { + this.currentMode = this.getSelectedMode(); + return this.getSelectedMode(); + } + this.logger.debug("Auto detecting TIC Mode"); + if (!this.checkAndUpdateMode(TICMode.HISTORIC)) + { + if (!this.checkAndUpdateMode(TICMode.STANDARD)) + { + this.logger.debug("TIC Mode not detected"); + + return null; + } + this.logger.debug("TIC Mode STANDARD detected"); + + return TICMode.STANDARD; + } + this.logger.debug("TIC Mode HISTORIC detected"); + + return TICMode.HISTORIC; + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java new file mode 100644 index 0000000..ecfeff6 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java @@ -0,0 +1,250 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.channels; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.io.channels.serialport.ChannelSerialPortConfiguration; +import enedis.lab.io.channels.serialport.Parity; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; + +/** + * ChannelTICSerialPortConfiguration class + * + * Generated + */ +public class ChannelTICSerialPortConfiguration extends ChannelSerialPortConfiguration +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_TIC_MODE = "ticMode"; + + protected static final Number BAUDRATE_HISTORIC = 1200; + protected static final Number BAUDRATE_STANDARD = 9600; + protected static final Number[] BAUDRATE_ACCEPTED_VALUES = { BAUDRATE_HISTORIC, BAUDRATE_STANDARD }; + protected static final Parity PARITY_ACCEPTED_VALUE = Parity.EVEN; + protected static final Number DATA_BITS_ACCEPTED_VALUE = 7; + protected static final Number STOP_BITS_ACCEPTED_VALUE = 1.0d; + protected static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kTicMode; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected ChannelTICSerialPortConfiguration() + { + super(); + this.loadKeyDescriptors(); + + this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); + this.kParity.setAcceptedValues(PARITY_ACCEPTED_VALUE); + this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUE); + this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUE); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ChannelTICSerialPortConfiguration(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ChannelTICSerialPortConfiguration(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * + * Constructor setting configuration name/file and parameters to default values + * + * @param name + * the configuration name + * @param file + * the configuration file + */ + public ChannelTICSerialPortConfiguration(String name, File file) + { + this(); + this.init(name, file); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param portName + * @param ticMode + * @throws DataDictionaryException + */ + public ChannelTICSerialPortConfiguration(String name, String portName, TICMode ticMode) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setPortName(portName); + this.setTicMode(ticMode); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// ChannelSerialPortConfiguration + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_TIC_MODE)) + { + this.setTicMode(TIC_MODE_DEFAULT_VALUE); + } + switch (this.getTicMode()) + { + case HISTORIC: + { + this.setBaudrate(BAUDRATE_HISTORIC); + this.setParity(PARITY_ACCEPTED_VALUE); + this.setDataBits(DATA_BITS_ACCEPTED_VALUE); + this.setStopBits(STOP_BITS_ACCEPTED_VALUE); + break; + } + case STANDARD: + case AUTO: + default: + { + this.setBaudrate(BAUDRATE_STANDARD); + this.setParity(PARITY_ACCEPTED_VALUE); + this.setDataBits(DATA_BITS_ACCEPTED_VALUE); + this.setStopBits(STOP_BITS_ACCEPTED_VALUE); + } + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get tic mode + * + * @return the tic mode + */ + public TICMode getTicMode() + { + return (TICMode) this.data.get(KEY_TIC_MODE); + } + + /** + * Set tic mode + * + * @param ticMode + * @throws DataDictionaryException + */ + public void setTicMode(TICMode ticMode) throws DataDictionaryException + { + this.setTicMode((Object) ticMode); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setTicMode(Object ticMode) throws DataDictionaryException + { + this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); + switch (this.getTicMode()) + { + case HISTORIC: + this.setBaudrate(BAUDRATE_HISTORIC); + break; + case STANDARD: + case AUTO: + this.setBaudrate(BAUDRATE_STANDARD); + break; + default: + break; + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); + this.keys.add(this.kTicMode); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java new file mode 100644 index 0000000..5a47500 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java @@ -0,0 +1,823 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels.serialport; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.apache.commons.lang3.SystemUtils; + +import enedis.lab.io.channels.ChannelConfiguration; +import enedis.lab.io.channels.ChannelException; +import enedis.lab.io.channels.ChannelPhysical; +import enedis.lab.io.channels.ChannelStatus; +import enedis.lab.io.serialport.SerialPortDescriptor; +import enedis.lab.io.serialport.SerialPortFinderBase; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.SystemError; +import jssc.SerialPort; +import jssc.SerialPortEvent; +import jssc.SerialPortEventListener; +import jssc.SerialPortException; +import jssc.SerialPortTimeoutException; + +/** + * Channel serial port + */ +public class ChannelSerialPort extends ChannelPhysical implements SerialPortEventListener +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final int ERROR_CODE_OFFSET = 1000; + private static final int DEFAULT_DELAY = 500; + private static final int DELAY_REOPEN = DEFAULT_DELAY * 10; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Check if the port is found + * + * @param portId + * @param portName + * @return true if the specified port is found + */ + public static boolean isPortFound(String portId, String portName) + { + return SerialPortFinderBase.getInstance().findByPortIdOrPortName(portId, portName) != null; + } + + /** + * Find the port name corresponding to the given portId or portName. portId has a higher priority + * + * @param portId + * @param portName + * @return port name + */ + public static String findPortName(String portId, String portName) + { + SerialPortDescriptor descriptor = null; + if (portId != null) + { + descriptor = SerialPortFinderBase.getInstance().findByPortId(portId); + } + else + { + descriptor = SerialPortFinderBase.getInstance().findByPortName(portName); + } + + return (descriptor != null) ? descriptor.getPortName() : null; + } + + /** + * Convert the parity provided by the SerialPortConfiguration to a value usable by the channel + * + * @param parity + * @return parity + */ + public static int parityFromConfiguration(Parity parity) + { + int retVal = -1; + + switch (parity) + { + case EVEN: + retVal = SerialPort.PARITY_EVEN; + break; + case MARK: + retVal = SerialPort.PARITY_MARK; + break; + case NONE: + retVal = SerialPort.PARITY_NONE; + break; + case ODD: + retVal = SerialPort.PARITY_ODD; + break; + case SPACE: + retVal = SerialPort.PARITY_SPACE; + break; + default: /* Cas non atteignable */ + } + + return retVal; + } + + /** + * Convert the stopBits parameter provided by the SerialPortConfiguration to a value usable by the channel + * + * @param stop_bits + * @return stopBits + */ + public static int stopBitsFromConfiguration(float stop_bits) + { + int retVal = -1; + + if (1 == stop_bits) + { + retVal = SerialPort.STOPBITS_1; + } + + else if (1.5 == stop_bits) + { + retVal = SerialPort.STOPBITS_1_5; + } + + else if (2 == stop_bits) + { + retVal = SerialPort.STOPBITS_2; + } + + return retVal; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private SerialPort portHandler = null; + + private String previousPortName; + private String currentPortName; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor: create an instance from a ChannelConfiguration + * + * @param configuration + * @throws ChannelException + */ + public ChannelSerialPort(ChannelConfiguration configuration) throws ChannelException + { + super(configuration); + this.setPeriod(DEFAULT_DELAY); + + this.currentPortName = this.findPortName(); + this.previousPortName = this.currentPortName; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Channel + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void start() + { + this.logger.info("Channel " + this.getName() + " start (" + this.findPortName() + ")"); + this.openPort(); + super.start(); + } + + @Override + public void stop() + { + this.logger.info("Channel " + this.getName() + " stop (" + this.findPortName() + ")"); + super.stop(); + this.closePort(); + } + + @Override + public void setup(ChannelConfiguration configuration) throws ChannelException + { + if (configuration == null) + { + ChannelException.raiseInvalidConfiguration("null"); + } + if (!(configuration instanceof ChannelSerialPortConfiguration)) + { + ChannelException.raiseInvalidConfigurationType(configuration, ChannelSerialPortConfiguration.class.getSimpleName()); + } + super.setup(configuration); + } + + @Override + public byte[] read() throws ChannelException + { + byte[] buffer = null; + + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + buffer = this.portHandler.readBytes(); + } + catch (SerialPortException exception) + { + this.setStatus(ChannelStatus.ERROR); + this.logger.error("Cannot read bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + + return buffer; + } + + @Override + public void write(byte[] data) throws ChannelException + { + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + this.portHandler.writeBytes(data); + } + catch (SerialPortException exception) + { + this.setStatus(ChannelStatus.ERROR); + this.logger.error("Cannot write bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + } + + @Override + public ChannelSerialPortConfiguration getConfiguration() + { + return (ChannelSerialPortConfiguration) this.configuration.clone(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// ChannelBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void setup() throws ChannelException + { + /* 1. Save current state */ + ChannelStatus currentStatus = this.status; + /* 2. Stop serial port */ + this.stop(); + /* 3. update path if symbolic link */ + this.updateRealPath(); + /* 4. Create serial port handler */ + if ((null == this.portHandler) || (false == this.getPortName().equals(this.portHandler.getPortName()))) + { + this.portHandler = new SerialPort(this.getPortName()); + } + /* 5. Restore current state */ + if (ChannelStatus.STARTED == currentStatus) + { + this.start(); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// SerialPortEventListener + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void serialEvent(SerialPortEvent event) + { + switch (event.getEventType()) + { + case SerialPortEvent.RXCHAR: + this.receiveData(); + break; + default: /* Aucune action */ + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Runnable + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void process() + { + int delay = DEFAULT_DELAY; + + if (this.status == ChannelStatus.STARTED) + { + if (!this.isPortFound()) + { + this.onPortNotFound(); + } + else if (this.hasPortChanged()) + { + this.onPortNameChanged(); + } + else if (!this.hasEventListener()) + { + this.receiveData(); + } + } + else + { + if (this.isPortFound()) + { + this.openPort(); + if (this.status == ChannelStatus.ERROR) + { + delay = DELAY_REOPEN; + } + } + } + this.setPeriod(delay); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Blocking Read + * + * @param bytesCount + * @return read bytes + * @throws ChannelException + */ + public byte[] read(int bytesCount) throws ChannelException + { + byte[] buffer = null; + + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + buffer = this.portHandler.readBytes(bytesCount); + } + catch (SerialPortException exception) + { + this.setStatus(ChannelStatus.ERROR); + this.logger.error("Cannot read bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + + return buffer; + } + + /** + * Blocking read with timeout + * + * @param bytesCount + * @param timeout + * @return read bytes + * @throws ChannelException + */ + public byte[] read(int bytesCount, int timeout) throws ChannelException + { + byte[] buffer = null; + + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + buffer = this.portHandler.readBytes(bytesCount, timeout); + } + catch (SerialPortException exception) + { + this.logger.error("Cannot read bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + catch (SerialPortTimeoutException exception) + { + ChannelException.raiseInternalError(exception.getMessage()); + } + + return buffer; + } + + /** + * Get available bytes count + * + * @return Available bytes count + * @throws ChannelException + * if serial port not open or internal error occurs + */ + public int available() throws ChannelException + { + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + return this.portHandler.getInputBufferBytesCount(); + } + catch (SerialPortException exception) + { + this.logger.error("Cannot get bytes available", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + + return 0; + } + + /** + * Flush port + * + * @throws ChannelException + */ + public void flush() throws ChannelException + { + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + int mask = SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_RXABORT | SerialPort.PURGE_TXCLEAR | SerialPort.PURGE_TXABORT; + if (!this.portHandler.purgePort(mask)) + { + this.logger.error("Channel " + this.getName() + " failed to purge port " + this.getPortName() + " (" + SystemError.getMessage() + ") "); + } + } + catch (SerialPortException exception) + { + this.logger.error("Cannot purge port", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + } + + /** + * + * @return the identifier of the port. If the parameter has not been set, 'null' is returned. + */ + public String getPortId() + { + return this.getChannelConfiguration().getPortId(); + } + + /** + * + * @return the name of the port. If the parameter has not been set, 'null' is returned. + */ + public String getPortName() + { + return this.getChannelConfiguration().getPortName(); + } + + /** + * + * @return the name of the port opened. If the port has not been opened, 'null' is returned. + */ + public String getPortNameOpened() + { + return (this.portHandler != null) ? this.portHandler.getPortName() : null; + } + + /** + * Find the port name associated with configuration + * + * @return Port name found or null if nothing found + */ + public String findPortName() + { + return findPortName(this.getPortId(), this.getPortName()); + } + + /** + * @return the current baudrate value. If the Baudrate has not been set (port has not been used), -1 is returned + */ + public int getBaudrate() + { + return this.getChannelConfiguration().getBaudrate().intValue(); + } + + /** + * @return the number of bits used for data (5, 6, 7, 8). If the parameter does not exist in channel configuration + * or is not consistent, the value '-1' is returned. + */ + public int getDataBits() + { + return this.getChannelConfiguration().getDataBits().intValue(); + } + + /** + * @return the current number of bits used for stop (1.0 , 1.5 , 2.0). + * + * NB : If the port has been used, the method returns -1. + */ + public float getStopBits() + { + return this.getChannelConfiguration().getStopBits().floatValue(); + } + + /** + * @return the current parity used by the port + * + * NB : If the port has been used, the method returns null. + */ + public Parity getParity() + { + return this.getChannelConfiguration().getParity(); + } + + /** + * @return the timeout used for a synchronous read operation + */ + public int getSyncReadTimeout() + { + return this.getChannelConfiguration().getSyncReadTimeout().intValue(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected boolean hasEventListener() + { + return false; + } + + protected boolean addEventListener() + { + if (this.portHandler == null) + { + return false; + } + int mask = SerialPort.MASK_RXCHAR + SerialPort.MASK_CTS + SerialPort.MASK_DSR; + try + { + if (!this.portHandler.setEventsMask(mask)) + { + this.logger.error("Channel " + this.getName() + " failed to set event mask on port " + this.getPortName() + " (" + SystemError.getMessage() + ") "); + return false; + } + } + catch (SerialPortException exception) + { + this.logger.error("Channel " + this.getName() + " failed to set event mask on port " + this.getPortName() + " : " + exception.getMessage()); + return false; + } + try + { + this.portHandler.addEventListener(this); + } + catch (SerialPortException e) + { + this.logger.error("Channel " + this.getName() + " failed to add listener :" + e.getMessage(), e); + return false; + } + + return true; + } + + protected boolean removeEventListener() + { + if (this.portHandler == null) + { + return false; + } + try + { + if (!this.portHandler.removeEventListener()) + { + this.logger.error("Channel " + this.getName() + " failed to remove listener"); + return false; + } + } + catch (SerialPortException e) + { + this.logger.error("Channel " + this.getName() + " failed to remove listener :" + e.getMessage(), e); + return false; + } + + return true; + } + + protected void receiveData() + { + try + { + if (this.available() > 0) + { + byte[] buffer = this.read(); + this.notifyOnDataRead(buffer); + } + } + catch (ChannelException exception) + { + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), exception.getMessage()); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private boolean hasPortChanged() + { + boolean portChanged = false; + + this.currentPortName = this.findPortName(); + + if (this.currentPortName != null && !this.currentPortName.equalsIgnoreCase(this.previousPortName)) + { + portChanged = true; + } + + return portChanged; + } + + private void onPortNameChanged() + { + String errorMessage = "Port name has changed ( " + this.previousPortName + " --> " + this.currentPortName + " )"; + this.logger.error(errorMessage); + this.setStatus(ChannelStatus.ERROR); + this.closePortForced(); + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_NAME_HAS_CHANGED.getCode(ERROR_CODE_OFFSET), errorMessage); + this.previousPortName = this.currentPortName; + } + + private void updateRealPath() throws ChannelException + { + if (SystemUtils.IS_OS_LINUX) + { + String realPortName; + try + { + Process p = Runtime.getRuntime().exec("realpath " + this.getPortName()); + BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream())); + realPortName = stdInput.readLine(); + this.getChannelConfiguration().setPortName(realPortName); + } + catch (IOException | DataDictionaryException e) + { + throw new ChannelException(ChannelException.ERRCODE_INVALID_CONFIGURATION, "Cannot resolve realpath (" + e.getMessage() + ")"); + } + } + } + + private ChannelSerialPortConfiguration getChannelConfiguration() + { + return (ChannelSerialPortConfiguration) this.configuration; + } + + protected void openPort() + { + /* 1. Check if serial port is already opened */ + if (this.portHandler != null && this.portHandler.isOpened()) + { + return; + } + /* 2. Open serial port */ + String portNameFound = this.findPortName(); + try + { + this.logger.info("Channel " + this.getName() + " opening port " + portNameFound); + if (this.portHandler == null) + { + this.portHandler = new SerialPort(portNameFound); + } + if (!this.portHandler.openPort()) + { + this.logger.error("Channel " + this.getName() + " failed to open port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); + return; + } + this.logger.info("Channel " + this.getName() + " configuring port " + portNameFound); + if (!this.portHandler.setParams(this.getBaudrate(), this.getDataBits(), stopBitsFromConfiguration(this.getStopBits()), parityFromConfiguration(this.getParity()))) + { + this.logger.error("Channel " + this.getName() + " failed to configure port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); + return; + } + } + catch (SerialPortException exception) + { + String errorMessage = "Channel " + this.getName() + " failed to open port " + this.getPortNameOpened() + " : " + exception.getMessage(); + this.logger.error(errorMessage); + if (exception.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) + { + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_BUSY.getCode(ERROR_CODE_OFFSET), errorMessage); + } + this.closePortForced(); + return; + } + /* 3. Add serial event listener */ + if (this.hasEventListener()) + { + if (!this.addEventListener()) + { + return; + } + } + this.setStatus(ChannelStatus.STARTED); + } + + protected void closePort() + { + /* 1. Check if serial port is already closed */ + if (this.portHandler == null || !this.portHandler.isOpened()) + { + return; + } + /* 2. Remove serial event listener */ + if (this.hasEventListener()) + { + this.removeEventListener(); + } + /* 3. Close serial port */ + try + { + this.logger.info("Channel " + this.getName() + " closing port " + this.getPortNameOpened()); + if (!this.portHandler.closePort()) + { + this.logger.error("Channel " + this.getName() + " failed to close port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); + return; + } + } + catch (SerialPortException exception) + { + this.logger.error("Channel " + this.getName() + " failed to close port " + this.getPortNameOpened() + " : " + exception.getMessage()); + return; + } + this.setStatus(ChannelStatus.STOPPED); + } + + protected void closePortForced() + { + try + { + this.portHandler.closePort(); + } + catch (SerialPortException exception) + { + this.logger.error("Channel " + this.getName() + " fail on close : close " + this.getPortNameOpened() + " failed due to " + exception.getMessage()); + } + this.portHandler = new SerialPort(this.findPortName()); + } + + protected boolean isPortFound() + { + return isPortFound(this.getPortId(), this.getPortName()); + } + + protected boolean isPortOpenedFound() + { + String portNameOpened = this.getPortNameOpened(); + if (portNameOpened == null) + { + return false; + } + String portNameFound = this.findPortName(); + + return portNameOpened.equals(portNameFound); + } + + protected void onPortNotFound() + { + String errorMessage = "Channel " + this.getName() + " : serial port " + this.findPortName() + " not found"; + this.logger.error(errorMessage); + this.setStatus(ChannelStatus.ERROR); + this.closePortForced(); + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); + } + + protected void onPortOpenedNotFound() + { + String errorMessage = "Channel " + this.getName() + " : serial port " + this.getPortNameOpened() + " opened not found"; + this.logger.error(errorMessage); + this.setStatus(ChannelStatus.ERROR); + this.closePortForced(); + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_OPENED_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java new file mode 100644 index 0000000..29647b4 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java @@ -0,0 +1,61 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels.serialport; + +/** + * Channel serial port error code + */ +public enum ChannelSerialPortErrorCode +{ + /** Port not found */ + PORT_NOT_FOUND(1), + + /** Port opened not found */ + PORT_OPENED_NOT_FOUND(2), + + /** Port name has changed */ + PORT_NAME_HAS_CHANGED(3), + + /** Port busy */ + PORT_BUSY(4), + + /** Read operation failed */ + READ_FAILED(5), + + /** Read operation timed out */ + READ_TIMEOUT(6); + + private int code; + + private ChannelSerialPortErrorCode(int code) + { + this.code = code; + } + + /** + * Get error code without offset + * + * @return error code + */ + public int getCode() + { + return this.code; + } + + /** + * Get error code with offset + * + * @param offset + * + * @return error code + */ + public int getCode(int offset) + { + return offset + this.code; + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java new file mode 100644 index 0000000..726f099 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java @@ -0,0 +1,172 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.codec; + +import java.util.List; + +import enedis.lab.codec.Codec; +import enedis.lab.codec.CodecException; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; +import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; +import enedis.lab.types.BytesArray; + +/** + * Codec TIC Frame Historic + * + */ +public class CodecTICFrameHistoric implements Codec +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Separator */ + public static final byte SEPARATOR = 0x20; // SP + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Codec + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public TICFrameHistoric decode(byte[] bytesBuffer) throws CodecException + { + String errorMessage = ""; + BytesArray rawDataSet = null; + TICFrameHistoric ticFrame = null; + CodecTICFrameHistoricDataSet codecTICFrameHistoricDataSet = new CodecTICFrameHistoricDataSet(); + + BytesArray bytesArray = new BytesArray(bytesBuffer); + + if ((true == bytesArray.startsWith(TICFrame.BEGINNING_PATTERN)) && (true == bytesArray.endsWith(TICFrame.END_PATTERN)) && (bytesArray.contains(TICFrame.EOT) == false)) + { + bytesArray.remove(0); + bytesArray.remove(bytesArray.size() - 1); + + ticFrame = new TICFrameHistoric(); + + List datasetList = bytesArray.slice(TICFrameDataSet.BEGINNING_PATTERN, TICFrameDataSet.END_PATTERN, BytesArray.CONTIGUOUS); + + if (datasetList.isEmpty() == false) + { + for (int i = 0; i < datasetList.size(); i++) + { + TICFrameHistoricDataSet dataSet = null; + rawDataSet = datasetList.get(i); + byte[] rawDataSetByte = rawDataSet.getBytes(); + + try + { + dataSet = codecTICFrameHistoricDataSet.decode(rawDataSetByte); + ticFrame.addDataSet(dataSet); + } + catch (CodecException exception) + { + errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; + } + } + } + } + + if (errorMessage.isEmpty()) + { + return ticFrame; + } + else + { + throw new CodecException(errorMessage, ticFrame); + } + + } + + @Override + public byte[] encode(TICFrameHistoric ticFrameHistoric) throws CodecException + { + String errorMessage = ""; + CodecTICFrameHistoricDataSet codec = new CodecTICFrameHistoricDataSet(); + BytesArray dataSet = new BytesArray(); + + List ticFrameHistoricList = ticFrameHistoric.getDataSetList(); + + if (ticFrameHistoric != null && !ticFrameHistoricList.isEmpty()) + { + List groups = ticFrameHistoric.getDataSetList(); + dataSet.add(TICFrame.BEGINNING_PATTERN); + for (int i = 0; i < groups.size(); i++) + { + try + { + byte[] buff = codec.encode((TICFrameHistoricDataSet) groups.get(i)); + dataSet.addAll(buff); + } + catch (CodecException e) + { + errorMessage += e.getMessage() + " : " + new String(ticFrameHistoric.getBytes()) + "\n"; + } + } + dataSet.add(TICFrame.END_PATTERN); + } + + else + { + return null; + } + + if (errorMessage.isEmpty()) + { + return dataSet.getBytes(); + } + else + { + throw new CodecException(errorMessage, dataSet.getBytes()); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} \ No newline at end of file diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java new file mode 100644 index 0000000..c275a52 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java @@ -0,0 +1,198 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.codec; + +import java.util.List; + +import enedis.lab.codec.Codec; +import enedis.lab.codec.CodecException; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; +import enedis.lab.types.BytesArray; + +/** + * Codec TIC Frame Historic Data Set + * + */ +public class CodecTICFrameHistoricDataSet implements Codec +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Codec + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public byte[] encode(TICFrameHistoricDataSet ticFrameHistoricDataSet) throws CodecException + { + BytesArray dataSet = new BytesArray(); + + if ((ticFrameHistoricDataSet.getLabel() != null) && (ticFrameHistoricDataSet.getData() != null)) + { + dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); + dataSet.addAll(ticFrameHistoricDataSet.getLabel().getBytes()); + + dataSet.add(TICFrameHistoricDataSet.SEPARATOR); + dataSet.addAll(ticFrameHistoricDataSet.getData().getBytes()); + + dataSet.add(TICFrameHistoricDataSet.SEPARATOR); + + if (!ticFrameHistoricDataSet.isValid()) + { + throw new CodecException("Invalid Checksum value"); + } + dataSet.add(ticFrameHistoricDataSet.getChecksum()); + dataSet.add(TICFrameDataSet.END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public TICFrameHistoricDataSet decode(byte[] bytes) throws CodecException + { + BytesArray bytesArray = new BytesArray(bytes); + TICFrameHistoricDataSet dataSet = null; + + if (this.isStartStopDelimiterPresent(bytesArray)) + { + this.removeStartStopDelimiter(bytesArray); + + } + + List parts = this.splitFrame(bytesArray); + + // Structure "LABEL/DATA/CHECKSUM" : + if (this.isStructureLabelDataChecksum(parts)) + { + + if (this.isChecksumInOneByte(parts.get(2))) + + { + dataSet = new TICFrameHistoricDataSet(); + dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); + + } + else + { + throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); + } + } + else + { + throw new CodecException("Invalid format of TICFrameHistoricDataSet"); + } + + if (dataSet.isValid() == false) + { + throw new CodecException("Invalid Checksum value - Historic"); + } + + return dataSet; + } + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isStartStopDelimiterPresent(BytesArray bytes) + { + return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); + } + + private void removeStartStopDelimiter(BytesArray bytes) + + { + bytes.remove(0); + bytes.remove(bytes.size() - 1); + } + + private TICFrameHistoricDataSet createDataSetLabelDataChecksum(List parts, TICFrameHistoricDataSet dataSet) + { + + dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); + dataSet.setChecksum(parts.get(2).get(0)); + return dataSet; + } + + private boolean isChecksumInOneByte(BytesArray part) + { + return part.size() == 1; + } + + private boolean isStructureLabelDataChecksum(List parts) + { + return parts.size() == 3; + } + + private List splitFrame(BytesArray bytesArray) throws CodecException + { + List parts; + + if (bytesArray.size() < 5) + { + throw new CodecException("Not enough bytes in TICFrameHistoricDataSet"); + } + if (bytesArray.get(bytesArray.size() - 1) == TICFrameHistoricDataSet.SEPARATOR) + { + BytesArray subList = bytesArray.subList(0, bytesArray.size() - 2); + parts = subList.split(TICFrameHistoricDataSet.SEPARATOR); + if (parts.size() < 1) + { + throw new CodecException("Invalid format of TICFrameHistoricDataSet"); + } + BytesArray checksum = parts.get(parts.size() - 1); + checksum.addAll(new byte[] { TICFrameHistoricDataSet.SEPARATOR }); + } + else + { + parts = bytesArray.split(TICFrameHistoricDataSet.SEPARATOR); + } + + return parts; + } + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java new file mode 100644 index 0000000..3e98acb --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java @@ -0,0 +1,156 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.codec; + +import java.util.List; + +import enedis.lab.codec.Codec; +import enedis.lab.codec.CodecException; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; +import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; +import enedis.lab.types.BytesArray; + +/** + * Codec TIC Frame Standard + * + */ +public class CodecTICFrameStandard implements Codec +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Codec + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public TICFrameStandard decode(byte[] bytes) throws CodecException + { + String errorMessage = ""; + BytesArray rawDataSet = null; + TICFrameStandard ticFrame = null; + CodecTICFrameStandardDataSet codecTICFrameStandardDataSet = new CodecTICFrameStandardDataSet(); + + BytesArray bytesArray = new BytesArray(bytes); + + // Extract the body of the frame + // NB: the presence of an EOT character makes the content of a frame invalid + if ((bytesArray.startsWith(TICFrame.BEGINNING_PATTERN) == true) && (bytesArray.endsWith(TICFrame.END_PATTERN) == true) && (bytesArray.contains(TICFrame.EOT) == false)) + { + bytesArray.remove(0); + bytesArray.remove(bytesArray.size() - 1); + + ticFrame = new TICFrameStandard(); + + // Isolate each memory area supposed to correspond to an Information Group + // (DataSet) + List datasetList = bytesArray.slice(TICFrameDataSet.BEGINNING_PATTERN, TICFrameDataSet.END_PATTERN, BytesArray.CONTIGUOUS); + + // Analyze each memory area to extract the controls of the Group of information + // If the format of a zone is invalid, then the browse is interrupted and the whole frame is invalid + // (null) + if (datasetList.isEmpty() == false) + { + for (int i = 0; i < datasetList.size(); i++) + { + TICFrameStandardDataSet dataSet = null; + rawDataSet = datasetList.get(i); + byte[] rawDataSetByte = rawDataSet.getBytes(); + try + { + dataSet = codecTICFrameStandardDataSet.decode(rawDataSetByte); + ticFrame.addDataSet(dataSet); + } + catch (CodecException exception) + { + errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; + } + } + } + } + + if (errorMessage.isEmpty()) + { + return ticFrame; + } + else + { + throw new CodecException(errorMessage, ticFrame); + } + } + + @Override + public byte[] encode(TICFrameStandard ticFrameStandard) throws CodecException + { + CodecTICFrameStandardDataSet codec = new CodecTICFrameStandardDataSet(); + BytesArray dataSet = new BytesArray(); + + List ticFrameStandardList = ticFrameStandard.getDataSetList(); + + if (ticFrameStandard != null && !ticFrameStandardList.isEmpty()) + { + List groups = ticFrameStandard.getDataSetList(); + dataSet.add(TICFrame.BEGINNING_PATTERN); + for (int i = 0; i < groups.size(); i++) + { + byte[] buff = codec.encode((TICFrameStandardDataSet) groups.get(i)); + dataSet.addAll(buff); + } + dataSet.add(TICFrame.END_PATTERN); + return dataSet.getBytes(); + } + else + { + return null; + } + } + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java new file mode 100644 index 0000000..9e89e85 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java @@ -0,0 +1,197 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.codec; + +import java.util.List; + +import enedis.lab.codec.Codec; +import enedis.lab.codec.CodecException; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; +import enedis.lab.types.BytesArray; + +/** + * Codec TIC Frame Standard Data Set + * + */ +public class CodecTICFrameStandardDataSet implements Codec +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Codec + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public byte[] encode(TICFrameStandardDataSet ticFrameStandardDataSet) throws CodecException + { + BytesArray dataSet = new BytesArray(); + + if ((null != ticFrameStandardDataSet.getLabel()) && (null != ticFrameStandardDataSet.getData())) + { + dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); + dataSet.addAll(ticFrameStandardDataSet.getLabel().getBytes()); + + if (ticFrameStandardDataSet.checkDateTime() == true) + { + dataSet.add(TICFrameStandardDataSet.SEPARATOR); + dataSet.addAll(ticFrameStandardDataSet.getDateTime().getBytes()); + } + + dataSet.add(TICFrameStandardDataSet.SEPARATOR); + dataSet.addAll(ticFrameStandardDataSet.getData().getBytes()); + + dataSet.add(TICFrameStandardDataSet.SEPARATOR); + if (!ticFrameStandardDataSet.isValid()) + { + throw new CodecException("Invalid Checksum value"); + } + + dataSet.add(ticFrameStandardDataSet.getChecksum()); + dataSet.add(TICFrameDataSet.END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public TICFrameStandardDataSet decode(byte[] bytes) throws CodecException + { + BytesArray bytesArray = new BytesArray(bytes); + TICFrameStandardDataSet dataSet = null; + + if (this.isStartStopDelimiterPresent(bytesArray)) + { + this.removeStartStopDelimiter(bytesArray); + } + + List parts = bytesArray.split(TICFrameStandardDataSet.SEPARATOR); + + // Structure "LABEL/DATA/CHECKSUM" : + if (this.isStructureLabelDataChecksum(parts)) + { + if (this.isChecksumInOneByte(parts.get(2))) + { + dataSet = new TICFrameStandardDataSet(); + dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); + } + else + { + throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); + } + } + // Structure "LABEL/DATETIME/DATA/CHECKSUM" : + else if (this.isStructureLabelDateTimeDataChecksum(parts)) + { + if (this.isChecksumInOneByte(parts.get(3))) + { + dataSet = new TICFrameStandardDataSet(); + dataSet = this.createDataSetLabelDatetimeDataChecksum(parts, dataSet); + } + else + { + throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); + } + } + else + { + throw new CodecException("Invalid bytes for decode - Standard"); + } + + if (dataSet.isValid() == false) + { + throw new CodecException("Invalid Checksum value"); + } + + return dataSet; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isStructureLabelDateTimeDataChecksum(List parts) + { + return parts.size() == 4; + } + + private boolean isStructureLabelDataChecksum(List parts) + { + return parts.size() == 3; + } + + private boolean isChecksumInOneByte(BytesArray part) + { + return part.size() == 1; + } + + private void removeStartStopDelimiter(BytesArray bytes) + { + bytes.remove(0); + bytes.remove(bytes.size() - 1); + } + + private boolean isStartStopDelimiterPresent(BytesArray bytes) + { + return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); + } + + private TICFrameStandardDataSet createDataSetLabelDataChecksum(List parts, TICFrameStandardDataSet dataSet) + { + dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); + dataSet.setChecksum(parts.get(2).get(0)); + return dataSet; + } + + private TICFrameStandardDataSet createDataSetLabelDatetimeDataChecksum(List parts, TICFrameStandardDataSet dataSet) + { + dataSet.setup(parts.get(0).getBytes(), parts.get(2).getBytes(), parts.get(1).getBytes()); + dataSet.setChecksum(parts.get(3).get(0)); + return dataSet; + } + +} \ No newline at end of file diff --git a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java new file mode 100644 index 0000000..2649dfb --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java @@ -0,0 +1,295 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.codec; + +import enedis.lab.codec.Codec; +import enedis.lab.codec.CodecException; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; +import enedis.lab.protocol.tic.frame.standard.TICException; +import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; +import enedis.lab.types.BytesArray; + +/** + * Codec TIC + */ +public class TICCodec implements Codec +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final TICMode DEFAULT_MODE = TICMode.STANDARD; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private BytesArray Buffer; + private TICMode mode = TICMode.UNKNOWN; + private TICMode currentMode = TICMode.UNKNOWN; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public TICCodec() + { + this.mode = TICMode.UNKNOWN; + this.currentMode = TICMode.UNKNOWN; + this.Buffer = new BytesArray(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Codec + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public TICFrame decode(byte[] newData) throws CodecException + { + CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); + CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); + TICFrameStandard ticFrameStandard = null; + TICFrameHistoric ticFrameHistoric = null; + + TICFrame ticFrame = null; + TICMode currentTICMode = null; + + try + { + switch (this.currentMode) + { + case STANDARD: + { + ticFrameStandard = codecStandard.decode(newData); + ticFrame = ticFrameStandard; + break; + } + case HISTORIC: + { + ticFrameHistoric = codecHistoric.decode(newData); + ticFrame = ticFrameHistoric; + break; + } + + case AUTO: + { + try + { + currentTICMode = TICMode.findModeFromFrameBuffer(newData); + } + catch (TICException exception) + { + throw new CodecException("can't determinated TIC Mode"); + } + + if (currentTICMode == TICMode.STANDARD) + { + ticFrameStandard = codecStandard.decode(newData); + ticFrame = ticFrameStandard; + break; + } + else if (currentTICMode == TICMode.HISTORIC) + { + ticFrameHistoric = codecHistoric.decode(newData); + ticFrame = ticFrameHistoric; + } + else + { + throw new CodecException("can't decode TIC, unable to find TIC Modem"); + } + + } + + default: + { + /**/ + } + } + } + catch (CodecException exception) + { + throw new CodecException(exception.getMessage(), exception.getData()); + } + return ticFrame; + } + + @Override + public byte[] encode(TICFrame ticFrame) throws CodecException + { + CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); + CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); + + byte[] bytesBuffer = new byte[0]; + + try + { + switch (ticFrame.getMode()) + { + case STANDARD: + { + bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); + break; + } + case HISTORIC: + { + bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); + break; + } + default: + { + bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); + break; + } + } + } + catch (CodecException exception) + { + throw new CodecException("Can't encode TICFrame" + exception.getMessage()); + } + return bytesBuffer; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Reset buffer + */ + public void reset() + { + this.Buffer.clear(); + } + + /** + * Append data to buffer + * + * @param data + */ + public void append(byte[] data) + { + this.Buffer.addAll(data); + } + + /** + * Append data to buffer + * + * @param data + */ + public void append(byte data) + { + this.Buffer.add(data); + } + + /** + * Append data to buffer + * + * @param data + */ + public void append(BytesArray data) + { + this.Buffer.addAll(data.getBytes()); + } + + /** + * Get mode + * + * @return mode + */ + public TICMode getMode() + { + return this.mode; + } + + /** + * Set mode + * + * @param mode + */ + public void setMode(TICMode mode) + { + if (this.mode != mode) + { + this.mode = mode; + + if (TICMode.AUTO != mode) + { + this.setCurrentMode(mode); + } + + else + { + this.setCurrentMode(DEFAULT_MODE); + } + } + } + + /** + * Get current mode + * + * @return current mode + */ + public TICMode getCurrentMode() + { + return this.currentMode; + } + + /** + * Set current mode + * + * @param currentMode + */ + public void setCurrentMode(TICMode currentMode) + { + if (currentMode != this.currentMode) + { + this.currentMode = currentMode; + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java new file mode 100644 index 0000000..ec4ebf3 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java @@ -0,0 +1,241 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.datastreams; + +import enedis.lab.codec.CodecException; +import enedis.lab.io.datastreams.DataInputStream; +import enedis.lab.io.datastreams.DataStreamException; +import enedis.lab.io.datastreams.DataStreamType; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.protocol.tic.channels.ChannelTICSerialPort; +import enedis.lab.protocol.tic.codec.TICCodec; +import enedis.lab.protocol.tic.frame.TICError; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; + +/** + * TIC input stream + */ +public class TICInputStream extends DataInputStream +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Key timestamp */ + public static final String KEY_TIMESTAMP = "timestamp"; + /** Key channel */ + public static final String KEY_CHANNEL = "channel"; + /** Key data */ + public static final String KEY_DATA = "data"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + protected TICCodec codec; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor using configuration + * + * @param configuration + * @throws DataStreamException + */ + public TICInputStream(TICStreamConfiguration configuration) throws DataStreamException + { + super(configuration); + this.codec = new TICCodec(); + this.codec.setCurrentMode(configuration.getTicMode()); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataInputStream + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public DataDictionary read() throws DataStreamException + { + return null; + } + + @Override + public DataStreamType getType() + { + return DataStreamType.TIC; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// ChannelListener + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onDataRead(String channelName, byte[] data) + { + if (!this.notifier.getSubscribers().isEmpty()) + { + DataDictionary ticFrame = null; + try + { + ticFrame = this.decodeTICFrame(data); + if (ticFrame != null) + { + this.notifyOnDataReceived(ticFrame); + } + } + catch (DataDictionaryException exception) + { + this.logger.error(exception.getMessage(), exception); + } + catch (CodecException exception) + { + DataDictionaryBase errorDataDictionary = new DataDictionaryBase(); + + String KEY_PARTIAL_FRAME = "partialTICFrame"; + + errorDataDictionary.addKey(KEY_PARTIAL_FRAME); + + try + { + Object exceptionData = exception.getData(); + if (exceptionData != null && exceptionData instanceof TICFrame) + { + try + { + DataDictionary frameDataDictionary = ((TICFrame) exceptionData).getDataDictionary(); + if (frameDataDictionary != null) + { + errorDataDictionary.set(KEY_PARTIAL_FRAME, frameDataDictionary); + } + else + { + this.logger.warn("Frame data dictionary is null"); + errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); + } + } + catch (DataDictionaryException frameDataDictionaryException) + { + this.logger.warn("Can't get TICFrame data dictionary: " + frameDataDictionaryException.getMessage()); + errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); + } + } + else + { + this.logger.error("No frame data available"); + errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); + } + } + catch (DataDictionaryException dataDictionaryException) + { + this.logger.error("Can't convert TICFrame to DataDictonary" + dataDictionaryException.getMessage()); + } + + this.logger.error(exception.getMessage() + errorDataDictionary.toString()); + this.onErrorDetected(channelName, TICError.TIC_READER_READ_FRAME_CHECKSUM_INVALID.getValue(), exception.getMessage(), errorDataDictionary); + } + } + } + + @Override + public void onDataWritten(String channelName, byte[] data) + { + // Not used + } + + @Override + public void onErrorDetected(String channelName, int errorCode, String errorMessage) + { + this.notifyOnErrorDetected(errorCode, errorMessage, null); + } + + @Override + public void onErrorDetected(String channelName, int errorCode, String errorMessage, DataDictionary errorData) + { + this.notifyOnErrorDetected(errorCode, errorMessage, errorData); + } + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get Mode + * + * @return TIC Mode + */ + public TICMode getMode() + { + ChannelTICSerialPort channelTIC = (ChannelTICSerialPort) this.channel; + + return channelTIC.getMode(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected DataDictionary decodeTICFrame(byte[] data) throws DataDictionaryException, CodecException + { + TICFrame ticFrame; + try + { + ticFrame = this.codec.decode(data); + } + catch (CodecException exception) + { + throw new CodecException(exception.getMessage(), exception.getData()); + } + + long timestamp = System.currentTimeMillis(); + + DataDictionaryBase decodedTICFrame = new DataDictionaryBase(); + + decodedTICFrame.set(KEY_TIMESTAMP, timestamp); + decodedTICFrame.set(KEY_CHANNEL, this.getConfiguration().getChannelName()); + decodedTICFrame.set(KEY_DATA, ticFrame); + + return decodedTICFrame; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java new file mode 100644 index 0000000..4003b4b --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java @@ -0,0 +1,220 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.datastreams; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.io.datastreams.DataStreamConfiguration; +import enedis.lab.io.datastreams.DataStreamDirection; +import enedis.lab.io.datastreams.DataStreamType; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; + +/** + * TICStreamConfiguration class + * + * Generated + */ +public class TICStreamConfiguration extends DataStreamConfiguration +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_TIC_MODE = "ticMode"; + + private static final DataStreamType TYPE_ACCEPTED_VALUE = DataStreamType.TIC; + private static final DataStreamDirection[] DIRECTION_ACCEPTED_VALUES = { DataStreamDirection.OUTPUT, DataStreamDirection.INPUT }; + private static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kTicMode; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected TICStreamConfiguration() + { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUES); + this.kChannelName.setMandatory(true); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICStreamConfiguration(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICStreamConfiguration(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * + * Constructor setting configuration name/file and parameters to default values + * + * @param name + * the configuration name + * @param file + * the configuration file + */ + public TICStreamConfiguration(String name, File file) + { + this(); + this.init(name, file); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param direction + * @param channelName + * @param ticMode + * @throws DataDictionaryException + */ + public TICStreamConfiguration(String name, DataStreamDirection direction, String channelName, TICMode ticMode) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setDirection(direction); + this.setChannelName(channelName); + this.setTicMode(ticMode); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataStreamConfiguration + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_TYPE)) + { + this.setType(TYPE_ACCEPTED_VALUE); + } + if (!this.exists(KEY_TIC_MODE)) + { + this.setTicMode(TIC_MODE_DEFAULT_VALUE); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get tic mode + * + * @return the tic mode + */ + public TICMode getTicMode() + { + return (TICMode) this.data.get(KEY_TIC_MODE); + } + + /** + * Set tic mode + * + * @param ticMode + * @throws DataDictionaryException + */ + public void setTicMode(TICMode ticMode) throws DataDictionaryException + { + this.setTicMode((Object) ticMode); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setTicMode(Object ticMode) throws DataDictionaryException + { + this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); + this.keys.add(this.kTicMode); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java new file mode 100644 index 0000000..db44f6e --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java @@ -0,0 +1,90 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame; + +import jssc.SerialPortException; + +/** + * TICClient errors definition + */ +public enum TICError +{ + /** No error */ + NO_ERROR(0), + /** Serial port not found */ + SERIAL_PORT_NOT_FOUND(17), + /** Serial port not found */ + SERIAL_PORT_INCORRECT_SERIAL_PORT(18), + /** Serial port not found */ + SERIAL_PORT_NULL_NOT_PERMITTED(19), + /** Serial port not found */ + SERIAL_PORT_ALREADY_OPENED(20), + /** Serial port not found */ + SERIAL_PORT_SERIAL_PORT_BUSY(21), + /** Serial port not found */ + SERIAL_PORT_CONFIGURATION_FAILURE(22), + /** TIC reader default error */ + TIC_READER_DEFAULT_ERROR(23), + /** TIC reader label name not found */ + TIC_READER_LABEL_NAME_NOT_FOUND(24), + /** TIC reader frame decode failed */ + TIC_READER_FRAME_DECODE_FAILED(25), + /** TIC reader read frame timeout */ + TIC_READER_READ_FRAME_TIMEOUT(26), + /** TIC reader read frame with checksum invalid */ + TIC_READER_READ_FRAME_CHECKSUM_INVALID(27),; + + private int value; + + private TICError(int value) + { + this.value = value; + } + + /** + * Get error value + * + * @return the value + */ + public int getValue() + { + return this.value; + } + + /** + * Get serial port error + * + * @param serialPortException + * @return TICError + */ + public static TICError getSerialPortError(SerialPortException serialPortException) + { + if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) + { + return TICError.SERIAL_PORT_SERIAL_PORT_BUSY; + } + if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_INCORRECT_SERIAL_PORT)) + { + return TICError.SERIAL_PORT_INCORRECT_SERIAL_PORT; + } + if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_NULL_NOT_PERMITTED)) + { + return TICError.SERIAL_PORT_NULL_NOT_PERMITTED; + } + if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_NOT_FOUND)) + { + return TICError.SERIAL_PORT_NOT_FOUND; + } + if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_ALREADY_OPENED)) + { + return TICError.SERIAL_PORT_ALREADY_OPENED; + } + return null; + } + +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java new file mode 100644 index 0000000..cd89e18 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java @@ -0,0 +1,522 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.json.JSONObject; + +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.BytesArray; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; + +/** + * TICFrame + */ +public abstract class TICFrame +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Beginning pattern */ + public static final byte BEGINNING_PATTERN = 0x02; // STX + /** End pattern */ + public static final byte END_PATTERN = 0x03; // ETX + /** End of trame pattern */ + public static final byte EOT = 0x04; // EOT + + // Options + /** No specific options */ + public static final int EXPLICIT = 0x0000; + /** No checksum options */ + public static final int NOCHECKSUM = 0x0001; + /** No date time options */ + public static final int NODATETIME = 0x0002; + /** No tic mode options */ + public static final int NOTICMODE = 0x0004; + /** No invalid options */ + public static final int NOINVALID = 0x0008; + /** Trimmed options */ + public static final int TRIMMED = NOCHECKSUM | NODATETIME | NOINVALID; + + /** Key tic frame */ + public static final String KEY_TICFRAME = "ticFrame"; + /** Key tic mode */ + public static final String KEY_TICMODE = "ticMode"; + /** Key label */ + public static final String KEY_LABEL = "label"; + /** Key data */ + public static final String KEY_DATA = "data"; + /** Key checksum */ + public static final String KEY_CHECKSUM = "checksum"; + /** Key date time */ + public static final String KEY_DATETIME = "dateTime"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + protected List DataSetList; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public TICFrame() + { + this.DataSetList = new ArrayList(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Object + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public int hashCode() + { + return Objects.hash(this.DataSetList); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (this.getClass() != obj.getClass()) + return false; + TICFrame other = (TICFrame) obj; + return Objects.equals(this.DataSetList, other.DataSetList); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Return the list of DataSet + * + * @return the list of DataSet + */ + public List getDataSetList() + { + return this.DataSetList; + } + + /** + * Set the list of DataSet + * + * @param dataSetList + */ + public void setDataSetList(List dataSetList) + { + this.DataSetList = dataSetList; + } + + /** + * Return the index of the given DataSet + * + * @param label + * Label of the DataSet + * @return the index of the given DataSet or '-1' if it does not exist + */ + public int indexOf(String label) + { + int index = -1; + + for (int i = 0; i < this.DataSetList.size(); i++) + { + if (this.DataSetList.get(i).getLabel().equals(label)) + { + index = i; + break; + } + } + + return index; + } + + /** + * Return the given DataSet + * + * @param label + * Label of the DataSet + * @return the given DataSet or 'null' if it does not exist + */ + public TICFrameDataSet getDataSet(String label) + { + TICFrameDataSet dataSet = null; + + for (int i = 0; i < this.DataSetList.size(); i++) + { + if (this.DataSetList.get(i).getLabel().equals(label)) + { + dataSet = this.DataSetList.get(i); + break; + } + } + + return dataSet; + } + + /** + * Return the data field of the given DataSet + * + * @param label + * Label of the DataSet + * @return the data field of the given DataSet + */ + public String getData(String label) + { + TICFrameDataSet dataSet = this.getDataSet(label); + + if (dataSet != null) + { + return dataSet.getData(); + } + + else + { + return null; + } + } + + /** + * Add the dataSet at the end of the list. If the dataSet already existed, its data and checksum are just set + * + * @param dataSet + */ + public void addDataSet(TICFrameDataSet dataSet) + { + TICFrameDataSet oldDataSet = this.getDataSet(dataSet.getLabel()); + + if (oldDataSet == null) + { + this.DataSetList.add(dataSet); + } + + else + { + oldDataSet.setData(dataSet.getData()); + oldDataSet.setChecksum(dataSet.getChecksum()); + } + } + + /** + * Add the dataSet at the given index of the list. If the dataSet already existed, its data and checksum are set and + * it is moved at the given index + * + * @param index + * @param dataSet + */ + public void addDataSet(int index, TICFrameDataSet dataSet) + { + int dataSetIndex = this.indexOf(dataSet.getLabel()); + + if (dataSetIndex < 0) + { + this.DataSetList.add(index, dataSet); + } + + else + { + if (dataSetIndex != index) + { + this.removeDataSet(dataSetIndex); + this.DataSetList.add(index, dataSet); + } + + else + { + this.DataSetList.get(index).setData(dataSet.getData()); + this.DataSetList.get(index).setChecksum(dataSet.getChecksum()); + } + } + } + + /** + * Add data set + * + * @param label + * @param data + * @return tic frame data set + */ + public abstract TICFrameDataSet addDataSet(String label, String data); + + /** + * Add data set + * + * @param index + * @param label + * @param data + * @return tic frame data set + */ + public abstract TICFrameDataSet addDataSet(int index, String label, String data); + + /** + * Move the DataSet at the given index. + * + * @param label + * @param index + * @return true if the operation has been done, else return false + */ + public boolean moveDataSet(String label, int index) + { + int previous_index = this.indexOf(label); + + // Si l'ensemble des conditions suivantes sont vérifiées : + // - le Groupe d'Information existe, + // - l'index donné est cohérent + // - l'index donné diffère de l'index actuel + if ((previous_index >= 0) && (index >= 0) && (index < this.DataSetList.size()) && (index != previous_index)) + { + // Alors déplacer le Groupe d'Information à l'index donné: + + TICFrameDataSet dataSet = this.DataSetList.remove(previous_index); + + if (index < this.DataSetList.size()) + { + this.DataSetList.add(index, dataSet); + } + + else + { + this.DataSetList.add(dataSet); + } + + return true; + } + + else + { + // Sinon, l'opération n'est pas effectuée + return false; + } + } + + /** + * Set data set + * + * @param label + * @param data + * @return true if succeed + */ + public boolean setDataSet(String label, String data) + { + TICFrameDataSet dataSet = this.getDataSet(label); + + if (dataSet != null) + { + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + return true; + } + + else + { + return false; + } + } + + /** + * Set data set + * + * @param label + * @param data + * @param checksum + * @return true if succeed + */ + public boolean setDataSet(String label, String data, byte checksum) + { + TICFrameDataSet dataSet = this.getDataSet(label); + + if (dataSet != null) + { + dataSet.setData(data); + dataSet.setChecksum(checksum); + + return true; + } + + else + { + return false; + } + } + + /** + * Remove data set + * + * @param label + * @return tic frame data set + */ + public TICFrameDataSet removeDataSet(String label) + { + TICFrameDataSet dataSet = null; + + int index = this.indexOf(label); + + if (index >= 0) + { + dataSet = this.DataSetList.remove(index); + } + + return dataSet; + } + + /** + * Remove data set + * + * @param index + * @return tic frame data set + */ + public TICFrameDataSet removeDataSet(int index) + { + TICFrameDataSet dataSet = null; + + if ((index >= 0) && (index < this.DataSetList.size())) + { + dataSet = this.DataSetList.remove(index); + } + + return dataSet; + } + + /** + * Get bytes + * + * @return bytes + */ + public byte[] getBytes() + { + BytesArray bytesFrame = new BytesArray(); + + bytesFrame.add(BEGINNING_PATTERN); + + for (int i = 0; i < this.DataSetList.size(); i++) + { + bytesFrame.addAll(this.DataSetList.get(i).getBytes()); + } + + bytesFrame.add(END_PATTERN); + + return bytesFrame.getBytes(); + } + + /** + * Get data dictionary + * + * @return data dictionary + * @throws DataDictionaryException + */ + public DataDictionary getDataDictionary() throws DataDictionaryException + { + return this.getDataDictionary(0); + } + + /** + * Return a DataDictionary object corresponding to the TIC frame + * + * @param options + * options to DataDictionary render (use like bits fields) : - EXPLICIT : frame is detailed - NOCHECKSUM + * : DataSet checksum are hidden - NODATETIME : DataSet dateTime fields are hidden - NOTICMODE : Tic mode + * is hidden; - TRIMMED : NOCHECKSUM | NODATETIME - FILTER_INVALID : Invalid DataSet are filtered + * + * + * @return data dictionary + * @throws DataDictionaryException + */ + public DataDictionary getDataDictionary(int options) throws DataDictionaryException + { + DataDictionaryBase dictionary = new DataDictionaryBase(); + + JSONObject jsonObject = new JSONObject(); + + if (0 == (options & NOTICMODE)) + { + dictionary.addKey(KEY_TICMODE); + dictionary.set(KEY_TICMODE, this.getMode().name()); + } + + for (int i = 0; i < this.DataSetList.size(); i++) + { + TICFrameDataSet dataSet = this.DataSetList.get(i); + + // Si le DataSet n'est pas valid et que l'option "filtrer les + // groupes invalides" est activée, + if ((false == dataSet.isValid()) && ((options & NOINVALID) > 0)) + { + // Ignorer le DataSet + } + + // Sinon, + else + { + jsonObject.append(KEY_TICFRAME, dataSet.toJSON(options)); + } + + } + + dictionary.addKey(KEY_TICFRAME); + dictionary.set(KEY_TICFRAME, jsonObject.get(KEY_TICFRAME)); + + return dictionary; + } + + /** + * Get mode + * + * @return mode + */ + public abstract TICMode getMode(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java new file mode 100644 index 0000000..ccea017 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java @@ -0,0 +1,313 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame; + +import java.util.Objects; + +import org.json.JSONObject; + +import enedis.lab.types.BytesArray; + +/** + * TIC frame data set + */ +public abstract class TICFrameDataSet +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Beginning pattern */ + public static final byte BEGINNING_PATTERN = 0x0A; // LF + /** End pattern */ + public static final byte END_PATTERN = 0x0D; // CR + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected BytesArray label = null; + protected BytesArray data = null; + protected byte checksum; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public TICFrameDataSet() + { + this.init(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Object + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public int hashCode() + { + return Objects.hash(this.checksum, this.data, this.label); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (this.getClass() != obj.getClass()) + return false; + TICFrameDataSet other = (TICFrameDataSet) obj; + return this.checksum == other.checksum && Objects.equals(this.data, other.data) && Objects.equals(this.label, other.label); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Setup from string label and data + * + * @param label + * @param data + */ + public void setup(String label, String data) + { + this.setup(label.getBytes(), data.getBytes()); + } + + /** + * Setup from byte[] label and data + * + * @param label + * @param data + */ + public void setup(byte[] label, byte[] data) + { + this.label = new BytesArray(label); + this.data = new BytesArray(data); + } + + /** + * Get label + * + * @return label + */ + public String getLabel() + { + return new String(this.label.getBytes()); + } + + /** + * Set the Label + * + * @param label + */ + public void setLabel(String label) + { + this.setLabel(label.getBytes()); + } + + /** + * Set the Label + * + * @param label + */ + public void setLabel(byte[] label) + { + this.setLabel(new BytesArray(label)); + } + + /** + * Set the Label + * + * @param label + */ + public void setLabel(BytesArray label) + { + this.label = label; + this.setChecksum(); + } + + /** + * Get data + * + * @return data + */ + public String getData() + { + return new String(this.data.getBytes()); + } + + /** + * Set data + * + * @param data + */ + public void setData(String data) + { + this.setData(data.getBytes()); + } + + /** + * Set data + * + * @param data + */ + public void setData(byte[] data) + { + this.setData(new BytesArray(data)); + } + + /** + * Set data + * + * @param data + */ + public void setData(BytesArray data) + { + this.data = data; + this.setChecksum(); + } + + /** + * Get checksum + * + * @return checksum + */ + public byte getChecksum() + { + + return this.checksum; + } + + /** + * Compute and set the checksum (NB: Label and Data shall have been set) + * + * @return true if the operation succeeds, else, return false + */ + public boolean setChecksum() + { + Byte checksum = this.getConsistentChecksum(); + + if (checksum != null) + { + this.checksum = checksum; + return true; + } + + else + { + return false; + } + } + + /** + * Set the checksum at a given value + * + * @param checksum + */ + public void setChecksum(byte checksum) + { + this.checksum = checksum; + + } + + /** + * + * @return true if fields are consistent with checksum + */ + public boolean isValid() + { + Byte consistentChecksum = this.getConsistentChecksum(); + + if ((consistentChecksum != null) && (consistentChecksum.byteValue() == (this.checksum & (byte) 0xFF))) + { + return true; + } + + else + { + return false; + } + } + + /** + * Get bytes + * + * @return bytes + */ + public abstract byte[] getBytes(); + + /** + * Conver to JSON + * + * @return json object + */ + public abstract JSONObject toJSON(); + + /** + * Conver to JSON + * + * @param option + * @return json object + */ + public abstract JSONObject toJSON(int option); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Return the consistent checksum of the DataSet + * + * @return the consistent checksum of the DataSet + */ + protected abstract Byte getConsistentChecksum(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private void init() + { + this.label = null; + this.data = null; + this.checksum = -1; + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java new file mode 100644 index 0000000..417b23b --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java @@ -0,0 +1,140 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame.historic; + +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.protocol.tic.frame.TICFrame; + +/** + * TIC frame historic + */ +public class TICFrameHistoric extends TICFrame +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Separator */ + public static final byte SEPARATOR = 0x20; // SP + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public TICFrameHistoric() + { + super(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICFrame + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public TICFrameHistoricDataSet addDataSet(String label, String data) + { + return this.addDataSet(this.DataSetList.size(), label, data); + } + + @Override + public TICFrameHistoricDataSet addDataSet(int index, String label, String data) + { + TICFrameHistoricDataSet dataSet = (TICFrameHistoricDataSet) this.getDataSet(label); + + if (dataSet == null) + { + dataSet = new TICFrameHistoricDataSet(); + dataSet.setLabel(label); + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + if ((index >= 0) && (index < this.DataSetList.size())) + { + this.DataSetList.add(index, dataSet); + } + + else + { + this.DataSetList.add(dataSet); + } + } + + else + { + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + this.DataSetList.remove(dataSet); + + if (index >= this.DataSetList.size()) + { + this.DataSetList.add(dataSet); + } + + else + { + this.DataSetList.add(index, dataSet); + } + } + + return dataSet; + } + + @Override + public TICMode getMode() + { + return TICMode.HISTORIC; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java new file mode 100644 index 0000000..8c4ef34 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java @@ -0,0 +1,190 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame.historic; + +import org.json.JSONObject; + +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.types.BytesArray; + +/** + * TIC frame historic data set + */ +public class TICFrameHistoricDataSet extends TICFrameDataSet +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Separator */ + public static final byte SEPARATOR = 0x20; // SP + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor + */ + public TICFrameHistoricDataSet() + { + super(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICFrameDataSet + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Byte getConsistentChecksum() + { + Byte crc = 0; + byte[] labelByte; + byte[] dataByte; + if ((this.label != null) && (this.data != null)) + { + labelByte = this.label.getBytes(); + for (int i = 0; i < labelByte.length; i++) + { + crc = this.computeUpdate(crc, labelByte[i]); + } + + crc = this.computeUpdate(crc, SEPARATOR); + dataByte = this.data.getBytes(); + + for (int i = 0; i < dataByte.length; i++) + { + crc = this.computeUpdate(crc, dataByte[i]); + } + + return this.computeEnd(crc); + } + else + { + return null; + } + + } + + /** + * compute Update + * + * @param crc + * @param octet + * @return Byte crc after compute update + */ + public Byte computeUpdate(Byte crc, byte octet) + { + return (byte) (crc + (octet & 0xff)); + } + + /** + * compute End + * + * @param crc + * @return Byte crc after compute end + */ + public Byte computeEnd(Byte crc) + { + return (byte) ((crc & 0x3F) + 0x20); + } + + @Override + public byte[] getBytes() + { + BytesArray dataSet = new BytesArray(); + + if ((this.label != null) && (this.data != null)) + { + dataSet.add(BEGINNING_PATTERN); + dataSet.addAll(this.label.getBytes()); + + dataSet.add(SEPARATOR); + dataSet.addAll(this.data.getBytes()); + + dataSet.add(SEPARATOR); + dataSet.add(this.checksum); + + dataSet.add(END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public JSONObject toJSON() + { + return this.toJSON(TICFrame.TRIMMED); + } + + @Override + public JSONObject toJSON(int option) + { + JSONObject jsonObject = new JSONObject(); + + if ((option & TICFrame.NOCHECKSUM) > 0) + { + jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); + } + + else + { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_CHECKSUM, this.checksum); + jsonObject.put(new String(this.label.getBytes()), content); + } + + return jsonObject; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java new file mode 100644 index 0000000..d50abe1 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java @@ -0,0 +1,73 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame.standard; + +import enedis.lab.protocol.tic.frame.TICError; + +/** + * TICException + * + */ +public class TICException extends Exception +{ + /** + * Unique identifier used for serialization + */ + private static final long serialVersionUID = -2780151361870269473L; + private TICError error; + + /** + * Constructor TICException + */ + public TICException() + { + super(); + this.reset(); + } + + /** + * Constructor TICException + * + * @param message + */ + public TICException(String message) + { + super(message); + this.error = TICError.TIC_READER_DEFAULT_ERROR; + } + + /** + * Constructor TICException + * + * @param message + * @param readerError + */ + public TICException(String message, TICError readerError) + { + super(message); + this.error = readerError; + } + + /** + * Reset TICError + */ + public void reset() + { + this.error = TICError.TIC_READER_DEFAULT_ERROR; + } + + /** + * Get error + * + * @return the readerError + */ + public TICError getError() + { + return this.error; + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java new file mode 100644 index 0000000..d23c26a --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java @@ -0,0 +1,247 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame.standard; + +import java.time.LocalDateTime; + +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.protocol.tic.frame.TICFrame; + +/** + * TIC frame standard + */ +public class TICFrameStandard extends TICFrame +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Begin info group */ + public static final byte INFOGROUP_BEGIN = 0x03; // LF + /** End info group */ + public static final byte INFOGROUP_END = 0x03; // CR + /** Sep info group */ + public static final byte INFOGROUP_SEP = 0x03; // HT + + /** Min size info group */ + public static final int INFOGROUP_MIN_SIZE = 7; // LF + Label + HT + Data + HT + + /** MIN_SIZE */ + public static final int MIN_SIZE = INFOGROUP_MIN_SIZE + 2; // ETX + INFGROUP_MIN_SIZE + STX + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public TICFrameStandard() + { + super(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICFrame + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public TICFrameStandardDataSet addDataSet(String label, String data) + { + TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); + + if (dataSet == null) + { + dataSet = new TICFrameStandardDataSet(); + dataSet.setLabel(label); + dataSet.setData(data); + dataSet.getConsistentChecksum(); + } + + else + { + dataSet.setData(data); + dataSet.getConsistentChecksum(); + } + + this.DataSetList.add(dataSet); + + return dataSet; + } + + @Override + public TICFrameStandardDataSet addDataSet(int index, String label, String data) + { + TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); + + if (dataSet == null) + { + dataSet = new TICFrameStandardDataSet(); + dataSet.setLabel(label); + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + if ((index >= 0) && (index < this.DataSetList.size())) + { + this.DataSetList.add(index, dataSet); + } + + else + { + this.DataSetList.add(dataSet); + } + } + + else + { + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + this.DataSetList.remove(dataSet); + + if (index >= this.DataSetList.size()) + { + this.DataSetList.add(dataSet); + } + + else + { + this.DataSetList.add(index, dataSet); + } + } + + return dataSet; + } + + @Override + public TICMode getMode() + { + return TICMode.STANDARD; + } + + @Override + public String getData(String label) + { + TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); + + if (dataSet != null) + { + if (dataSet.getLabel().equals("DATE")) + { + return dataSet.getDateTime(); + } + else + { + return dataSet.getData(); + } + } + + else + { + return null; + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Add data set + * + * @param label + * @param data + * @param dateTime + * @return the added data set + */ + public TICFrameStandardDataSet addDataSet(String label, String data, String dateTime) + { + TICFrameStandardDataSet dataSet = this.addDataSet(label, data); + + dataSet.setDateTime(dateTime); + + return dataSet; + } + + /** + * Get string datetime + * + * @param label + * @return string datetime + */ + public String getDateTime(String label) + { + TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); + if (dataSet != null) + { + return dataSet.getDateTime(); + } + else + { + return null; + } + } + + /** + * Get date time as localDateTime + * + * @param label + * @return LocalDateTime + */ + public LocalDateTime getDateTimeAsLocalDateTime(String label) + { + TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); + if (dataSet != null) + { + return dataSet.getDateTimeAsLocalDateTime(); + } + else + { + return null; + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java new file mode 100644 index 0000000..8ee0198 --- /dev/null +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java @@ -0,0 +1,352 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame.standard; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import org.json.JSONObject; + +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.types.BytesArray; + +/** + * TIC frame standard data set + */ +public class TICFrameStandardDataSet extends TICFrameDataSet +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Separator */ + public static final byte SEPARATOR = 0x09; // HT + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected BytesArray dateTime = null; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor + */ + public TICFrameStandardDataSet() + { + super(); + this.init(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICFrameDataSet + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Byte getConsistentChecksum() + { + Byte crc = 0; + byte[] labelByte; + byte[] dataByte; + byte[] dateByte; + + if ((this.label != null) && (this.data != null)) + { + labelByte = this.label.getBytes(); + for (int i = 0; i < labelByte.length; i++) + { + crc = this.computeUpdate(crc, labelByte[i]); + } + + crc = this.computeUpdate(crc, SEPARATOR); + + if (this.dateTime != null) + { + dateByte = this.dateTime.getBytes(); + for (int i = 0; i < dateByte.length; i++) + { + crc = this.computeUpdate(crc, dateByte[i]); + } + + crc = this.computeUpdate(crc, SEPARATOR); + } + + dataByte = this.data.getBytes(); + + for (int i = 0; i < dataByte.length; i++) + { + crc = this.computeUpdate(crc, dataByte[i]); + } + crc = this.computeUpdate(crc, SEPARATOR); + + return this.computeEnd(crc); + } + else + { + return null; + } + + } + + /** + * Compute update + * + * @param crc + * @param octet + * @return Byte crc compute after Update + */ + public Byte computeUpdate(Byte crc, byte octet) + { + return (byte) (crc + (octet & 0xff)); + } + + /** + * Compute end + * + * @param crc + * @return Byte crc compute after End + */ + public Byte computeEnd(Byte crc) + { + return (byte) ((crc & 0x3F) + 0x20); + } + + @Override + public byte[] getBytes() + { + BytesArray dataSet = new BytesArray(); + + if ((this.label != null) && (this.data != null)) + { + dataSet.add(BEGINNING_PATTERN); + dataSet.addAll(this.label.getBytes()); + + if (null != this.dateTime) + { + dataSet.add(SEPARATOR); + dataSet.addAll(this.dateTime.getBytes()); + } + + dataSet.add(SEPARATOR); + dataSet.addAll(this.data.getBytes()); + + dataSet.add(SEPARATOR); + dataSet.add(this.checksum); + + dataSet.add(END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public JSONObject toJSON() + { + return this.toJSON(TICFrame.TRIMMED); + } + + @Override + public JSONObject toJSON(int option) + { + JSONObject jsonObject = new JSONObject(); + + if ((option & TICFrame.NOCHECKSUM) > 0) + { + if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) + { + jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); + } + + else + { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); + jsonObject.put(new String(this.label.getBytes()), content); + } + } + + else + { + if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) + { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_CHECKSUM, this.checksum); + jsonObject.put(new String(this.label.getBytes()), content); + } + + else + { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); + content.put(TICFrame.KEY_CHECKSUM, this.checksum); + jsonObject.put(new String(this.label.getBytes()), content); + } + } + + return jsonObject; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Setup + * + * @param label + * @param data + * @param dateTime + */ + public void setup(String label, String data, String dateTime) + { + this.setup(label.getBytes(), data.getBytes(), dateTime.getBytes()); + } + + /** + * Setup + * + * @param label + * @param data + * @param dateTime + */ + public void setup(byte[] label, byte[] data, byte[] dateTime) + { + super.setup(label, data); + this.dateTime = new BytesArray(dateTime); + } + + /** + * Get datetime + * + * @return datetime + */ + public String getDateTime() + { + return new String(this.dateTime.getBytes()); + } + + /** + * Check datetime + * + * @return result + */ + public Boolean checkDateTime() + { + if (this.dateTime == null) + { + return false; + } + return true; + } + + /** + * Get datetime as local datetime + * + * @return datetime as local datetime + */ + // DATE H190730100158 yyMMddHHmmss + public LocalDateTime getDateTimeAsLocalDateTime() + { + String strDateTime = this.getDateTime(); + LocalDateTime localDateTime = null; + + if (strDateTime != null && strDateTime.length() == 13) + { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMddHHmmss"); + try + { + localDateTime = LocalDateTime.parse(strDateTime.substring(1), formatter); + } + catch (DateTimeParseException e) + { + localDateTime = null; + } + } + + return localDateTime; + } + + /** + * Set date time + * + * @param dateTime + */ + public void setDateTime(String dateTime) + { + this.setDateTime(dateTime.getBytes()); + } + + /** + * Set datetime + * + * @param dateTime + */ + public void setDateTime(byte[] dateTime) + { + this.setDateTime(new BytesArray(dateTime)); + } + + /** + * Set date time + * + * @param dateTime + */ + public void setDateTime(BytesArray dateTime) + { + this.dateTime = dateTime; + this.setChecksum(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void init() + { + this.dateTime = null; + } +} diff --git a/src/main/java/enedis/lab/types/BytesArray.java b/src/main/java/enedis/lab/types/BytesArray.java new file mode 100644 index 0000000..fb1cd50 --- /dev/null +++ b/src/main/java/enedis/lab/types/BytesArray.java @@ -0,0 +1,1280 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * Bytes array + */ +public class BytesArray implements List +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Default option */ + public static final int DEFAULT_OPTIONS = 0x0000; + /** Greedy option */ + public static final int GREEDY = 0x0001; + /** Contiguous option */ + public static final int CONTIGUOUS = 0x0002; + /** Remove patterns option */ + public static final int REMOVE_PATTERNS = 0x0004; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Convert int to byte[] + * + * @param value + * @param numberOfByte + * @return byte[] + * @throws NumberFormatException + */ + public static byte[] intToArrayOfByte(int value, int numberOfByte) throws NumberFormatException + { + if (countBytes(value) > numberOfByte) + { + throw new NumberFormatException("Value " + value + " can't be converted in a byte[" + numberOfByte + "]"); + } + + byte[] byteArray = new byte[numberOfByte]; + + for (int i = 0; i < byteArray.length; i++) + { + if (i < Integer.BYTES) + { + byteArray[byteArray.length - i - 1] = (byte) (value >> (i * 8)); + } + } + + return byteArray; + } + + /** + * Conver int to BytesArray + * + * @param value + * @param numberOfByte + * @return BytesArray + * @throws Exception + */ + public static BytesArray valueOf(int value, int numberOfByte) throws Exception + { + return new BytesArray(intToArrayOfByte(value, numberOfByte)); + } + + private static int countBytes(int value) + { + int tmp = Math.abs(value); + int count = 0; + + for (int i = 0; i < Integer.BYTES; i++) + { + tmp = tmp >> 8; + count++; + if (tmp == 0) + { + break; + } + } + return count; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected byte[] buffer; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public BytesArray() + { + this.buffer = new byte[0]; + } + + /** + * Constructor from array of byte + * + * @param bytesArray + */ + public BytesArray(byte[] bytesArray) + { + this.buffer = bytesArray.clone(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// List + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public int size() + { + return this.buffer.length; + } + + @Override + public boolean isEmpty() + { + return (0 == this.buffer.length) ? true : false; + } + + @Override + public boolean contains(Object element) + { + int index = this.indexOf(element); + return (index >= 0) ? true : false; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Iterator iterator() + { + throw new UnsupportedOperationException(); + } + + @Override + public Byte[] toArray() + { + Byte[] bytesArray = new Byte[this.buffer.length]; + + for (int i = 0; i < this.buffer.length; i++) + { + bytesArray[i] = this.buffer[i]; + } + + return bytesArray; + } + + @SuppressWarnings("unchecked") + @Override + public Byte[] toArray(Object[] a) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean add(Byte element) + { + byte byte_element = element; + byte[] bytesArray = new byte[this.buffer.length + 1]; + + System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); + bytesArray[bytesArray.length - 1] = byte_element; + this.buffer = bytesArray; + + return true; + } + + @Override + public void add(int index, Byte element) + { + if ((index < 0) || (index >= this.buffer.length)) + { + this.add(element); + } + + else + { + byte[] bytesArray = new byte[this.buffer.length + 1]; + + // Si l'index est le premier + if (0 == index) + { + System.arraycopy(this.buffer, 0, bytesArray, 1, this.buffer.length); + } + + // Sinon, si l'index est le dernier + else if ((this.buffer.length - 1) == index) + { + System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length - 1); + + bytesArray[bytesArray.length - 1] = this.buffer[this.buffer.length - 1]; + } + + // Sinon, si l'index est au milieu du tableau + else + { + System.arraycopy(this.buffer, 0, bytesArray, 0, index - 1); + + System.arraycopy(this.buffer, index, // src , src_pos + bytesArray, index + 1, // dest, dest_pos + this.buffer.length - index - 1); // size + } + + bytesArray[index] = element; + this.buffer = bytesArray; + } + } + + @Override + public boolean remove(Object element) + { + boolean success = true; + + if ((null != element) && (element instanceof Number)) + { + // Obtenir l'index de la première occurence de l'élément + int index = this.indexOf(element); + + // Si un tel élément a été trouvé, + if (index >= 0) + { + this.removeIndex(index); + } + + // Sinon, + else + { + success = false; + } + } + + else + { + success = false; + } + + return success; + } + + @Override + public Byte remove(int index) + { + Byte element = null; + + if ((index >= 0) && (index < this.buffer.length)) + { + element = new Byte(this.buffer[index]); + this.removeIndex(index); + } + + else + { + // Aucune action + } + + return element; + } + + /** + * Remove the buffer from an index to other + * + * @param fromIndex + * @param toIndex + * @return new length of buffer + */ + public int remove(int fromIndex, int toIndex) + { + int length = 0; + + if ((fromIndex >= 0) && (toIndex < this.buffer.length) && (toIndex > fromIndex)) + { + this.removeSubList(fromIndex, toIndex); + length = toIndex - fromIndex + 1; + } + + return length; + } + + @Override + public boolean containsAll(Collection c) + { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean addAll(Collection collection) + { + boolean hasChanged; + + if ((null != collection) && (collection.size() > 0)) + { + Object[] collectionArray = collection.toArray(); + byte[] bytesArray = new byte[this.buffer.length + collection.size()]; + + System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); + + int j = this.buffer.length; + for (int i = 0; i < collectionArray.length; i++) + { + bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); + j++; + } + + this.buffer = bytesArray; + + hasChanged = true; + } + + else + { + hasChanged = false; + } + + return hasChanged; + } + + @Override + public boolean addAll(int index, Collection collection) + { + boolean hasChanged; + + if ((null != collection) && (collection.size() > 0) && (index >= 0) && (index <= this.buffer.length)) + { + Object[] collectionArray = collection.toArray(); + byte[] bytesArray = new byte[this.buffer.length + collection.size()]; + + // Cas ou on ajoute en début: + if (0 == index) + { + for (int i = 0; i < collectionArray.length; i++) + { + bytesArray[i] = ((Byte) collectionArray[i]).byteValue(); + } + + System.arraycopy(this.buffer, 0, bytesArray, collectionArray.length, this.buffer.length); + } + + // Cas où on ajoute à la fin: + else if (this.buffer.length == index) + { + System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); + + int j = index; + for (int i = 0; i < collectionArray.length; i++) + { + bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); + j++; + } + } + + // Cas où on ajoute au milieu: + else + { + System.arraycopy(this.buffer, 0, bytesArray, 0, index); + + int j = index; + for (int i = 0; i < collectionArray.length; i++) + { + bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); + j++; + } + + System.arraycopy(this.buffer, index, bytesArray, index + collectionArray.length, this.buffer.length - index); + } + + this.buffer = bytesArray; + + hasChanged = true; + } + + else + { + hasChanged = false; + } + + return hasChanged; + } + + @Override + public boolean removeAll(Collection c) + { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean retainAll(Collection c) + { + // TODO Auto-generated method stub + return false; + } + + @Override + public void clear() + { + this.buffer = new byte[0]; + } + + @Override + public Byte get(int index) + { + Byte element = null; + + if ((index >= 0) && (index < this.buffer.length)) + { + element = new Byte(this.buffer[index]); + } + + else + { + + } + + return element; + } + + @Override + public Byte set(int index, Byte element) + { + Byte previous_element = null; + + if ((index >= 0) && (index < this.buffer.length)) + { + previous_element = new Byte(this.buffer[index]); + this.buffer[index] = element; + } + + else + { + previous_element = new Byte(this.buffer[this.buffer.length - 1]); + this.buffer[this.buffer.length - 1] = element; + } + + return new Byte(previous_element); + } + + @Override + public int indexOf(Object element) + { + return this.indexOf(element, 0); + } + + /** + * Get the buffer element of the given index + * + * @param element + * @param offset + * @return the element + */ + public int indexOf(Object element, int offset) + { + int index = -1; + + if ((null != element) && (element instanceof Number) && offset >= 0 && offset < this.buffer.length) + { + Byte byte_element = ((Number) element).byteValue(); + + for (int i = offset; i < this.buffer.length; i++) + { + if (this.buffer[i] == byte_element) + { + index = i; + break; + } + } + } + + else + { + // Aucune action + } + + return index; + } + + @Override + public int lastIndexOf(Object element) + { + int index = -1; + + if ((null != element) && (element instanceof Number)) + { + Byte byte_element = ((Number) element).byteValue(); + + for (int i = this.buffer.length - 1; i >= 0; i--) + { + if (this.buffer[i] == byte_element) + { + index = i; + break; + } + } + } + + else + { + // Aucune action + } + + return index; + } + + @Override + public ListIterator listIterator() + { + // TODO Auto-generated method stub + return null; + } + + @Override + public ListIterator listIterator(int index) + { + // TODO Auto-generated method stub + return null; + } + + @Override + public BytesArray subList(int fromIndex, int toIndex) + { + BytesArray subBytesArray = new BytesArray(new byte[0]); + + if (fromIndex >= 0) + { + if ((toIndex > fromIndex) && (toIndex < this.buffer.length)) + { + int size = (toIndex - fromIndex) + 1; + byte[] buffer = new byte[size]; + + System.arraycopy(this.buffer, fromIndex, buffer, 0, size); + + subBytesArray = new BytesArray(buffer); + } + + else if ((toIndex < 0) || (toIndex >= this.buffer.length)) + { + int size = this.buffer.length - fromIndex; + byte[] buffer = new byte[size]; + + System.arraycopy(this.buffer, fromIndex, buffer, 0, size); + + subBytesArray = new BytesArray(buffer); + } + + else + { + + } + } + + return subBytesArray; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get bytes + * + * @return bytes + */ + public byte[] getBytes() + { + return this.buffer; + } + + /** + * Copy + * + * @return copied BytesArray + */ + public BytesArray copy() + { + return new BytesArray(this.buffer); + } + + /** + * Get index of element + * + * @param element + * @return index of element + */ + public List indexesOf(Object element) + { + List indexesList = new ArrayList(); + + if ((null != element) && (element instanceof Number)) + { + Byte byte_element = ((Number) element).byteValue(); + + for (int i = 0; i < this.buffer.length; i++) + { + if (this.buffer[i] == byte_element) + { + indexesList.add(i); + } + } + } + + else + { + // Aucune action + } + + return indexesList; + } + + /** + * + * @param value + * @return true if the BytesArray starts with the given value, else return false + */ + public boolean startsWith(byte value) + { + boolean retVal; + + if ((this.buffer.length > 0) && (this.buffer[0] == value)) + { + retVal = true; + } + + else + { + retVal = false; + } + + return retVal; + } + + /** + * + * @param value + * @return true if the BytesArray ends with the given value, else return false + */ + public boolean endsWith(byte value) + { + boolean retVal; + + if ((this.buffer.length > 0) && (this.buffer[this.buffer.length - 1] == value)) + { + retVal = true; + } + + else + { + retVal = false; + } + + return retVal; + } + + /** + * Get element count + * + * @param element + * @return element count + */ + public int count(Object element) + { + return this.indexesOf(element).size(); + } + + /** + * Split + * + * @param pattern + * @return split BytesArray + */ + public List split(Number pattern) + { + List patternsIndexesList = this.indexesOf(pattern.byteValue()); + List bytesArraysList = new ArrayList(); + + // Si le motif de séparation existe, copier chaque segment dans l'élément associé du tableau + if (false == patternsIndexesList.isEmpty()) + { + int begin_index = 0; + int end_index; + int segment_length; + int nb_segments = patternsIndexesList.size() + 1; + + for (int i = 0; i < nb_segments; i++) + { + end_index = (i < patternsIndexesList.size()) ? patternsIndexesList.get(i) : this.buffer.length; + + segment_length = end_index - begin_index; + + // Si la taille du segment est non nulle, + if (segment_length > 0) + { + byte[] segment = new byte[segment_length]; + + System.arraycopy(this.buffer, begin_index, segment, 0, segment_length); + + bytesArraysList.add(new BytesArray(segment)); + } + + // Sinon, + else + { + bytesArraysList.add(new BytesArray()); + } + + begin_index = end_index + 1; + } + } + + // Sinon, copier dans l'unique élément du tableau l'intégralité des données + else + { + bytesArraysList.add(new BytesArray(this.buffer)); + } + + return bytesArraysList; + } + + /** + * Extract parts in the ByteArray according specific 'begin' and 'end' patterns.
+ * NB : 'begin' and 'end' patterns shall be different. Use split() method if they don't.
+ *
+ * Example:
+ *
+ * BytesArray myArray(new byte[12] {42,14,20,98,34,47,14,61,17,98,110,25});
+ * List mySlicedArray = myArray.slice(14,98);
+ * mySlicedArray.get(0) contains {14,20,98}
+ * mySlicedArray.get(1) contains {14,61,17,98}
+ * + * @param beginPattern + * @param endPattern + * @return slices list + */ + public List slice(Number beginPattern, Number endPattern) + { + return this.slice(beginPattern, endPattern, -1, 0); + } + + /** + * + * @param beginPattern + * @param endPattern + * @param options + * @return slices list + */ + public List slice(Number beginPattern, Number endPattern, int options) + { + return this.slice(beginPattern, endPattern, -1, options); + } + + /** + * + * @param beginPattern + * @param endPattern + * @param occurences + * @param options + * @return slices list + */ + public List slice(Number beginPattern, Number endPattern, int occurences, int options) + { + List bytesArraysList; + + // ... Si l'appel de la fonction est "gourmand" (greedy), il n'y aura au mieux qu'un élément à retourner qui + // correspondra au plus grand segment [begin_pattern, end_pattern] du tableau + // NB le paramètre 'occurences' est alors non signifiant + if ((options & GREEDY) != 0) + { + bytesArraysList = this.sliceGreedy(beginPattern, endPattern, options); + } + + // ... Si l'appel de la fonction est "non gourmand" (not greedy), les éléments du tableau à retourner + // correspondront aux segments [begin_pattern, end_pattern] + // les plus rationnels, dans la limite des n premières occurences demandées + else + { + bytesArraysList = this.sliceNotGreedy(beginPattern, endPattern, occurences, options); + } + + return bytesArraysList; + } + + /** + * Add all byte + * + * @param bytesArray + * @return true if changed has been applied + */ + public boolean addAll(byte[] bytesArray) + { + boolean hasChanged = false; + + if (bytesArray != null && bytesArray.length > 0) + { + byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; + + // copie des données existantes + System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); + + // copie des nouvelles données + System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); + + this.buffer = newBytesArray; + hasChanged = true; + } + + else + { + hasChanged = false; + } + + return hasChanged; + } + + /** + * Add all byte at index + * + * @param index + * @param bytesArray + * @return true if changed has been applied + */ + public boolean addAll(int index, byte[] bytesArray) + { + boolean hasChanged = false; + + if ((index >= 0) && (index <= this.buffer.length)) + { + byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; + + // Cas ou on ajoute au début: + if (0 == index) + { + System.arraycopy(bytesArray, 0, newBytesArray, 0, bytesArray.length); + + // copie des données existantes + System.arraycopy(this.buffer, 0, newBytesArray, bytesArray.length, this.buffer.length); + } + + // Cas où on ajoute à la fin: + else if (this.buffer.length == index) + { + // copie des données existantes + System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); + + // copie des nouvelles données + System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); + } + + // Cas où on ajoute au milieu: + else + { + // copie des données existantes (premier segment) + System.arraycopy(this.buffer, 0, newBytesArray, 0, index); + + // copie des nouvelles données + System.arraycopy(bytesArray, 0, newBytesArray, index, bytesArray.length); + + // copie des données existantes (deuxième segment) + System.arraycopy(this.buffer, index, newBytesArray, index + bytesArray.length, this.buffer.length - index); + } + + this.buffer = newBytesArray; + hasChanged = true; + } + + else + { + hasChanged = false; + } + + return hasChanged; + } + + /** + * Compute the BytesArray checksum on 8bits (Byte) + * + * @return the computed checksum or 'null' if the BytesArray is empty + * + */ + public Byte checksum8() + { + Byte checksum = null; + + if (this.buffer.length > 0) + { + byte checksum_8 = 0; + + for (int i = 0; i < this.buffer.length; i++) + { + checksum_8 += this.buffer[i]; + } + + checksum = new Byte(checksum_8); + } + + else + { + + } + + return checksum; + } + + /** + * Compute the BytesArray checksum on 16 bits (Short) + * + * @return the computed checksum or 'null' if the BytesArray is empty + * + */ + public Short checksum16() + { + Short checksum = null; + + if (this.buffer.length > 0) + { + short checksum_16 = 0; + + for (int i = 0; i < this.buffer.length; i++) + { + checksum_16 += this.buffer[i]; + } + + checksum = new Short(checksum_16); + } + + else + { + + } + + return checksum; + } + + /** + * Compute the BytesArray checksum on 32 bits (Integer) + * + * @return the computed checksum or 'null' if the BytesArray is empty + * + */ + public Integer checksum32() + { + Integer checksum = null; + + if (this.buffer.length > 0) + { + int checksum_32 = 0; + + for (int i = 0; i < this.buffer.length; i++) + { + checksum_32 += this.buffer[i]; + } + + checksum = new Integer(checksum_32); + } + + else + { + + } + + return checksum; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * + * @param beginPattern + * @param endPattern + * @param options + * @return + */ + private List sliceGreedy(Number beginPattern, Number endPattern, int options) + { + List bytesArraysList = new ArrayList(); + + int beginIndex = this.indexOf(beginPattern.byteValue()); + + if (beginIndex >= 0) + { + int endIndex = this.lastIndexOf(endPattern.byteValue()); + + if (endIndex > 0) + { + if ((options & REMOVE_PATTERNS) != 0) + { + beginIndex++; + endIndex--; + } + + int size = endIndex - beginIndex + 1; + + if (size > 0) + { + byte[] bytesBuffer = new byte[size]; + + System.arraycopy(this.buffer, beginIndex, bytesBuffer, 0, size); + + bytesArraysList.add(new BytesArray(bytesBuffer)); + } + + else + { + bytesArraysList.add(new BytesArray(new byte[0])); + } + } + } + + else + { + + } + + return bytesArraysList; + } + + /** + * + * @param beginPattern + * @param endPattern + * @param occurences + * @param options + * @return + */ + private List sliceNotGreedy(Number beginPattern, Number endPattern, int occurences, int options) + { + List bytesArraysList = new ArrayList(); + + // Si la contiguité des segments est requise, + if ((options & CONTIGUOUS) != 0) + { + bytesArraysList = this.sliceContiguous(beginPattern, endPattern, occurences, options); + } + + else + { + bytesArraysList = this.sliceNotContiguous(beginPattern, endPattern, occurences, options); + } + + return bytesArraysList; + } + + /** + * + * @param beginPattern + * @param endPattern + * @param occurences + * @param options + * @return + */ + private List sliceContiguous(Number beginPattern, Number endPattern, int occurences, int options) + { + List bytesArraysList = new ArrayList(); + + int beginIndex = -1; + int endIndex = -1; + + for (int i = 0; i < this.buffer.length; i++) + { + // Traiter un motif DEBUT + if (this.buffer[i] == beginPattern.byteValue()) + { + if ((-1 == endIndex) || (this.buffer[i - 1] == endPattern.byteValue())) + { + beginIndex = i; + } + } + + // Traiter un motif FIN + else if (this.buffer[i] == endPattern.byteValue()) + { + // Si nous ne sommes pas au dernier élément du tableau + if (i < (this.buffer.length - 1)) + { + // Si le prochain caractère est le motif de début de trame + if (this.buffer[i + 1] == beginPattern.byteValue()) + { + endIndex = i; + + this.slicePart(bytesArraysList, beginIndex, endIndex, options); + + // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, + // interrompre le traitement + if ((occurences > 0) && (bytesArraysList.size() >= occurences)) + { + break; + } + } + } + + // Sinon, si nous sommes au dernier élément du tableau + else + { + endIndex = i; + + this.slicePart(bytesArraysList, beginIndex, endIndex, options); + + // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, + // interrompre le traitement + if ((occurences > 0) && (bytesArraysList.size() >= occurences)) + { + break; + } + } + } + + else + { + + } + } + + return bytesArraysList; + } + + /** + * + * @param beginPattern + * @param endPattern + * @param occurences + * @param options + * @return + */ + private List sliceNotContiguous(Number beginPattern, Number endPattern, int occurences, int options) + { + List bytesArraysList = new ArrayList(); + + int beginIndex = -1; + int endIndex = -1; + + for (int i = 0; i < this.buffer.length; i++) + { + if (this.buffer[i] == beginPattern.byteValue()) + { + beginIndex = i; + } + + else if (this.buffer[i] == endPattern.byteValue()) + { + endIndex = i; + + this.slicePart(bytesArraysList, beginIndex, endIndex, options); + + // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, + // interrompre le traitement + if ((occurences > 0) && (bytesArraysList.size() >= occurences)) + { + break; + } + } + + else + { + + } + } + + return bytesArraysList; + } + + /** + * + * @param bytesArraysList + * @param beginIndex + * @param endIndex + * @param options + */ + private void slicePart(List bytesArraysList, int beginIndex, int endIndex, int options) + { + if (beginIndex >= 0) + { + if ((options & REMOVE_PATTERNS) != 0) + { + beginIndex++; + endIndex--; + } + + BytesArray part = this.subList(beginIndex, endIndex); + + if (null != part) + { + bytesArraysList.add(part); + } + } + } + + /** + * @param index + */ + private void removeIndex(int index) + { + byte[] bytesArray = new byte[this.buffer.length - 1]; + + // Si l'index est le premier du tableau + if (0 == index) + { + System.arraycopy(this.buffer, 1, bytesArray, 0, bytesArray.length); + } + + // Sinon, si l'index est le dernier + else if ((this.buffer.length - 1) == index) + { + System.arraycopy(this.buffer, 0, bytesArray, 0, bytesArray.length); + } + + // Sinon, si l'index est au milieu du tableau + else + { + System.arraycopy(this.buffer, 0, bytesArray, 0, index); + + System.arraycopy(this.buffer, index + 1, // src , src_pos + bytesArray, index, // dest, dest_pos + bytesArray.length - index); // size + } + + this.buffer = bytesArray; + } + + private void removeSubList(int fromIndex, int toIndex) + { + byte[] bytesArray = new byte[this.buffer.length - (toIndex - fromIndex + 1)]; + + if (fromIndex > 0) + { + System.arraycopy(this.buffer, 0, bytesArray, 0, fromIndex); + } + if (toIndex + 1 < this.buffer.length) + { + System.arraycopy(this.buffer, toIndex + 1, bytesArray, fromIndex, bytesArray.length - fromIndex); + } + + this.buffer = bytesArray; + } +} diff --git a/src/main/java/enedis/lab/types/DataArrayList.java b/src/main/java/enedis/lab/types/DataArrayList.java new file mode 100644 index 0000000..a6b267c --- /dev/null +++ b/src/main/java/enedis/lab/types/DataArrayList.java @@ -0,0 +1,392 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.RandomAccess; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Resizable-array implementation of the {@code DataList} interface. + * + * @param the type of elements in this data list + */ +public class DataArrayList extends ArrayList implements DataList +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = 3116080161929203526L; + + private static final int JSON_INDENT_FACTOR = 0; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Returns a fixed-size data list backed by the specified array. (Changes to + * the returned data list "write through" to the array.) This method acts + * as bridge between array-based and collection-based APIs, in + * combination with {@link Collection#toArray}. The returned list is + * serializable and implements {@link RandomAccess}. + * + *

This method also provides a convenient way to create a fixed-size + * data list initialized to contain several elements: + *

+     *     DataList<String> stooges = DataArrayList.asList("Larry", "Moe", "Curly");
+     * 
+ * + * @param the class of the objects in the array + * @param items the array by which the data list will be backed + * @return a data list view of the specified array + */ + @SafeVarargs + public static DataList asList(E... items) + { + DataList dataList = new DataArrayList(); + + for(E item : items) + { + dataList.add(item); + } + + return dataList; + } + + /** + * Instantiate a data list from a File + * + * @param the class of the objects in the list + * @param file + * @param clazz + * + * @return a data list + * @throws JSONException + * @throws IOException + */ + public static DataList fromFile(File file,Class clazz) throws JSONException, IOException + { + InputStream stream = new FileInputStream(file); + + return fromStream(stream,clazz); + } + + /** + * Instantiate a data list from a Stream + * + * @param the class of the objects in the list + * @param stream + * @param clazz + * + * @return a data list + * @throws JSONException + * @throws IOException + */ + public static DataList fromStream(InputStream stream,Class clazz) throws JSONException, IOException + { + byte[] buffer = new byte[stream.available()]; + stream.read(buffer); + stream.close(); + String text = new String(buffer); + + return fromString(text,clazz); + } + + /** + * Instantiate a data list from a String + * + * @param the class of the objects in the list + * @param text + * @param clazz + * + * @return a data list + * @throws JSONException + */ + public static DataList fromString(String text,Class clazz) throws JSONException + { + if (text == null) + { + return null; + } + + JSONArray jsonArray = new JSONArray(text); + + return fromJSON(jsonArray,clazz); + } + + /** + * Instantiate a data list from a JSONObject + * + * @param the class of the objects in the list + * @param jsonArray + * @param clazz + * + * @return a data list + * @throws JSONException + */ + public static DataList fromJSON(JSONArray jsonArray, Class clazz) throws JSONException + { + if(jsonArray == null) + { + return null; + } + DataList dataList = new DataArrayList(); + for(int i = 0; i < jsonArray.length(); i++) + { + Object jsonItem = jsonArray.get(i); + E dataListItem; + try + { + if(JSONObject.NULL.equals(jsonItem)) + { + dataListItem = null; + } + else if(clazz.isAssignableFrom(jsonItem.getClass())) + { + dataListItem = clazz.cast(jsonItem); + } + else if(Enum.class.isAssignableFrom(clazz)) + { + dataListItem = convertToEnum(jsonItem,clazz); + } + else if(LocalDateTime.class.isAssignableFrom(clazz)) + { + dataListItem = convertToLocalDateTime(jsonItem,clazz); + } + else if(List.class.isAssignableFrom(clazz)) + { + dataListItem = convertToDataList(jsonItem,clazz); + } + else if(DataDictionary.class.isAssignableFrom(clazz)) + { + dataListItem = convertToDataDictionary(jsonItem,clazz); + } + else + { + throw new IllegalArgumentException("Cannot convert " + jsonItem.getClass().getName() + " to " + clazz.getName()); + } + } + catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) + { + throw new IllegalArgumentException("Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getMessage() + ")",e); + } + catch (InvocationTargetException e) + { + throw new IllegalArgumentException("Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getTargetException().getMessage() + ")",e); + } + dataList.add(dataListItem); + } + + return dataList; + } + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructs an empty data list with an initial capacity of ten. + */ + public DataArrayList() + { + super(); + } + + /** + * Constructs a data list containing the elements of the specified + * collection, in the order they are returned by the collection's + * iterator. + * + * @param collection the collection whose elements are to be placed into this data list + * @throws NullPointerException if the specified collection is null + */ + public DataArrayList(Collection collection) + { + super(collection); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public JSONArray toJSON() + { + JSONArray jsonArray = new JSONArray(); + + for(E item : this) + { + Object objectItem; + if(item instanceof DataDictionary) + { + DataDictionary dictionaryItem = (DataDictionary)item; + objectItem = dictionaryItem.toJSON(); + } + else + { + objectItem = item; + } + jsonArray.put(objectItem); + } + + return jsonArray; + } + + @Override + public void toFile(File file, int indentFactor) throws IOException + { + OutputStream stream = new FileOutputStream(file); + + this.toStream(stream, indentFactor); + } + + @Override + public void toStream(OutputStream stream, int indentFactor) throws IOException + { + String text = this.toString(indentFactor); + + stream.write(text.getBytes()); + } + + @Override + public String toString() + { + return this.toString(JSON_INDENT_FACTOR); + } + + @Override + public String toString(int identFactor) + { + return this.toJSON().toString(identFactor); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("unchecked") + private static E convertToDataDictionary(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + E result; + + if(value instanceof JSONObject) + { + Method method = clazz.getMethod("fromJSON",JSONObject.class,Class.class); + result = (E) method.invoke(null,value,clazz); + } + else + { + throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } + + @SuppressWarnings("unchecked") + private static E convertToDataList(Object value,Class clazz) + { + E result; + + if(value instanceof JSONArray) + { + result = (E) DataArrayList.fromJSON((JSONArray)value, Object.class); + } + else + { + throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } + + @SuppressWarnings("unchecked") + private static E convertToEnum(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + E result; + + if(value instanceof String) + { + Method method = clazz.getDeclaredMethod("valueOf", String.class); + result = (E) method.invoke(null, (String)value); + } + else + { + throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } + + @SuppressWarnings("unchecked") + private static E convertToLocalDateTime(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + E result; + + if(value instanceof String) + { + Method method = clazz.getDeclaredMethod("parse", CharSequence.class); + result = (E) method.invoke(null, (String)value); + } + else + { + throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } +} diff --git a/src/main/java/enedis/lab/types/DataDictionary.java b/src/main/java/enedis/lab/types/DataDictionary.java new file mode 100644 index 0000000..56f3bfe --- /dev/null +++ b/src/main/java/enedis/lab/types/DataDictionary.java @@ -0,0 +1,146 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +import org.json.JSONObject; + +/** + * Interface used to handle data dictionaries + */ +public interface DataDictionary extends Cloneable +{ + /** + * Check key exists + * + * @param key + * Key to check + * + * @return true if key exists, false otherwise + */ + public boolean exists(String key); + + /** + * Get the datadictionary current map keys list + * + * @return the datadictionary current map keys list + */ + public String[] keys(); + + /** + * Check if the datadictionary map contains the given key + * + * @param key + * @return true if the datadictionary map contains the given key + */ + public boolean existsInKeys(String key); + + /** + * Add the given key in the datadictionary map, if the given key already exists, do nothing + * + * @param key + */ + public void addKey(String key); + + /** + * Remove the given key in the datadictionary map + * + * @param key + */ + public void removeKey(String key); + + /** + * Clear the datadictionary map + */ + public void clear(); + + /** + * Get value of the given key + * + * @param key + * @return value of the given key + */ + public Object get(String key); + + /** + * Set value of the given key + * + * @param key + * @param value + * @throws DataDictionaryException + */ + public void set(String key, Object value) throws DataDictionaryException; + + /** + * Copy an other datadictionary into this one + * + * @param other + * @throws DataDictionaryException + */ + public void copy(DataDictionary other) throws DataDictionaryException; + + /** + * Convert this dataditionary to JSON + * + * @return a JSONObject + */ + public JSONObject toJSON(); + + /** + * Convert this dataditionary to Map + * + * @return a JSONObject + */ + public Map toMap(); + + /** + * Convert the given datadictionary to a File + * + * @param file + * @param indentFactor + * @throws IOException + */ + public void toFile(File file, int indentFactor) throws IOException; + + /** + * Convert the given datadictionary to a Stream + * + * @param stream + * @param indentFactor + * @throws IOException + */ + public void toStream(OutputStream stream, int indentFactor) throws IOException; + + @Override + public String toString(); + + /** + * Convert this dataditionary to a String + * + * @param indentFactor + * @return a JSONObject + */ + public String toString(int indentFactor); + + /** + * Convert this dataditionary to Map + * + * @return a JSONObject + * @throws CloneNotSupportedException + */ + public Object clone() throws CloneNotSupportedException; + + /** + * Print this datactionary in the console + */ + public void print(); +} diff --git a/src/main/java/enedis/lab/types/DataDictionaryException.java b/src/main/java/enedis/lab/types/DataDictionaryException.java new file mode 100644 index 0000000..0d2f088 --- /dev/null +++ b/src/main/java/enedis/lab/types/DataDictionaryException.java @@ -0,0 +1,125 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +/** + * Data dictionary exception + */ +public class DataDictionaryException extends Exception +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -7967428428453584771L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public DataDictionaryException() + { + super(); + } + + /** + * Constructor using message, cause, enable suppression flag and writable stack trace flag + * + * @param message + * @param cause + * @param enableSuppression + * @param writableStackTrace + */ + public DataDictionaryException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) + { + super(message, cause, enableSuppression, writableStackTrace); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public DataDictionaryException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public DataDictionaryException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public DataDictionaryException(Throwable cause) + { + super(cause); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/types/DataList.java b/src/main/java/enedis/lab/types/DataList.java new file mode 100644 index 0000000..12eed5f --- /dev/null +++ b/src/main/java/enedis/lab/types/DataList.java @@ -0,0 +1,57 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +import org.json.JSONArray; +/** + * Interface extending Java standard List to provide JSON serialization mechanism + * + * @param the type of elements in this list + */ +public interface DataList extends List +{ + /** + * Write this data list to a File + * + * @param file the output file + * @param indentFactor JSON indentation factor + * @throws IOException if writing file fails + */ + public void toFile(File file, int indentFactor) throws IOException; + + /** + * Write this data list to a Stream + * + * @param stream the output stream + * @param indentFactor JSON indentation factor + * @throws IOException if writing stream fails + */ + public void toStream(OutputStream stream, int indentFactor) throws IOException; + + @Override + public String toString(); + /** + * Convert this data list to a String + * + * @param indentFactor JSON indentation factor + * @return JSON string representation + */ + public String toString(int indentFactor); + + /** + * Convert this data list to JSON array + * + * @return JSON array + */ + public JSONArray toJSON(); +} diff --git a/src/main/java/enedis/lab/types/ExceptionBase.java b/src/main/java/enedis/lab/types/ExceptionBase.java new file mode 100644 index 0000000..99e3e7f --- /dev/null +++ b/src/main/java/enedis/lab/types/ExceptionBase.java @@ -0,0 +1,132 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +/** + * Exception base + */ +@SuppressWarnings("serial") +public class ExceptionBase extends Exception +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected int code; + protected String info; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor + * + * @param code + * @param info + */ + public ExceptionBase(int code, String info) + { + this.setErrorCode(code); + this.setErrorInfo(info); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Exception + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getMessage() + { + return this.info + " (" + this.code + ")"; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get error code + * + * @return error code + */ + public int getErrorCode() + { + return this.code; + } + + /** + * Set error code + * + * @param errorCode + */ + public void setErrorCode(int errorCode) + { + this.code = errorCode; + } + + /** + * Get error info + * + * @return error info + */ + public String getErrorInfo() + { + return this.info; + } + + /** + * Set error info + * + * @param errorInfo + */ + public void setErrorInfo(String errorInfo) + { + this.info = errorInfo; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/types/configuration/Configuration.java b/src/main/java/enedis/lab/types/configuration/Configuration.java new file mode 100644 index 0000000..46939e9 --- /dev/null +++ b/src/main/java/enedis/lab/types/configuration/Configuration.java @@ -0,0 +1,104 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.configuration; + +import java.io.File; + +import enedis.lab.types.DataDictionary; + +/** + * Interface used to handle any configuration + */ +public interface Configuration extends DataDictionary +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Load configuration from file + * + * @throws ConfigurationException + * load file failed + */ + public void load() throws ConfigurationException; + + /** + * Save configuration to file + * + * @throws ConfigurationException + * save file failed + */ + public void save() throws ConfigurationException; + + /** + * Get config name + * + * @return configuration name + */ + public String getConfigName(); + + /** + * Get config file + * + * @return configuration file reference + */ + public File getConfigFile(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java b/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java new file mode 100644 index 0000000..be58331 --- /dev/null +++ b/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java @@ -0,0 +1,186 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.configuration; + +import java.io.File; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; + +/** + * Basic implementation of Configuration + */ +public class ConfigurationBase extends DataDictionaryBase implements Configuration +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private String name = null; + private File file = null; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected ConfigurationBase() + { + super(); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ConfigurationBase(DataDictionary other) throws DataDictionaryException + { + super(other); + } + + /** + * Constructor using Map + * + * @param map + * @throws DataDictionaryException + */ + public ConfigurationBase(Map map) throws DataDictionaryException + { + super(map); + } + + /** + * Constructor using name and file + * + * @param name + * @param file + */ + public ConfigurationBase(String name, File file) + { + super(); + this.init(name, file); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Configuration + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void load() throws ConfigurationException + { + DataDictionary configuration; + + try + { + configuration = DataDictionaryBase.fromFile(this.file, this.getClass()); + } + catch (Exception exception) + { + throw new ConfigurationException("Cannot load configuration '" + ((this.name == null) ? "" : this.name) + "' from file '" + ((this.file == null) ? "" : this.file) + + "' (" + exception.getMessage() + ") " + exception.getClass().getSimpleName()); + } + try + { + this.copy(configuration); + } + catch (Exception exception) + { + throw new ConfigurationException("Cannot copy configuration '" + ((this.name == null) ? "" : this.name) + "' from file '" + ((this.file == null) ? "" : this.file) + + "' (" + exception.getMessage() + ")"); + } + } + + @Override + public void save() throws ConfigurationException + { + try + { + this.toFile(this.file, 2); + } + catch (Exception exception) + { + throw new ConfigurationException("Cannot save configuration '" + ((this.name == null) ? "" : this.name) + "' to file '" + ((this.file == null) ? "" : this.file) + "' (" + + exception.getMessage() + ")"); + } + } + + @Override + public String getConfigName() + { + return this.name; + } + + @Override + public File getConfigFile() + { + return this.file; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set config name + * + * @param configName + */ + public void setConfigName(String configName) + { + this.name = configName; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void init(String name, File file) + { + this.name = name; + this.file = file; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java b/src/main/java/enedis/lab/types/configuration/ConfigurationException.java new file mode 100644 index 0000000..3e435e2 --- /dev/null +++ b/src/main/java/enedis/lab/types/configuration/ConfigurationException.java @@ -0,0 +1,172 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.configuration; + +import enedis.lab.types.DataDictionaryException; + +/** + * Configuration exception + */ +public class ConfigurationException extends DataDictionaryException +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = 8090072693974075297L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Create missing parameter exception + * + * @param parameter + * @return configurationException + */ + public static ConfigurationException createMissingParameterException(String parameter) + { + return new ConfigurationException("Parameter \'" + parameter + "\' is missing"); + } + + /** + * Create unknown parameter exception + * + * @param parameter + * @return configurationException + */ + public static ConfigurationException createUnknownParameterException(String parameter) + { + return new ConfigurationException("Parameter \'" + parameter + "\' is unknown"); + } + + /** + * Create invalid parameter value exception + * + * @param parameter + * @param info + * @return configurationException + */ + public static ConfigurationException createInvalidParameterValueException(String parameter, String info) + { + return new ConfigurationException("Parameter \'" + parameter + "\' has an invalid value : " + info); + } + + /** + * Create invalid paramter type exception + * + * @param parameter + * @param type + * @return configurationException + */ + public static ConfigurationException createInvalidParameterTypeException(String parameter, Class type) + { + return new ConfigurationException("Parameter \'" + parameter + "\' has an invalid type : " + type.getName()); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public ConfigurationException() + { + super(); + } + + /** + * Constructor using message, cause, enable suppression flag and writable stack trace flag + * + * @param message + * @param cause + * @param enableSuppression + * @param writableStackTrace + */ + public ConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) + { + super(message, cause, enableSuppression, writableStackTrace); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public ConfigurationException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public ConfigurationException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public ConfigurationException(Throwable cause) + { + super(cause); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java b/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java new file mode 100644 index 0000000..02ca337 --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java @@ -0,0 +1,723 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; + +/** + * DataDictionary basic implementation + */ +public class DataDictionaryBase implements DataDictionary +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final int JSON_INDENT_FACTOR = 0; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Instantiate a datadictionary from a File + * + * @param file + * @param clazz + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + * @throws IOException + */ + public static DataDictionary fromFile(File file, Class clazz) throws JSONException, DataDictionaryException, IOException + { + if (file == null) + { + throw new DataDictionaryException("Can't load file from null"); + } + + InputStream stream = new FileInputStream(file); + + return DataDictionaryBase.fromStream(stream, clazz); + } + + /** + * Instantiate a datadictionary from a Stream + * + * @param stream + * @param clazz + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + * @throws IOException + */ + public static DataDictionary fromStream(InputStream stream, Class clazz) throws JSONException, DataDictionaryException, IOException + { + if (stream == null) + { + return null; + } + + byte[] buffer = new byte[stream.available()]; + stream.read(buffer); + stream.close(); + String text = new String(buffer); + + return DataDictionaryBase.fromString(text, clazz); + } + + /** + * Instantiate a datadictionary from a String + * + * @param text + * @param clazz + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromString(String text, Class clazz) throws JSONException, DataDictionaryException + { + if (text == null) + { + return null; + } + + JSONObject jsonObject = new JSONObject(text); + + return DataDictionaryBase.fromJSON(jsonObject, clazz); + } + + /** + * Instantiate a datadictionary from a JSONObject + * + * @param jsonObject + * @param clazz + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromJSON(JSONObject jsonObject, Class clazz) throws JSONException, DataDictionaryException + { + if (jsonObject == null) + { + return null; + } + + return fromMap(jsonObject.toMap(), clazz); + } + + /** + * Instantiate a datadictionary from a Map + * + * @param map + * @param clazz + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromMap(Map map, Class clazz) throws DataDictionaryException + { + if (map == null) + { + return null; + } + if (clazz == null) + { + return fromMap(map); + } + DataDictionary dataDictionnary; + try + { + Constructor constructor = clazz.getConstructor(Map.class); + dataDictionnary = constructor.newInstance(map); + } + catch (InvocationTargetException exception) + { + throw new DataDictionaryException("Cannot create instance of " + clazz.getName() + " : " + exception.getTargetException().getMessage()); + } + catch (Exception exception) + { + throw new DataDictionaryException("Cannot create instance of " + clazz.getName() + " : " + exception.getMessage()); + } + + return dataDictionnary; + } + + /** + * Instantiate a datadictionary from a Map + * + * @param map + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromMap(Map map) throws DataDictionaryException + { + if (map == null) + { + return null; + } + DataDictionaryBase dataDictionnary = new DataDictionaryBase(); + for (String key : map.keySet()) + { + Object value = map.get(key); + if (value instanceof JSONObject) + { + JSONObject jsonObjectValue = (JSONObject) value; + DataDictionary dataDictionaryValue = fromMap(jsonObjectValue.toMap()); + dataDictionnary.set(key, dataDictionaryValue); + } + else if (value instanceof JSONArray) + { + JSONArray jsonArrayValue = (JSONArray) value; + DataList listValue = new DataArrayList(jsonArrayValue.toList()); + dataDictionnary.set(key, listValue); + } + else + { + dataDictionnary.set(key, value); + } + } + + return dataDictionnary; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keyDescriptors; + protected HashMap data; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public DataDictionaryBase() + { + this.init(); + } + + /** + * Constructor using map and setting key not defined allowed flag + * + * @param map + * @throws DataDictionaryException + */ + public DataDictionaryBase(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using dataDictionary and setting key not defined allowed flag + * + * @param other + * @throws DataDictionaryException + */ + public DataDictionaryBase(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionnary + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public final boolean exists(String key) + { + return this.data.containsKey(key); + } + + @Override + public final boolean existsInKeys(String key) + { + // @formatter:off + return this.keyDescriptors.stream() + .filter(k -> k.getName().equals(key)) + .findAny() + .isPresent(); + // @formatter:on + } + + @Override + public final String[] keys() + { + Set setKeys = this.data.keySet(); + String[] keys = new String[setKeys.size()]; + + setKeys.toArray(keys); + + return keys; + } + + @Override + public final void addKey(String key) + { + if (key != null) + { + this.data.putIfAbsent(key, null); + } + } + + @Override + public final void removeKey(String key) + { + if (key != null) + { + this.data.remove(key); + } + } + + @Override + public final Object get(String key) + { + return this.data.get(key); + } + + @Override + public final void set(String key, Object value) throws DataDictionaryException + { + if (key == null) + { + throw new DataDictionaryException("Key null not allowed"); + } + + if (this.keyDescriptors.isEmpty()) + { + this.data.put(key, value); + } + else + { + Optional> keyDescriptor = this.getKeyDescriptor(key); + if (keyDescriptor.isPresent()) + { + try + { + String setterName = this.getSetterName(key); + // @formatter:off + List methodNameList = Arrays.asList(this.getClass().getDeclaredMethods()) + .stream() + .map(m -> m.getName()) + .collect(Collectors.toList()); + // @formatter:on + if (methodNameList.contains(setterName)) + { + Method method = this.getClass().getDeclaredMethod(setterName, Object.class); + method.setAccessible(true); + method.invoke(this, value); + } + else + { + this.data.put(key, keyDescriptor.get().convert(value)); + } + } + catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) + { + throw new DataDictionaryException("Set key " + key + " failed : " + e.getClass().getSimpleName() + " -> " + e.getMessage()); + } + catch (InvocationTargetException e) + { + throw new DataDictionaryException( + "Set key " + key + " failed : " + e.getTargetException().getClass().getSimpleName() + " -> " + e.getTargetException().getMessage()); + } + + } + else + { + throw new DataDictionaryException("Key " + key + " not allowed"); + } + } + } + + @Override + public void copy(DataDictionary other) throws DataDictionaryException + { + String[] otherKeys = other.keys(); + + this.clear(); + for (int i = 0; i < otherKeys.length; i++) + { + this.set(otherKeys[i], other.get(otherKeys[i])); + } + + this.checkAndUpdate(); + } + + @Override + public final void clear() + { + this.data.clear(); + } + + @Override + public final int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.data == null) ? 0 : this.data.hashCode()); + return result; + } + + @Override + public final boolean equals(Object object) + { + if (object == null) + { + return false; + } + if (this == object) + { + return true; + } + if (!(object instanceof DataDictionary)) + { + return false; + } + DataDictionaryBase other = (DataDictionaryBase) object; + + if (this.data == null) + { + if (other.data != null) + { + return false; + } + } + else + { + if (!this.data.keySet().equals(other.data.keySet())) + { + return false; + } + for (String key : this.data.keySet()) + { + Object thisValue = this.data.get(key); + Object otherValue = other.data.get(key); + + if (thisValue == null) + { + if (otherValue != null) + { + return false; + } + else + { + continue; + } + } + else if (otherValue == null) + { + return false; + } + if (!thisValue.getClass().equals(otherValue.getClass())) + { + if ((thisValue instanceof Number) && (otherValue instanceof Number)) + { + double thisDoubleValue = ((Number) thisValue).doubleValue(); + double otherDoubleValue = ((Number) otherValue).doubleValue(); + return thisDoubleValue == otherDoubleValue; + } + else + { + return false; + } + } + if (thisValue.getClass().isArray()) + { + if (thisValue instanceof boolean[]) + { + if (!Arrays.equals((boolean[]) thisValue, (boolean[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof byte[]) + { + if (!Arrays.equals((byte[]) thisValue, (byte[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof char[]) + { + if (!Arrays.equals((char[]) thisValue, (char[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof short[]) + { + if (!Arrays.equals((short[]) thisValue, (short[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof int[]) + { + if (!Arrays.equals((int[]) thisValue, (int[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof long[]) + { + if (!Arrays.equals((long[]) thisValue, (long[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof float[]) + { + if (!Arrays.equals((float[]) thisValue, (float[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof double[]) + { + if (!Arrays.equals((double[]) thisValue, (double[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof Object[]) + { + if (!Arrays.equals((Object[]) thisValue, (Object[]) otherValue)) + { + return false; + } + } + } + else + { + if (!thisValue.equals(otherValue)) + { + return false; + } + } + } + } + + return true; + } + + @Override + public final void toFile(File file, int indentFactor) throws IOException + { + OutputStream stream = new FileOutputStream(file); + + this.toStream(stream, indentFactor); + } + + @Override + public final void toStream(OutputStream stream, int indentFactor) throws IOException + { + String text = this.toString(indentFactor); + + stream.write(text.getBytes()); + } + + @Override + public JSONObject toJSON() + { + JSONObject jsonObject = new JSONObject(); + String[] keys = this.keys(); + + for (int i = 0; i < keys.length; i++) + { + Object value = this.get(keys[i]); + if (value instanceof DataDictionaryBase) + { + DataDictionaryBase dataDictionary = (DataDictionaryBase) value; + jsonObject.put(keys[i], dataDictionary.toJSON()); + } + else if (value instanceof LocalDateTime) + { + String textValue = ((LocalDateTime) value).format(KeyDescriptorLocalDateTime.DEFAULT_FORMATTER); + jsonObject.put(keys[i], textValue); + } + else + { + jsonObject.put(keys[i], value); + } + } + + return jsonObject; + } + + @SuppressWarnings("unchecked") + @Override + public final Map toMap() + { + return (Map) this.data.clone(); + } + + @Override + public String toString() + { + return this.toString(DataDictionaryBase.JSON_INDENT_FACTOR); + } + + @Override + public String toString(int identFactor) + { + return this.toJSON().toString(identFactor); + } + + @Override + public DataDictionaryBase clone() + { + try + { + Constructor constructor = this.getClass().getConstructor(DataDictionary.class); + return this.getClass().cast(constructor.newInstance(this)); + } + catch (Exception e) + { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public void print() + { + System.out.println(this.toString(2)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected final void checkAndUpdate() throws DataDictionaryException + { + this.updateOptionalParameters(); + this.checkMandatoryParameters(); + } + + protected void checkMandatoryParameters() throws DataDictionaryException + { + for (KeyDescriptor key : this.keyDescriptors) + { + if (key.isMandatory() && !this.exists(key.getName())) + { + throw new DataDictionaryException("Mandatory key " + key.getName() + " not defined"); + } + } + } + + protected void updateOptionalParameters() throws DataDictionaryException + { + } + + protected void addKeyDescriptor(KeyDescriptor keyDescriptor) throws DataDictionaryException + { + if (this.existsInKeys(keyDescriptor.getName())) + { + throw new DataDictionaryException("Key " + keyDescriptor.getName() + "already exists"); + } + this.keyDescriptors.add(keyDescriptor); + } + + protected void addAllKeyDescriptor(List> keyDescriptor) throws DataDictionaryException + { + for (KeyDescriptor key : keyDescriptor) + { + this.addKeyDescriptor(key); + } + } + + protected Optional> getKeyDescriptor(String name) + { + // @formatter:off + return this.keyDescriptors.stream() + .filter(k -> k.getName().equals(name)) + .findFirst(); + // @formatter:on + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private String getSetterName(String key) + { + return "set" + key.substring(0, 1).toUpperCase() + key.substring(1); + } + + private void init() + { + this.keyDescriptors = new ArrayList>(); + this.data = new HashMap(); + } + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java new file mode 100644 index 0000000..1596b47 --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java @@ -0,0 +1,66 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor interface + * + * @param + */ +public interface KeyDescriptor +{ + /** + * Get key name + * + * @return key name + */ + public String getName(); + + /** + * Get mandatory flag + * + * @return mandatory flag + */ + public boolean isMandatory(); + + /** + * Set mandatory flag + * + * @param mandatory + */ + public void setMandatory(boolean mandatory); + + /** + * Set a list of accepted values + * + * @param acceptedValues + */ + @SuppressWarnings("unchecked") + public void setAcceptedValues(T... acceptedValues); + + /** + * Convert a Object value to a T value + * + * @param value + * object to convert + * @return value converted to T type + * @throws DataDictionaryException + */ + public T convert(Object value) throws DataDictionaryException; + + /** + * Convert a T value to String + * + * @param value + * value to convert to String + * @return String representation of the value + */ + public String toString(T value); +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java new file mode 100644 index 0000000..cac21c7 --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java @@ -0,0 +1,210 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.util.Arrays; +import java.util.List; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor base + * + * @param + */ +public abstract class KeyDescriptorBase implements KeyDescriptor +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private String name; + private boolean mandatory; + protected List acceptedValues; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor setting all attributes + * + * @param name + * @param mandatory + */ + public KeyDescriptorBase(String name, boolean mandatory) + { + super(); + this.name = name; + this.mandatory = mandatory; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// KeyDescriptor + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public final T convert(Object value) throws DataDictionaryException + { + T convertedValue = null; + + if (value == null) + { + this.handleNullValue(); + return null; + } + + convertedValue = this.convertValue(value); + + this.checkAcceptedValues(convertedValue); + + return convertedValue; + } + + @Override + public String toString(T value) + { + if (value == null) + { + return null; + } + + return value.toString(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get key name + * + * @return key name + */ + @Override + public String getName() + { + return this.name; + } + + /** + * Set key name + * + * @param name + */ + public void setName(String name) + { + this.name = name; + } + + /** + * Get mandatory flag + * + * @return mandatory flag + */ + @Override + public boolean isMandatory() + { + return this.mandatory; + } + + /** + * Set mandatory flag + * + * @param mandatory + */ + @Override + public void setMandatory(boolean mandatory) + { + this.mandatory = mandatory; + } + + /** + * Set mandatory value + * + * @param acceptedValues + */ + @SuppressWarnings("unchecked") + @Override + public void setAcceptedValues(T... acceptedValues) + { + this.acceptedValues = Arrays.asList(acceptedValues); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract T convertValue(Object value) throws DataDictionaryException; + + protected final void handleNullValue() throws DataDictionaryException + { + if (this.isMandatory()) + { + throw new DataDictionaryException("Cannot set null " + this.getName()); + } + } + + protected void checkAcceptedValues(T value) throws DataDictionaryException + { + if (this.acceptedValues != null) + { + boolean accepted = false; + + for (T v : this.acceptedValues) + { + if (v.equals(value)) + { + accepted = true; + break; + } + } + + if (!accepted) + { + throw new DataDictionaryException("Key " + this.getName() + " doesn't respect mandatory value"); + } + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java new file mode 100644 index 0000000..397464d --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java @@ -0,0 +1,152 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor DataDictionary + * + * @param + */ +public class KeyDescriptorDataDictionary extends KeyDescriptorBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Class dataDictionaryClass; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param dataDictionaryClass + */ + public KeyDescriptorDataDictionary(String name, boolean mandatory, Class dataDictionaryClass) + { + super(name, mandatory); + this.dataDictionaryClass = dataDictionaryClass; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryKeyDescriptor + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("unchecked") + @Override + public T convertValue(Object value) throws DataDictionaryException + { + T convertedValue = null; + + if (this.dataDictionaryClass.isAssignableFrom(value.getClass())) + { + convertedValue = this.dataDictionaryClass.cast(value); + } + else if (value instanceof String) + { + convertedValue = (T) DataDictionaryBase.fromString((String) value, this.dataDictionaryClass); + } + else if (value instanceof DataDictionary) + { + try + { + Constructor constructor = this.dataDictionaryClass.getConstructor(DataDictionary.class); + convertedValue = constructor.newInstance((DataDictionary) value); + } + catch (InvocationTargetException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + + this.dataDictionaryClass.getSimpleName() + " : " + e.getTargetException().getMessage()); + } + catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + + this.dataDictionaryClass.getSimpleName() + " : " + e.getMessage()); + } + } + else if (value instanceof Map) + { + try + { + Constructor constructor = this.dataDictionaryClass.getConstructor(Map.class); + convertedValue = constructor.newInstance((Map) value); + } + catch (InvocationTargetException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + + this.dataDictionaryClass.getSimpleName() + " : " + e.getTargetException().getMessage()); + } + catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + + this.dataDictionaryClass.getSimpleName() + " : " + e.getMessage()); + } + } + else + { + throw new DataDictionaryException( + "Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + this.dataDictionaryClass.getSimpleName()); + } + + return convertedValue; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java new file mode 100644 index 0000000..aeffc8f --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java @@ -0,0 +1,142 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor Enum + * + * @param + */ +@SuppressWarnings("rawtypes") +public class KeyDescriptorEnum extends KeyDescriptorBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Class enumClass; + private String prefix; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param enumClass + */ + public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass) + { + this(name, mandatory, enumClass, ""); + } + + /** + * Constructor setting all parameters + * + * @param name + * @param mandatory + * @param enumClass + * @param prefix + */ + public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass, String prefix) + { + super(name, mandatory); + this.enumClass = enumClass; + this.prefix = prefix; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryKeyDescriptor + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public T convertValue(Object value) throws DataDictionaryException + { + T convertedValue = null; + + if (this.enumClass.isAssignableFrom(value.getClass())) + { + convertedValue = this.enumClass.cast(value); + } + else if (value instanceof String) + { + convertedValue = this.toEnum((String) value); + } + else + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + this.enumClass.getSimpleName()); + } + + return convertedValue; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("unchecked") + protected T toEnum(String value) throws DataDictionaryException + { + T convertedValue = null; + try + { + String preparedValue = this.prefix + value.toUpperCase().replace(".", "_").replace("-", "_"); + convertedValue = (T) Enum.valueOf(this.enumClass, preparedValue); + } + catch (IllegalArgumentException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": " + this.enumClass.getSimpleName() + " doesn't contain " + value); + } + return convertedValue; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java new file mode 100644 index 0000000..464997d --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java @@ -0,0 +1,233 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.json.JSONObject; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor List + * + * @param + */ +public class KeyDescriptorList extends KeyDescriptorBase> +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Class itemClass; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param itemClass + */ + public KeyDescriptorList(String name, boolean mandatory, Class itemClass) + { + super(name, mandatory); + this.itemClass = itemClass; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// KeyDescriptor> + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public List convertValue(Object value) throws DataDictionaryException + { + List convertedValue = new ArrayList(); + + if (this.itemClass.isAssignableFrom(value.getClass())) + { + T convertedItem = this.convertItem(value); + convertedValue.add(convertedItem); + } + else if (value instanceof HashMap) + { + T convertedItem = this.convertItem(value); + convertedValue.add(convertedItem); + } + else if (value instanceof List) + { + for (Object item : (List) value) + { + T convertedItem = this.convertItem(item); + convertedValue.add(convertedItem); + } + } + else if (value instanceof Object[]) + { + for (Object item : (Object[]) value) + { + T convertedItem = this.convertItem(item); + convertedValue.add(convertedItem); + } + } + else + { + throw new DataDictionaryException( + "Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to List<" + this.itemClass.getSimpleName() + ">"); + } + return convertedValue; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private T convertItem(Object item) throws DataDictionaryException + { + T convertedItem = null; + if (this.itemClass.isAssignableFrom(item.getClass())) + { + convertedItem = this.itemClass.cast(item); + } + else if (DataDictionary.class.isAssignableFrom(this.itemClass)) + { + convertedItem = this.convertDataDictionaryItem(item); + } + else if (Enum.class.isAssignableFrom(this.itemClass)) + { + convertedItem = this.convertEnumItem(item); + } + else + { + throw new DataDictionaryException( + "Key " + this.getName() + ": at least on item isn't a " + this.itemClass.getSimpleName() + ", type received :" + item.getClass().getSimpleName()); + } + return convertedItem; + } + + private T convertDataDictionaryItem(Object item) throws DataDictionaryException + { + T convertedItem = null; + if (item instanceof JSONObject) + { + JSONObject itemJsonObject = (JSONObject) item; + try + { + Constructor constructor = this.itemClass.getConstructor(Map.class); + convertedItem = (T) constructor.newInstance(itemJsonObject.toMap()); + } + catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) + { + e.printStackTrace(); + } + catch (InvocationTargetException e) + { + e.printStackTrace(); + } + } + else if (item instanceof Map) + { + try + { + Constructor constructor = this.itemClass.getConstructor(Map.class); + convertedItem = (T) constructor.newInstance((Map) item); + } + catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) + { + e.printStackTrace(); + } + catch (InvocationTargetException e) + { + e.printStackTrace(); + } + } + else + { + throw new DataDictionaryException("Key " + this.getName() + ": at least on item isn't a JSONObject, type received :" + item.getClass().getSimpleName()); + } + return convertedItem; + } + + @SuppressWarnings("unchecked") + private T convertEnumItem(Object item) throws DataDictionaryException + { + T convertedItem = null; + if (item instanceof String) + { + String itemString = (String) item; + try + { + Method valueOf = this.itemClass.getMethod("valueOf", String.class); + convertedItem = (T) valueOf.invoke(null, itemString.toUpperCase()); + } + catch (SecurityException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) + { + e.printStackTrace(); + } + catch (InvocationTargetException e) + { + e.printStackTrace(); + } + + } + else + { + throw new DataDictionaryException("Key " + this.getName() + ": at least on item isn't a JSONObject, type received :" + item.getClass().getSimpleName()); + } + return convertedItem; + } +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java new file mode 100644 index 0000000..92b47e7 --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java @@ -0,0 +1,170 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.util.List; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.MinMaxChecker; + +/** + * DataDictionary key descriptor Number min max + * + * @param + */ +public class KeyDescriptorListMinMaxSize extends KeyDescriptorList +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private MinMaxChecker minMaxChecker; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param itemClass + */ + public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass) + { + this(name, mandatory, itemClass, null, null); + } + + /** + * Constructor setting all attributes + * + * @param name + * @param mandatory + * @param itemClass + * @param min + * @param max + */ + public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass, Integer min, Integer max) + { + super(name, mandatory, itemClass); + this.minMaxChecker = new MinMaxChecker(min, max); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryKeyDescriptor + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public List convertValue(Object value) throws DataDictionaryException + { + List convertedValue = super.convertValue(value); + + this.check(convertedValue); + + return convertedValue; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get min + * + * @return min + */ + public Integer getMin() + { + return this.minMaxChecker.getMin().intValue(); + } + + /** + * Set min + * + * @param min + * @throws IllegalArgumentException + * if min is greater than max + */ + public void setMin(Integer min) + { + this.minMaxChecker.setMin(min); + } + + /** + * Get max + * + * @return max + */ + public Integer getMax() + { + return this.minMaxChecker.getMax().intValue(); + } + + /** + * Set max + * + * @param max + * @throws IllegalArgumentException + * if max is smaller than min + */ + public void setMax(Integer max) + { + this.minMaxChecker.setMax(max); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void check(List list) throws DataDictionaryException + { + if (list != null) + { + if (!this.minMaxChecker.check(list.size())) + { + throw new DataDictionaryException("Key " + this.getName() + ": value size (" + list.size() + ") out of bound"); + } + } + } +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java new file mode 100644 index 0000000..7f868dd --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java @@ -0,0 +1,160 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor LocalDateTime + */ +public class KeyDescriptorLocalDateTime extends KeyDescriptorBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Default pattern used */ + public static final String DEFAULT_PATTERN = "dd/MM/yyyy HH:mm:ss"; + /** Default formatter used */ + public static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_PATTERN); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private DateTimeFormatter formatter; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorLocalDateTime(String name, boolean mandatory) + { + this(name, mandatory, DEFAULT_PATTERN); + } + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param formatterPattern + */ + public KeyDescriptorLocalDateTime(String name, boolean mandatory, String formatterPattern) + { + super(name, mandatory); + this.formatter = DateTimeFormatter.ofPattern(formatterPattern); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// KeyDescriptor + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String toString(LocalDateTime value) + { + if (value == null) + { + return null; + } + + return this.formatter.format(value); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// KeyDescriptorBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public LocalDateTime convertValue(Object value) throws DataDictionaryException + { + LocalDateTime convertedValue = null; + + if (value instanceof LocalDateTime) + { + convertedValue = (LocalDateTime) value; + } + else if (value instanceof String) + { + convertedValue = this.toLocalDateTime((String) value); + } + else + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to LocalDateTime"); + } + + return convertedValue; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private LocalDateTime toLocalDateTime(String value) throws DataDictionaryException + { + LocalDateTime convertedValue = null; + try + { + convertedValue = LocalDateTime.parse(value, this.formatter); + } + catch (DateTimeParseException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": string " + value + " cannot be converted to LocalDateTime"); + } + return convertedValue; + } + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java new file mode 100644 index 0000000..c455a86 --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java @@ -0,0 +1,139 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor Number + */ +public class KeyDescriptorNumber extends KeyDescriptorBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorNumber(String name, boolean mandatory) + { + super(name, mandatory); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryKeyDescriptor + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Number convertValue(Object value) throws DataDictionaryException + { + Number convertedValue = null; + + if (value instanceof Number) + { + convertedValue = (Number) value; + } + else if (value instanceof String) + { + convertedValue = this.toNumber((String) value); + } + else + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to Number"); + } + + return convertedValue; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void checkAcceptedValues(Number value) throws DataDictionaryException + { + if (this.acceptedValues != null) + { + boolean accepted = false; + + for(Number v : this.acceptedValues) + { + if(v.doubleValue() == value.doubleValue()) + { + accepted = true; + break; + } + } + + if(!accepted) + { + throw new DataDictionaryException("Key " + this.getName() + " doesn't respect mandatory value"); + } + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Number toNumber(String value) throws DataDictionaryException + { + Number out = null; + try + { + out = Double.valueOf((String) value); + } + catch (NumberFormatException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to Number"); + } + return out; + } +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java new file mode 100644 index 0000000..ce295f3 --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java @@ -0,0 +1,164 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.MinMaxChecker; + +/** + * DataDictionary key descriptor Number min max + */ +public class KeyDescriptorNumberMinMax extends KeyDescriptorNumber +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private MinMaxChecker minMaxChecker; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorNumberMinMax(String name, boolean mandatory) + { + this(name, mandatory, null, null); + } + + /** + * Constructor setting all attributes + * + * @param name + * @param mandatory + * @param min + * @param max + */ + public KeyDescriptorNumberMinMax(String name, boolean mandatory, Number min, Number max) + { + super(name, mandatory); + this.minMaxChecker = new MinMaxChecker(min, max); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryKeyDescriptor + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Number convertValue(Object value) throws DataDictionaryException + { + Number convertedValue = super.convertValue(value); + + this.check(convertedValue); + + return convertedValue; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get min + * + * @return min + */ + public Number getMin() + { + return this.minMaxChecker.getMin(); + } + + /** + * Set min + * + * @param min + * @throws IllegalArgumentException + * if min is greater than max + */ + public void setMin(Number min) + { + this.minMaxChecker.setMin(min); + } + + /** + * Get max + * + * @return max + */ + public Number getMax() + { + return this.minMaxChecker.getMax(); + } + + /** + * Set max + * + * @param max + * @throws IllegalArgumentException + * if max is smaller than min + */ + public void setMax(Number max) + { + this.minMaxChecker.setMax(max); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void check(Number convertedValue) throws DataDictionaryException + { + if (convertedValue != null) + { + if (!this.minMaxChecker.check(convertedValue)) + { + throw new DataDictionaryException("Key " + this.getName() + ": value (" + convertedValue + ") out of bound"); + } + } + } +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java new file mode 100644 index 0000000..a7ca434 --- /dev/null +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java @@ -0,0 +1,140 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor String + */ +public class KeyDescriptorString extends KeyDescriptorBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final boolean DEFAULT_EMPTY_ALLOW_FLAG = true; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private boolean emptyAllow; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorString(String name, boolean mandatory) + { + this(name, mandatory, DEFAULT_EMPTY_ALLOW_FLAG); + } + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param emptyAllow + */ + public KeyDescriptorString(String name, boolean mandatory, boolean emptyAllow) + { + super(name, mandatory); + this.emptyAllow = emptyAllow; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryKeyDescriptor + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String convertValue(Object value) throws DataDictionaryException + { + String convertedValue = null; + + convertedValue = value.toString(); + + this.check(convertedValue); + + return convertedValue; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get empty allow flag + * + * @return empty allow flag + */ + public boolean isEmptyAllow() + { + return this.emptyAllow; + } + + /** + * Set empty allow flag + * + * @param emptyAllow + */ + public void setEmptyAllow(boolean emptyAllow) + { + this.emptyAllow = emptyAllow; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void check(String value) throws DataDictionaryException + { + if (!this.emptyAllow && value.trim().isEmpty()) + { + throw new DataDictionaryException("Key " + this.getName() + ": value can't be empty"); + } + } + +} diff --git a/src/main/java/enedis/lab/util/MinMaxChecker.java b/src/main/java/enedis/lab/util/MinMaxChecker.java new file mode 100644 index 0000000..12ad83c --- /dev/null +++ b/src/main/java/enedis/lab/util/MinMaxChecker.java @@ -0,0 +1,185 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util; + +/** + * Min max class + */ +public class MinMaxChecker +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Number min; + private Number max; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public MinMaxChecker() + { + super(); + } + + /** + * Constructor setting parameters + * + * @param min + * @param max + */ + public MinMaxChecker(Number min, Number max) + { + this(); + this.setMin(min); + this.setMax(max); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get min + * + * @return min + */ + public Number getMin() + { + return this.min; + } + + /** + * Set min + * + * @param min + * @throws IllegalArgumentException + * if min is greater than max + */ + public void setMin(Number min) + { + if (min != null) + { + if (this.max != null && min.doubleValue() > this.max.doubleValue()) + { + throw new IllegalArgumentException("min can't be greater than max"); + } + } + this.min = min; + } + + /** + * Get max + * + * @return max + */ + public Number getMax() + { + return this.max; + } + + /** + * Set max + * + * @param max + * @throws IllegalArgumentException + * if max is smaller than min + */ + public void setMax(Number max) + { + if (max != null) + { + if (this.min != null && max.doubleValue() < this.min.doubleValue()) + { + throw new IllegalArgumentException("max can't be smaller than min"); + } + } + this.max = max; + } + + /** + * Check the given value + * + * @param value + * @return true if the value is in [min, max] + */ + public boolean check(Number value) + { + if (value != null) + { + if (this.min != null) + { + if (value.doubleValue() < this.min.doubleValue()) + { + return false; + } + } + if (this.max != null) + { + if (value.doubleValue() > this.max.doubleValue()) + { + return false; + } + } + return true; + } + else + { + return false; + } + + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/util/SystemError.java b/src/main/java/enedis/lab/util/SystemError.java new file mode 100644 index 0000000..90b48c1 --- /dev/null +++ b/src/main/java/enedis/lab/util/SystemError.java @@ -0,0 +1,120 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util; + +import org.apache.commons.lang3.SystemUtils; + +import com.sun.jna.Native; +import com.sun.jna.platform.win32.Kernel32Util; + +/** + * Class for system error + * + */ +public class SystemError +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get system last error code + * + * @return System last error code + */ + static public int getCode() + { + return Native.getLastError(); + } + + /** + * Get system last error message + * + * @return System last error message + */ + static public String getMessage() + { + return getMessage(getCode()); + } + + /** + * Get system error message associated with code + * + * @param code + * the system error code + * + * @return System error message + */ + static public String getMessage(int code) + { + String message = null; + + if (SystemUtils.IS_OS_WINDOWS) + { + message = Kernel32Util.formatMessage(code); + } + else if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_UNIX || SystemUtils.IS_OS_MAC_OSX) + { + message = SystemLibC.INSTANCE.strerror(code); + } + + return message; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/util/SystemLibC.java b/src/main/java/enedis/lab/util/SystemLibC.java new file mode 100644 index 0000000..49592d5 --- /dev/null +++ b/src/main/java/enedis/lab/util/SystemLibC.java @@ -0,0 +1,31 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util; + +import com.sun.jna.Library; +import com.sun.jna.Native; + +/** + * Interface for system of C library + * + */ +public interface SystemLibC extends Library +{ + /** + * Instance + */ + SystemLibC INSTANCE = Native.load("c", SystemLibC.class); + + /** + * Get string error from code + * + * @param code + * @return string error + */ + public String strerror(int code); +} diff --git a/src/main/java/enedis/lab/util/message/Event.java b/src/main/java/enedis/lab/util/message/Event.java new file mode 100644 index 0000000..1b23b0a --- /dev/null +++ b/src/main/java/enedis/lab/util/message/Event.java @@ -0,0 +1,189 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; + +/** + * Event class + * + * Generated + */ +public abstract class Event extends Message +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATE_TIME = "dateTime"; + + private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.EVENT; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorLocalDateTime kDateTime; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected Event() + { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Event(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Event(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param dateTime + * @throws DataDictionaryException + */ + public Event(String name, LocalDateTime dateTime) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setDateTime(dateTime); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Message + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_TYPE)) + { + this.setType(TYPE_ACCEPTED_VALUE); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get date time + * + * @return the date time + */ + public LocalDateTime getDateTime() + { + return (LocalDateTime) this.data.get(KEY_DATE_TIME); + } + + /** + * Set date time + * + * @param dateTime + * @throws DataDictionaryException + */ + public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException + { + this.setDateTime((Object) dateTime); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setDateTime(Object dateTime) throws DataDictionaryException + { + this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); + this.keys.add(this.kDateTime); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/util/message/Message.java b/src/main/java/enedis/lab/util/message/Message.java new file mode 100644 index 0000000..efec77a --- /dev/null +++ b/src/main/java/enedis/lab/util/message/Message.java @@ -0,0 +1,216 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * Message class + * + * Generated + */ +public class Message extends DataDictionaryBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Key type */ + public static final String KEY_TYPE = "type"; + /** Key name */ + public static final String KEY_NAME = "name"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kType; + protected KeyDescriptorString kName; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected Message() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Message(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Message(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param type + * @param name + * @throws DataDictionaryException + */ + public Message(MessageType type, String name) throws DataDictionaryException + { + this(); + + this.setType(type); + this.setName(name); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get type + * + * @return the type + */ + public MessageType getType() + { + return (MessageType) this.data.get(KEY_TYPE); + } + + /** + * Get name + * + * @return the name + */ + public String getName() + { + return (String) this.data.get(KEY_NAME); + } + + /** + * Set type + * + * @param type + * @throws DataDictionaryException + */ + public void setType(MessageType type) throws DataDictionaryException + { + this.setType((Object) type); + } + + /** + * Set name + * + * @param name + * @throws DataDictionaryException + */ + public void setName(String name) throws DataDictionaryException + { + this.setName((Object) name); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setType(Object type) throws DataDictionaryException + { + this.data.put(KEY_TYPE, this.kType.convert(type)); + } + + protected void setName(Object name) throws DataDictionaryException + { + this.data.put(KEY_NAME, this.kName.convert(name)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kType = new KeyDescriptorEnum(KEY_TYPE, true, MessageType.class); + this.keys.add(this.kType); + + this.kName = new KeyDescriptorString(KEY_NAME, true, false); + this.keys.add(this.kName); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/util/message/MessageType.java b/src/main/java/enedis/lab/util/message/MessageType.java new file mode 100644 index 0000000..6441f34 --- /dev/null +++ b/src/main/java/enedis/lab/util/message/MessageType.java @@ -0,0 +1,21 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +/** + * Message type + */ +public enum MessageType +{ + /** Event */ + EVENT, + /** Request */ + REQUEST, + /** Response */ + RESPONSE; +} diff --git a/src/main/java/enedis/lab/util/message/Request.java b/src/main/java/enedis/lab/util/message/Request.java new file mode 100644 index 0000000..7ad542b --- /dev/null +++ b/src/main/java/enedis/lab/util/message/Request.java @@ -0,0 +1,155 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; + +/** + * Request class + * + * Generated + */ +public abstract class Request extends Message +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.REQUEST; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected Request() + { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Request(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Request(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param type + * @param name + * @throws DataDictionaryException + */ + public Request(MessageType type, String name) throws DataDictionaryException + { + this(); + + this.setType(type); + this.setName(name); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Message + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_TYPE)) + { + this.setType(TYPE_ACCEPTED_VALUE); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/util/message/Response.java b/src/main/java/enedis/lab/util/message/Response.java new file mode 100644 index 0000000..772c4b0 --- /dev/null +++ b/src/main/java/enedis/lab/util/message/Response.java @@ -0,0 +1,259 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; +import enedis.lab.types.datadictionary.KeyDescriptorNumber; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * Response class + * + * Generated + */ +public abstract class Response extends Message +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATE_TIME = "dateTime"; + protected static final String KEY_ERROR_CODE = "errorCode"; + protected static final String KEY_ERROR_MESSAGE = "errorMessage"; + + private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.RESPONSE; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorLocalDateTime kDateTime; + protected KeyDescriptorNumber kErrorCode; + protected KeyDescriptorString kErrorMessage; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected Response() + { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Response(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Response(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param type + * @param name + * @param dateTime + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public Response(MessageType type, String name, LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException + { + this(); + + this.setType(type); + this.setName(name); + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Message + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_TYPE)) + { + this.setType(TYPE_ACCEPTED_VALUE); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get date time + * + * @return the date time + */ + public LocalDateTime getDateTime() + { + return (LocalDateTime) this.data.get(KEY_DATE_TIME); + } + + /** + * Get error code + * + * @return the error code + */ + public Number getErrorCode() + { + return (Number) this.data.get(KEY_ERROR_CODE); + } + + /** + * Get error message + * + * @return the error message + */ + public String getErrorMessage() + { + return (String) this.data.get(KEY_ERROR_MESSAGE); + } + + /** + * Set date time + * + * @param dateTime + * @throws DataDictionaryException + */ + public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException + { + this.setDateTime((Object) dateTime); + } + + /** + * Set error code + * + * @param errorCode + * @throws DataDictionaryException + */ + public void setErrorCode(Number errorCode) throws DataDictionaryException + { + this.setErrorCode((Object) errorCode); + } + + /** + * Set error message + * + * @param errorMessage + * @throws DataDictionaryException + */ + public void setErrorMessage(String errorMessage) throws DataDictionaryException + { + this.setErrorMessage((Object) errorMessage); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setDateTime(Object dateTime) throws DataDictionaryException + { + this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); + } + + protected void setErrorCode(Object errorCode) throws DataDictionaryException + { + this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); + } + + protected void setErrorMessage(Object errorMessage) throws DataDictionaryException + { + this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); + this.keys.add(this.kDateTime); + + this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); + this.keys.add(this.kErrorCode); + + this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, false, true); + this.keys.add(this.kErrorMessage); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/util/message/ResponseBase.java b/src/main/java/enedis/lab/util/message/ResponseBase.java new file mode 100644 index 0000000..bd44949 --- /dev/null +++ b/src/main/java/enedis/lab/util/message/ResponseBase.java @@ -0,0 +1,189 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; + +/** + * ResponseBase class + * + * Generated + */ +public class ResponseBase extends Response +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATA = "data"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected ResponseBase() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseBase(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseBase(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseBase(String name, LocalDateTime dateTime, Number errorCode, String errorMessage, DataDictionaryBase data) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Response + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get data + * + * @return the data + */ + public DataDictionaryBase getData() + { + return (DataDictionaryBase) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(DataDictionaryBase data) throws DataDictionaryException + { + this.setData((Object) data); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, DataDictionaryBase.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageException.java b/src/main/java/enedis/lab/util/message/exception/MessageException.java new file mode 100644 index 0000000..217fe8e --- /dev/null +++ b/src/main/java/enedis/lab/util/message/exception/MessageException.java @@ -0,0 +1,112 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Message exception + */ +public class MessageException extends Exception +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -2263755971102386572L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public MessageException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageException(Throwable cause) + { + super(cause); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java new file mode 100644 index 0000000..27847b0 --- /dev/null +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java @@ -0,0 +1,112 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class MessageInvalidContentException extends MessageException +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -2263755971102386572L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public MessageInvalidContentException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageInvalidContentException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageInvalidContentException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageInvalidContentException(Throwable cause) + { + super(cause); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java new file mode 100644 index 0000000..c1e04fb --- /dev/null +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java @@ -0,0 +1,112 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class MessageInvalidFormatException extends MessageException +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -2263755971102386572L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public MessageInvalidFormatException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageInvalidFormatException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageInvalidFormatException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageInvalidFormatException(Throwable cause) + { + super(cause); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java new file mode 100644 index 0000000..0eee2df --- /dev/null +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java @@ -0,0 +1,112 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class MessageInvalidTypeException extends MessageException +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -2263755971102386572L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public MessageInvalidTypeException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageInvalidTypeException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageInvalidTypeException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageInvalidTypeException(Throwable cause) + { + super(cause); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java new file mode 100644 index 0000000..326eb13 --- /dev/null +++ b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java @@ -0,0 +1,112 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class MessageKeyNameDoesntExistException extends MessageException +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -2263755971102386572L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public MessageKeyNameDoesntExistException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageKeyNameDoesntExistException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageKeyNameDoesntExistException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageKeyNameDoesntExistException(Throwable cause) + { + super(cause); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java new file mode 100644 index 0000000..747843b --- /dev/null +++ b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java @@ -0,0 +1,112 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class MessageKeyTypeDoesntExistException extends MessageException +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -2263755971102386572L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public MessageKeyTypeDoesntExistException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageKeyTypeDoesntExistException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageKeyTypeDoesntExistException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageKeyTypeDoesntExistException(Throwable cause) + { + super(cause); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java new file mode 100644 index 0000000..e1f0277 --- /dev/null +++ b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java @@ -0,0 +1,112 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class UnsupportedMessageException extends MessageException +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -2263755971102386572L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public UnsupportedMessageException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public UnsupportedMessageException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public UnsupportedMessageException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public UnsupportedMessageException(Throwable cause) + { + super(cause); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java new file mode 100644 index 0000000..9ecbf5b --- /dev/null +++ b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java @@ -0,0 +1,153 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import java.util.HashMap; +import java.util.Map; + +import org.json.JSONException; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.util.message.Message; +import enedis.lab.util.message.exception.MessageInvalidContentException; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.UnsupportedMessageException; + +/** + * Message factory + * + * @param + */ +public class AbstractMessageFactory +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Class clazz; + private Map> messageClasses; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param clazz + */ + public AbstractMessageFactory(Class clazz) + { + this.clazz = clazz; + this.messageClasses = new HashMap>(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Response + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get message from text + * + * @param text + * @param name + * @return message + * @throws UnsupportedMessageException + * @throws MessageInvalidFormatException + * @throws MessageInvalidContentException + */ + public final T getMessage(String text, String name) throws UnsupportedMessageException, MessageInvalidFormatException, MessageInvalidContentException + { + T message = null; + + try + { + Class messageClazz = this.messageClasses.get(name); + + if (messageClazz != null) + { + message = messageClazz.cast(DataDictionaryBase.fromString(text, messageClazz)); + } + else + { + throw new UnsupportedMessageException("Unsupported " + this.clazz.getSimpleName() + " : " + name); + } + } + catch (JSONException e) + { + throw new MessageInvalidFormatException("Invalid " + this.clazz.getSimpleName() + " " + name + " format, it should be JSON : " + e.getMessage(), e); + } + catch (DataDictionaryException e) + { + throw new MessageInvalidContentException("Invalid " + this.clazz.getSimpleName() + " " + name + " content : " + e.getMessage(), e); + } + + return message; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Add a Message class to decode message with the given name + * + * @param name + * @param messageClazz + */ + public final void addMessageClass(String name, Class messageClazz) + { + if (name == null || messageClazz == null) + { + throw new IllegalArgumentException("Name and " + this.clazz.getSimpleName() + " class can't be null"); + } + + this.messageClasses.put(name, messageClazz); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} \ No newline at end of file diff --git a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java new file mode 100644 index 0000000..84a8e75 --- /dev/null +++ b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java @@ -0,0 +1,118 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import org.json.JSONException; +import org.json.JSONObject; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.message.Message; +import enedis.lab.util.message.MessageType; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.MessageInvalidTypeException; +import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; +import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; + +/** + * Message factory + */ +public abstract class BasicMessageFactory +{ + /** + * Get message from text + * + * @param text + * @return message + * @throws MessageInvalidFormatException + * @throws MessageKeyTypeDoesntExistException + * @throws MessageKeyNameDoesntExistException + * @throws MessageInvalidTypeException + */ + public static Message getMessage(String text) + throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, MessageInvalidTypeException + { + JSONObject messageJson = convertTextToJson(text); + + MessageType type = extractType(messageJson); + String name = extractName(messageJson); + + return createMessage(type, name); + } + + private static JSONObject convertTextToJson(String text) throws MessageInvalidFormatException + { + try + { + return new JSONObject(text); + } + catch (JSONException e) + { + throw new MessageInvalidFormatException("Invalid format, it should be JSON : " + e.getMessage()); + } + } + + private static MessageType extractType(JSONObject jsonObj) throws MessageKeyTypeDoesntExistException, MessageInvalidFormatException, MessageInvalidTypeException + { + try + { + checkKeyTypeExists(jsonObj); + String typeStr = jsonObj.getString(Message.KEY_TYPE); + MessageType type = MessageType.valueOf(typeStr); + return type; + } + catch (JSONException e) + { + throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); + } + catch (IllegalArgumentException e) + { + throw new MessageInvalidTypeException("Invalid format : " + e.getMessage()); + } + } + + private static void checkKeyTypeExists(JSONObject jsonObj) throws MessageKeyTypeDoesntExistException + { + if (!jsonObj.has(Message.KEY_TYPE)) + { + throw new MessageKeyTypeDoesntExistException("Key type missing"); + } + } + + private static String extractName(JSONObject jsonObj) throws MessageInvalidFormatException, MessageKeyNameDoesntExistException + { + try + { + checkKeyNameExists(jsonObj); + return jsonObj.getString(Message.KEY_NAME); + } + catch (JSONException e) + { + throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); + } + } + + private static void checkKeyNameExists(JSONObject jsonObj) throws MessageKeyNameDoesntExistException + { + if (!jsonObj.has(Message.KEY_NAME)) + { + throw new MessageKeyNameDoesntExistException("Key name missing"); + } + } + + private static Message createMessage(MessageType type, String name) + { + try + { + return new Message(type, name); + } + catch (DataDictionaryException e) + { + return null; + } + } +} diff --git a/src/main/java/enedis/lab/util/message/factory/EventFactory.java b/src/main/java/enedis/lab/util/message/factory/EventFactory.java new file mode 100644 index 0000000..f349021 --- /dev/null +++ b/src/main/java/enedis/lab/util/message/factory/EventFactory.java @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import enedis.lab.util.message.Event; + +/** + * Request factory + */ +public class EventFactory extends AbstractMessageFactory +{ + /** + * Default constructor + */ + public EventFactory() + { + super(Event.class); + } +} diff --git a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java new file mode 100644 index 0000000..daa4df0 --- /dev/null +++ b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java @@ -0,0 +1,177 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import enedis.lab.util.message.Message; +import enedis.lab.util.message.exception.MessageInvalidContentException; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.MessageInvalidTypeException; +import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; +import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; +import enedis.lab.util.message.exception.UnsupportedMessageException; + +/** + * Message factory + */ +public class MessageFactory +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private RequestFactory requestFactory; + private ResponseFactory responseFactory; + private EventFactory eventFactory; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public MessageFactory() + { + super(); + } + + /** + * Constructor using field + * + * @param requestFactory + * @param responseFactory + * @param eventFactory + */ + public MessageFactory(RequestFactory requestFactory, ResponseFactory responseFactory, EventFactory eventFactory) + { + super(); + this.requestFactory = requestFactory; + this.responseFactory = responseFactory; + this.eventFactory = eventFactory; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get message from String + * + * @param text + * @return message + * @throws MessageInvalidTypeException + * @throws MessageKeyNameDoesntExistException + * @throws MessageKeyTypeDoesntExistException + * @throws MessageInvalidFormatException + * @throws MessageInvalidContentException + * @throws UnsupportedMessageException + */ + public Message getMessage(String text) throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + this.checkSubFactoryReferences(); + + Message genericMessage = BasicMessageFactory.getMessage(text); + Message message = null; + + // @formatter:off + switch (genericMessage.getType()) + { + case REQUEST: message = this.requestFactory.getMessage(text, genericMessage.getName()); break; + case RESPONSE: message = this.responseFactory.getMessage(text, genericMessage.getName()); break; + case EVENT: message = this.eventFactory.getMessage(text, genericMessage.getName()); break; + default: + break; + } + // @formatter:on + + return message; + } + + /** + * Set request factory + * + * @param requestFactory + */ + public void setRequestFactory(RequestFactory requestFactory) + { + this.requestFactory = requestFactory; + } + + /** + * Set response factory + * + * @param responseFactory + */ + public void setResponseFactory(ResponseFactory responseFactory) + { + this.responseFactory = responseFactory; + } + + /** + * Set event factory + * + * @param eventFactory + */ + public void setEventFactory(EventFactory eventFactory) + { + this.eventFactory = eventFactory; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void checkSubFactoryReferences() + { + if (this.requestFactory == null || this.responseFactory == null || this.eventFactory == null) + { + throw new IllegalStateException("requestFactory, responseFactory and eventFactory have to be set"); + } + } + +} diff --git a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java new file mode 100644 index 0000000..4df5690 --- /dev/null +++ b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import enedis.lab.util.message.Request; + +/** + * Request factory + */ +public class RequestFactory extends AbstractMessageFactory +{ + /** + * Default constructor + */ + public RequestFactory() + { + super(Request.class); + } +} diff --git a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java new file mode 100644 index 0000000..eda280e --- /dev/null +++ b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import enedis.lab.util.message.Response; + +/** + * Request factory + */ +public class ResponseFactory extends AbstractMessageFactory +{ + /** + * Default constructor + */ + public ResponseFactory() + { + super(Response.class); + } +} diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifier.java b/src/main/java/enedis/lab/util/task/FilteredNotifier.java new file mode 100644 index 0000000..bf86a63 --- /dev/null +++ b/src/main/java/enedis/lab/util/task/FilteredNotifier.java @@ -0,0 +1,113 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.Collection; +import java.util.function.Predicate; + +/** + * Notifier interface with filter + * + * @param + * the filter + * @param + * the subscriber + */ +public interface FilteredNotifier extends Notifier +{ + /** + * Add a subscriber with filter + * + * @param filter + * the filter used for subscription + * @param listener + * the subscriber reference + * @throws Exception + * on subscription failure + */ + public void subscribe(F filter, T listener) throws Exception; + + /** + * Remove a subscriber with filter + * + * @param filter + * the filter used for subscription + * @param listener + * the subscriber reference + * @throws Exception + * if filter or listener not found + */ + public void unsubscribe(F filter, T listener) throws Exception; + + /** + * Check if the given filter has a given subscriber + * + * @param filter + * the filter used for subscription + * @param listener + * the subscriber reference + * @return true if the given subscriber exists + */ + public boolean hasSubscriber(F filter, T listener); + + /** + * Get subscribers associated with filter + * + * @param filter + * the filter used for subscription + * @return The subscribers collection + */ + public Collection getSubscribers(F filter); + + /** + * Get subscribers including global and/or filters + * + * @param includeGlobal + * indicates if includes global subscribers + * @param includeFilter + * indicates if includes filters subscribers + * @return The subscribers collection + */ + public Collection getSubscribers(boolean includeGlobal, boolean includeFilter); + + /** + * Get subscribers associated with predicate filter + * + * @param predicate + * the predicate used to test with filters used for subscription + * @param includeGlobal + * indicates if includes global subscribers + * @return The subscribers collection + */ + public Collection getSubscribers(Predicate predicate, boolean includeGlobal); + + /** + * Check if the given filter has been set + * + * @param filter + * the filter used for subscription + * @return true if the given filter exists + */ + public boolean hasFilter(F filter); + + /** + * Get filters + * + * @return The filters collection + */ + public Collection getFilters(); + + /** + * Get filters associated with listener + * + * @param listener + * the subscriber reference + * @return The filters collection + */ + public Collection getFilters(T listener); +} diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java new file mode 100644 index 0000000..5b772d7 --- /dev/null +++ b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java @@ -0,0 +1,302 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; + +/** + * Filtered notifier with subscribers basic implementation + * + * @param + * the filter type + * @param + * the subscriber type + */ +public class FilteredNotifierBase implements FilteredNotifier +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected Map> subscribersFiltered; + private Lock subscribersFilteredLock = new ReentrantLock(); + protected Collection subscribersUnfiltered; + private Lock subscribersUnfilteredLock = new ReentrantLock(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public FilteredNotifierBase() + { + super(); + this.subscribersFiltered = new ConcurrentHashMap>(); + this.subscribersUnfiltered = new CopyOnWriteArraySet(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Listenable + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void subscribe(T listener) + { + if (listener == null) + { + return; + } + this.subscribersUnfilteredLock.lock(); + this.subscribersUnfiltered.add(listener); + this.subscribersUnfilteredLock.unlock(); + } + + @Override + public void unsubscribe(T listener) + { + if (listener == null) + { + return; + } + for (F filter : this.getFilters()) + { + this.unsubscribe(filter, listener); + } + this.subscribersUnfilteredLock.lock(); + this.subscribersUnfiltered.remove(listener); + this.subscribersUnfilteredLock.unlock(); + } + + @Override + public boolean hasSubscriber(T listener) + { + for (F filter : this.getFilters()) + { + if (this.hasSubscriber(filter, listener)) + { + return true; + } + } + + this.subscribersUnfilteredLock.lock(); + boolean hasSubscriber = this.subscribersUnfiltered.contains(listener); + this.subscribersUnfilteredLock.unlock(); + + return hasSubscriber; + } + + @Override + public Collection getSubscribers() + { + return this.getSubscribers(true, true); + } + + @Override + public void subscribe(F filter, T listener) + { + if (filter == null || listener == null) + { + return; + } + Collection subscribers; + if (this.hasFilter(filter)) + { + subscribers = this.getSubscribers(filter); + if (!subscribers.contains(listener)) + { + subscribers.add(listener); + } + } + else + { + subscribers = new HashSet(); + subscribers.add(listener); + } + this.subscribersFilteredLock.lock(); + this.subscribersFiltered.put(filter, subscribers); + this.subscribersFilteredLock.unlock(); + } + + @Override + public void unsubscribe(F filter, T listener) + { + if (filter == null || listener == null) + { + return; + } + Collection subscribers = this.getSubscribers(filter); + if (subscribers.contains(listener)) + { + subscribers.remove(listener); + if (subscribers.size() == 0) + { + this.subscribersFilteredLock.lock(); + this.subscribersFiltered.remove(filter); + this.subscribersFilteredLock.unlock(); + } + } + } + + @Override + public boolean hasSubscriber(F filter, T listener) + { + Collection subscribers = this.getSubscribers(filter); + + return subscribers.contains(listener); + } + + @Override + public Collection getSubscribers(F filter) + { + this.subscribersFilteredLock.lock(); + Collection subscribersFiltered = (this.hasFilter(filter)) ? this.subscribersFiltered.get(filter) : new HashSet(); + this.subscribersFilteredLock.unlock(); + + return subscribersFiltered; + } + + @Override + public Collection getSubscribers(boolean includeGlobal, boolean includeFilter) + { + Collection subscribers = new HashSet(); + + if (includeFilter) + { + for (F filter : this.getFilters()) + { + Collection filterSubscribers = this.getSubscribers(filter); + subscribers.addAll(filterSubscribers); + } + } + if (includeGlobal) + { + this.subscribersUnfilteredLock.lock(); + subscribers.addAll(this.subscribersUnfiltered); + this.subscribersUnfilteredLock.unlock(); + } + + return subscribers; + } + + @Override + public Collection getSubscribers(Predicate predicate, boolean includeGlobal) + { + Collection subscribers = new HashSet(); + + if (predicate != null) + { + this.subscribersFilteredLock.lock(); + for (F filter : this.subscribersFiltered.keySet()) + { + if (predicate.test(filter)) + { + subscribers.addAll(this.subscribersFiltered.get(filter)); + } + } + this.subscribersFilteredLock.unlock(); + } + if (includeGlobal) + { + this.subscribersUnfilteredLock.lock(); + subscribers.addAll(this.subscribersUnfiltered); + this.subscribersUnfilteredLock.unlock(); + } + + return subscribers; + } + + @Override + public boolean hasFilter(F filter) + { + this.subscribersFilteredLock.lock(); + boolean hasFilter = (filter != null) ? this.subscribersFiltered.containsKey(filter) : false; + this.subscribersFilteredLock.unlock(); + + return hasFilter; + } + + @Override + public Collection getFilters() + { + this.subscribersFilteredLock.lock(); + Collection subscribersFiltered = this.subscribersFiltered.keySet(); + this.subscribersFilteredLock.unlock(); + + return subscribersFiltered; + } + + @Override + public Collection getFilters(T listener) + { + Collection filters = new HashSet(); + + for (F filter : this.getFilters()) + { + Collection subscribers = this.getSubscribers(filter); + if (subscribers.contains(listener)) + { + filters.add(filter); + } + } + + return filters; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} \ No newline at end of file diff --git a/src/main/java/enedis/lab/util/task/Notifier.java b/src/main/java/enedis/lab/util/task/Notifier.java new file mode 100644 index 0000000..e8ec73e --- /dev/null +++ b/src/main/java/enedis/lab/util/task/Notifier.java @@ -0,0 +1,47 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.Collection; + +/** + * Notifier interface + * + * @param + */ +public interface Notifier +{ + /** + * Add a subscriber + * + * @param subscriber + */ + public void subscribe(T subscriber); + + /** + * Remove a subscriber + * + * @param subscriber + */ + public void unsubscribe(T subscriber); + + /** + * Check if the given subscriber has been added + * + * @param subscriber + * @return true if the given subscriber has been added + */ + public boolean hasSubscriber(T subscriber); + + /** + * Get subscribers + * + * @return The subscribers collection + */ + public Collection getSubscribers(); +} diff --git a/src/main/java/enedis/lab/util/task/NotifierBase.java b/src/main/java/enedis/lab/util/task/NotifierBase.java new file mode 100644 index 0000000..d0a9979 --- /dev/null +++ b/src/main/java/enedis/lab/util/task/NotifierBase.java @@ -0,0 +1,118 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Notifier with subscribers basic implementation + * + * @param + * the subscriber type + */ +public class NotifierBase implements Notifier +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected Set subscribers; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public NotifierBase() + { + super(); + this.subscribers = new CopyOnWriteArraySet(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Listenable + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void subscribe(T listener) + { + if (listener != null && !this.subscribers.contains(listener)) + { + this.subscribers.add(listener); + } + } + + @Override + public void unsubscribe(T listener) + { + if (listener != null && this.subscribers.contains(listener)) + { + this.subscribers.remove(listener); + } + } + + @Override + public boolean hasSubscriber(T listener) + { + return this.subscribers.contains(listener); + } + + @Override + public Collection getSubscribers() + { + return this.subscribers; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} \ No newline at end of file diff --git a/src/main/java/enedis/lab/util/task/Subscriber.java b/src/main/java/enedis/lab/util/task/Subscriber.java new file mode 100644 index 0000000..253daec --- /dev/null +++ b/src/main/java/enedis/lab/util/task/Subscriber.java @@ -0,0 +1,16 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +/** + * Subscriber interface + */ +public interface Subscriber +{ + +} diff --git a/src/main/java/enedis/lab/util/task/Task.java b/src/main/java/enedis/lab/util/task/Task.java new file mode 100644 index 0000000..d3ead6f --- /dev/null +++ b/src/main/java/enedis/lab/util/task/Task.java @@ -0,0 +1,31 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +/** + * Task interface + */ +public interface Task +{ + /** + * Start task + */ + public void start(); + + /** + * Stop task + */ + public void stop(); + + /** + * Get the task running state + * + * @return true if the task running + */ + public boolean isRunning(); +} diff --git a/src/main/java/enedis/lab/util/task/TaskBase.java b/src/main/java/enedis/lab/util/task/TaskBase.java new file mode 100644 index 0000000..15b94d8 --- /dev/null +++ b/src/main/java/enedis/lab/util/task/TaskBase.java @@ -0,0 +1,205 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Task basic implementation + */ +public abstract class TaskBase implements Task, Runnable +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private AtomicBoolean stopRequired; + private Thread task; + protected static Logger logger = LogManager.getLogger(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public TaskBase() + { + super(); + this.stopRequired = new AtomicBoolean(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Task + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void start() + { + if (this.task == null || !this.task.isAlive()) + { + this.stopRequired.set(false); + this.task = new Thread(this); + this.task.start(); + } + } + + @Override + public void stop() + { + try + { + if (this.task != null && this.task.isAlive()) + { + this.stopRequired.set(true); + if (this.task != Thread.currentThread()) + { + this.task.join(); + } + } + } + catch (InterruptedException exception) + { + logger.error("Stop task interrupted", exception); + } + } + + @Override + public final boolean isRunning() + { + return this.task == null ? false : this.task.isAlive(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void run() + { + this.runOnStart(); + this.runProcess(); + this.runOnTerminate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected final boolean isStopRequired() + { + return this.stopRequired.get(); + } + + protected final void runOnStart() + { + try + { + this.onStart(); + } + catch (Exception exception) + { + logger.error("Task on start aborted", exception); + this.runOnError(exception); + } + } + + protected final void runProcess() + { + try + { + this.process(); + } + catch (Exception exception) + { + logger.error("Task process aborted", exception); + this.runOnError(exception); + } + } + + protected final void runOnTerminate() + { + try + { + this.onTerminate(); + } + catch (Exception exception) + { + logger.error("Task on terminate aborted", exception); + this.runOnError(exception); + } + } + + protected final void runOnError(Exception exception) + { + try + { + this.onError(exception); + } + catch (Exception onErrorException) + { + logger.error("Task on error aborted", exception); + } + } + + /** + * Task core process method + */ + protected abstract void process(); + + protected void onStart() + { + } + + protected void onTerminate() + { + } + + protected void onError(Exception exception) + { + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodic.java b/src/main/java/enedis/lab/util/task/TaskPeriodic.java new file mode 100644 index 0000000..84f279d --- /dev/null +++ b/src/main/java/enedis/lab/util/task/TaskPeriodic.java @@ -0,0 +1,151 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.concurrent.atomic.AtomicLong; + +import enedis.lab.util.time.Time; + +/** + * Periodic task with configurable period + */ +public abstract class TaskPeriodic extends TaskBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default period (in milliseconds) used to execute process + */ + public static final long DEFAULT_PERIOD = 1000; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private AtomicLong period = new AtomicLong(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor with default parameter + * + * @see #DEFAULT_PERIOD + */ + public TaskPeriodic() + { + super(); + this.setPeriod(DEFAULT_PERIOD); + } + + /** + * Constructor with polling period + * + * @param period + * the period (in milliseconds) used to execute process + */ + public TaskPeriodic(long period) + { + super(); + this.setPeriod(period); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Runnable + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void run() + { + this.runOnStart(); + while (!this.isStopRequired()) + { + this.runProcess(); + if (this.isStopRequired()) + { + break; + } + this.waitPeriod(); + } + this.runOnTerminate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get polling period + * + * @return The period (in milliseconds) used to execute process + */ + public long getPeriod() + { + return this.period.get(); + } + + /** + * Set the polling period + * + * @param period + * the period (in milliseconds) used to execute process + * @throws IllegalArgumentException + * if period <= 0 + */ + public void setPeriod(long period) + { + if (period <= 0) + { + throw new IllegalArgumentException("Cannot set period " + period + " : must be > 0"); + } + this.period.set(period); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void waitPeriod() + { + Time.sleep(this.getPeriod()); + } +} diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java new file mode 100644 index 0000000..bf34fb8 --- /dev/null +++ b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java @@ -0,0 +1,122 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.Collection; + +/** + * Periodic task with subscribers basic implementation + * + * @param + * the subscriber type + */ +public abstract class TaskPeriodicWithSubscribers extends TaskPeriodic implements Notifier +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected Notifier notifier; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public TaskPeriodicWithSubscribers() + { + super(); + this.notifier = new NotifierBase(); + } + + /** + * Constructor setting period + * + * @param period + * the period in milliseconds + */ + public TaskPeriodicWithSubscribers(long period) + { + super(period); + this.notifier = new NotifierBase(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void subscribe(T listener) + { + this.notifier.subscribe(listener); + } + + @Override + public void unsubscribe(T listener) + { + this.notifier.unsubscribe(listener); + } + + @Override + public boolean hasSubscriber(T listener) + { + return this.notifier.hasSubscriber(listener); + } + + @Override + public Collection getSubscribers() + { + return this.notifier.getSubscribers(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/util/time/Time.java b/src/main/java/enedis/lab/util/time/Time.java new file mode 100644 index 0000000..25999f0 --- /dev/null +++ b/src/main/java/enedis/lab/util/time/Time.java @@ -0,0 +1,163 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.time; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Time implementation + */ +public abstract class Time +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Millisecond */ + public final static int MILLISECOND = 1; + /** Second */ + public final static int SECOND = 1000 * MILLISECOND; + /** Minute */ + public final static int MINUTE = 60 * SECOND; + /** Hour */ + public final static int HOUR = 60 * MINUTE; + /** Day */ + public final static int DAY = 24 * HOUR; + + /* Default format for displaying the date/time */ + private static final String DFLT_DATETIME_FORMAT = "yyyy/MM/dd HH:mm:ss.SSS"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Wait the given duration in millisecond + * + * @param duration + */ + public static void sleep(long duration) + { + if (duration <= 0L) + { + return; + } + try + { + Thread.sleep(duration); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + + /** + * Convert date/time from long number (since an origin) to a string in the default format + * + * @param timestamp + * a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) + * @return the time/date string in the default format + */ + public static String timestampToDateTimeStr(long timestamp) + { + return timestampToDateTimeStr(timestamp, DFLT_DATETIME_FORMAT); + } + + /** + * Convert date/time from long number to a string in the specified format + * + * @param timestamp + * a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) + * @param dateTimeFormatStr + * the format specified + * @return the time/date string in the specified format + */ + public static String timestampToDateTimeStr(long timestamp, String dateTimeFormatStr) + { + DateFormat dateFormat = new SimpleDateFormat(dateTimeFormatStr); + Date date = new Date(timestamp); + return dateFormat.format(date); + } + + /** + * Timestamp a message with the current DateTime + * + * @param msg + * @return the message decorated with current DateTime + */ + public static String timestamp(String msg) + { + return timestamp(msg, DFLT_DATETIME_FORMAT); + } + + /** + * Timestamp a message with the current DateTime + * + * @param msg + * @param dateTimeFormat + * @return the message decorated with current DateTime + */ + public static String timestamp(String msg, String dateTimeFormat) + { + DateFormat dateFormat = new SimpleDateFormat(dateTimeFormat); + Date date = new Date(); + String text = dateFormat.format(date) + " : " + msg; + return text; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java new file mode 100644 index 0000000..1cbce3d --- /dev/null +++ b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java @@ -0,0 +1,104 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +public class ReadNextFrameSubscriber implements TICCoreSubscriber +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private TICCoreFrame frame; + private TICCoreError error; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public ReadNextFrameSubscriber() + { + super(); + this.clear(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onData(TICCoreFrame frame) + { + this.frame = frame; + } + + @Override + public void onError(TICCoreError error) + { + this.error = error; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCoreFrame getData() + { + return this.frame; + } + + public TICCoreError getError() + { + return this.error; + } + + public void clear() + { + this.frame = null; + this.error = null; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/tic/core/TICCore.java b/src/main/java/enedis/tic/core/TICCore.java new file mode 100644 index 0000000..b04d740 --- /dev/null +++ b/src/main/java/enedis/tic/core/TICCore.java @@ -0,0 +1,107 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.util.List; + +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.util.task.Task; + +/** + * TICCore interface + */ +public interface TICCore extends Task +{ + /** + * Get available TICs + * + * @return The collection of TIC identifiers + */ + public List getAvailableTICs(); + + /** + * Get modems informations + * + * @return The collection of TIC port descriptors + */ + public List getModemsInfo(); + + /** + * Read next frame + * + * @param identifier + * the TIC identifier + * + * @return Frame read or null if timed out + * @throws TICCoreException + * if identifier not found or if any error occurs + */ + public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException; + + /** + * Read next frame + * + * @param identifier + * the TIC identifier + * @param timeout + * the read timeout in milliseconds + * + * @return Frame read or null if timed out + * @throws TICCoreException + * if identifier not found or if any error occurs + */ + public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException; + + /** + * Add a subscriber + * + * @param subscriber + */ + public void subscribe(TICCoreSubscriber subscriber); + + /** + * Remove a subscriber + * + * @param subscriber + */ + public void unsubscribe(TICCoreSubscriber subscriber); + + /** + * Add a subscriber with identifier + * + * @param identifier + * the identifier used for subscription + * @param listener + * the subscriber reference + * @throws Exception + * on subscription failure + */ + public void subscribe(TICIdentifier identifier, TICCoreSubscriber listener) throws TICCoreException; + + /** + * Remove a subscriber with identifier + * + * @param identifier + * the identifier used for subscription + * @param listener + * the subscriber reference + * @throws Exception + * if filter or listener not found + */ + public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber listener) throws TICCoreException; + + /** + * Get TICs identifier associated with a subscriber + * + * @param listener + * the subscriber reference + * + * @return The collection of TIC identifiers for the subscriber + */ + public List getIndentifiers(TICCoreSubscriber listener); +} diff --git a/src/main/java/enedis/tic/core/TICCoreBase.java b/src/main/java/enedis/tic/core/TICCoreBase.java new file mode 100644 index 0000000..206c705 --- /dev/null +++ b/src/main/java/enedis/tic/core/TICCoreBase.java @@ -0,0 +1,581 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.function.Predicate; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import enedis.lab.io.PlugSubscriber; +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.io.tic.TICPortFinder; +import enedis.lab.io.tic.TICPortFinderBase; +import enedis.lab.io.tic.TICPortPlugNotifier; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.time.Time; +import enedis.lab.util.task.FilteredNotifier; +import enedis.lab.util.task.FilteredNotifierBase; +import enedis.lab.util.task.Task; +import enedis.lab.util.task.TaskBase; + +/** + * TICCore implementation + */ +public class TICCoreBase implements TICCore, Task, TICCoreSubscriber, PlugSubscriber +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final int PLUG_NOTIFIER_POLLING_PERIOD = 100; + private static final int READ_NEXT_FRAME_TIMEOUT = 30000; + private static final int READ_NEXT_FRAME_POLLING_PERIOD = 100; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private TICPortFinder portFinder; + private TICPortPlugNotifier plugNotifier; + private long plugNotifierPeriod; + private Constructor streamConstructor; + private TICMode streamMode; + private List nativePortNamesOnStart; + private Collection streamList; + private FilteredNotifier eventNotifier; + private static Logger logger = LogManager.getLogger(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCoreBase() + { + this(TICMode.AUTO, null); + } + + public TICCoreBase(TICMode streamMode, List nativePortNamesStart) + { + this(TICPortFinderBase.getInstance(), PLUG_NOTIFIER_POLLING_PERIOD, TICCoreStreamBase.class, streamMode, nativePortNamesStart); + } + + public TICCoreBase(TICPortFinder ticPortFinder, long plugNotifierPeriod, Class streamClass, TICMode streamMode, List nativePortNamesOnStart) + { + super(); + this.portFinder = ticPortFinder; + this.plugNotifierPeriod = plugNotifierPeriod; + try + { + this.streamConstructor = streamClass.getConstructor(String.class, String.class, TICMode.class); + } + catch (NoSuchMethodException e) + { + throw new IllegalArgumentException("Class " + streamClass.getName() + " has no valid constructor : " + e.getMessage()); + } + if (streamMode == null) + { + throw new IllegalArgumentException("TICMode should be defined"); + } + this.streamMode = streamMode; + this.nativePortNamesOnStart = nativePortNamesOnStart; + this.streamList = Collections.synchronizedSet(new HashSet()); + this.eventNotifier = new FilteredNotifierBase(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICCore + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public List getAvailableTICs() + { + List identifiers = new DataArrayList(); + + for (TICCoreStream stream : this.streamList) + { + identifiers.add(stream.getIdentifier()); + } + + return identifiers; + } + + @Override + public List getModemsInfo() + { + List descriptors = new DataArrayList(); + + for (TICCoreStream stream : this.streamList) + { + TICPortDescriptor descriptor = this.portFinder.findByPortName(stream.getIdentifier().getPortName()); + descriptors.add(descriptor); + } + + return descriptors; + } + + @Override + public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException + { + return this.readNextFrame(identifier, READ_NEXT_FRAME_TIMEOUT); + } + + @Override + public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException + { + TICCoreFrame frame = null; + TICPortDescriptor descriptor = null; + TICCoreStream stream = this.findStream(identifier); + + if (stream == null) + { + descriptor = this.portFinder.findNative(identifier.getPortName()); + if (descriptor == null) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + stream = this.startNewStream(descriptor); + } + ReadNextFrameSubscriber subscriber = new ReadNextFrameSubscriber(); + stream.subscribe(subscriber); + + long begin = System.nanoTime(); + long elapsed = 0; + while (elapsed < timeout) + { + if (subscriber.getError() != null) + { + TICCoreException exception = new TICCoreException(subscriber.getError().getErrorCode().intValue(), subscriber.getError().getErrorMessage()); + logger.error(exception.getMessage(), exception); + stream.unsubscribe(subscriber); + this.closeNativeStream(identifier, stream, subscriber); + throw exception; + } + if (subscriber.getData() != null) + { + frame = subscriber.getData(); + break; + } + Time.sleep(READ_NEXT_FRAME_POLLING_PERIOD); + elapsed = (System.nanoTime() - begin) / 1000000; + } + stream.unsubscribe(subscriber); + this.closeNativeStream(identifier, stream, subscriber); + if (elapsed >= timeout) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), "Stream " + identifier + " data read timeout !"); + logger.error(exception.getMessage(), exception); + throw exception; + } + return frame; + } + + @Override + public void subscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException + { + TICCoreStream stream = this.findStream(identifier); + + if (stream == null) + { + TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); + if (descriptor == null) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + this.startNewStream(descriptor); + } + try + { + this.eventNotifier.subscribe(identifier, subscriber); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + } + + @Override + public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException + { + TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); + if (descriptor != null) + { + if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) + { + Collection subscribers = this.findSubscribers(identifier, false); + if (subscribers.contains(subscriber) && subscribers.size() == 1) + { + this.stopStream(descriptor); + } + } + } + try + { + this.eventNotifier.unsubscribe(identifier, subscriber); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + } + + @Override + public void subscribe(TICCoreSubscriber subscriber) + { + this.eventNotifier.subscribe(subscriber); + } + + @Override + public void unsubscribe(TICCoreSubscriber subscriber) + { + this.eventNotifier.unsubscribe(subscriber); + } + + @Override + public List getIndentifiers(TICCoreSubscriber listener) + { + List identifiers = new ArrayList(); + + if (this.eventNotifier.getSubscribers(true, false).contains(listener)) + { + identifiers.addAll(this.getAvailableTICs()); + } + else if (this.eventNotifier.getSubscribers(false, true).contains(listener)) + { + identifiers.addAll(this.eventNotifier.getFilters(listener)); + } + + return identifiers; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICCoreSubscriber + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onData(TICCoreFrame frame) + { + logger.trace("TICCore frame:\n" + frame.toString(2)); + Collection subscriberList = this.findSubscribers(frame.getIdentifier(), true); + Task task = new TaskBase() + { + @Override + public void process() + { + TICCoreBase.this.notifyOnData(frame, subscriberList); + } + }; + task.start(); + } + + @Override + public void onError(TICCoreError error) + { + logger.error("TICCore error:\n" + error.toString(2)); + Collection subscriberList = this.findSubscribers(error.getIdentifier(), true); + Task task = new TaskBase() + { + @Override + public void process() + { + TICCoreBase.this.notifyOnError(error, subscriberList); + } + }; + task.start(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// PortPlugSubscriber + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onPlugged(TICPortDescriptor descriptor) + { + logger.info("TICCore modem plugged:\n" + descriptor.toString(2)); + this.startNewStream(descriptor); + } + + @Override + public void onUnplugged(TICPortDescriptor descriptor) + { + logger.info("TICCore modem unplugged:\n" + descriptor.toString(2)); + TICIdentifier identifier = this.stopStream(descriptor); + Task task = new TaskBase() + { + @Override + public void process() + { + TICCoreBase.this.notifyOnUnpluggedAndUnsubscribe(identifier); + } + }; + task.start(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Task + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void start() + { + if (!this.isRunning()) + { + logger.info("Starting TICCore"); + logger.debug("Starting TIC port plug notifier"); + this.plugNotifier = new TICPortPlugNotifier(this.plugNotifierPeriod, this.portFinder); + this.plugNotifier.subscribe(this); + this.plugNotifier.start(); + logger.debug("TIC port plug notifier started"); + if (this.nativePortNamesOnStart != null && this.nativePortNamesOnStart.size() > 0) + { + logger.debug("Starting natives TIC port: " + Arrays.toString(this.nativePortNamesOnStart.toArray())); + for (String portName : this.nativePortNamesOnStart) + { + TICPortDescriptor descriptor = this.portFinder.findNative(portName); + logger.debug("Starting native TIC port " + portName + " : " + descriptor); + if (descriptor != null && this.findStream(descriptor) == null) + { + this.startNewStream(descriptor); + } + } + } + } + } + + @Override + public void stop() + { + logger.info("Stopping TICCore"); + logger.debug("Stopping TIC port plug notifier"); + this.plugNotifier.unsubscribe(this); + this.plugNotifier.stop(); + logger.debug("Stopping all streams"); + for (TICCoreStream stream : this.streamList) + { + stream.unsubscribe(this); + stream.stop(); + } + this.streamList.clear(); + logger.debug("Removing all subscribers"); + this.unsubscribe(this.eventNotifier.getSubscribers()); + } + + @Override + public boolean isRunning() + { + return (this.plugNotifier != null) ? this.plugNotifier.isRunning() : false; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private TICCoreStream findStream(TICIdentifier identifier) + { + for (TICCoreStream stream : this.streamList) + { + if (stream.getIdentifier().matches(identifier)) + { + return stream; + } + } + + return null; + } + + private TICCoreStream findStream(TICPortDescriptor descriptor) + { + for (TICCoreStream stream : this.streamList) + { + TICIdentifier identifier = stream.getIdentifier(); + if (descriptor.getPortId() != null && identifier.getPortId() != null) + { + if (descriptor.getPortId().equals(identifier.getPortId())) + { + return stream; + } + } + if (descriptor.getPortName() != null && identifier.getPortName() != null) + { + if (descriptor.getPortName().equals(identifier.getPortName())) + { + return stream; + } + } + } + + return null; + } + + private TICCoreStream startNewStream(TICPortDescriptor descriptor) + { + TICCoreStream stream = null; + logger.debug("TICCore starting new stream : " + descriptor.toString()); + try + { + stream = this.streamConstructor.newInstance(descriptor.getPortId(), descriptor.getPortName(), this.streamMode); + + stream.subscribe(this); + + stream.start(); + this.streamList.add(stream); + logger.debug("TICCore started new stream : " + descriptor.toString()); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + return stream; + } + + private TICIdentifier stopStream(TICPortDescriptor descriptor) + { + TICIdentifier identifier = null; + TICCoreStream stream = this.findStream(descriptor); + logger.debug("TICCore stopping new stream : " + descriptor.toString()); + if (stream != null) + { + identifier = stream.getIdentifier(); + stream.unsubscribe(this); + stream.stop(); + this.streamList.remove(stream); + logger.debug("TICCore stopped new stream : " + descriptor.toString()); + } + + return identifier; + } + + private void closeNativeStream(TICIdentifier identifier, TICCoreStream stream, ReadNextFrameSubscriber subscriber) + { + TICPortDescriptor nativeDescriptor = this.portFinder.findNative(identifier.getPortName()); + if (nativeDescriptor != null) + { + if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) + { + Collection streamSubscribers = stream.getSubscribers(); + Collection ticCoreSubscribers = this.findSubscribers(identifier, false); + if (!streamSubscribers.contains(subscriber) && streamSubscribers.contains(this) && streamSubscribers.size() == 1 && ticCoreSubscribers.size() == 0) + { + this.stopStream(nativeDescriptor); + } + } + } + } + + private Collection findSubscribers(TICIdentifier sourceIdentifier, boolean globalSubscribers) + { + Predicate predicate = new Predicate() + { + @Override + public boolean test(TICIdentifier identifier) + { + return sourceIdentifier.matches(identifier); + } + }; + + return this.eventNotifier.getSubscribers(predicate, globalSubscribers); + } + + private void notifyOnUnpluggedAndUnsubscribe(TICIdentifier identifier) + { + try + { + TICCoreError error = new TICCoreError(identifier, TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + "TICCore stream " + identifier + " has been unplugged (unsubscribe has been forced)"); + Collection subscriberList = this.findSubscribers(identifier, true); + this.notifyOnError(error, subscriberList); + subscriberList = this.findSubscribers(identifier, false); + this.unsubscribe(subscriberList); + } + catch (DataDictionaryException e) + { + logger.error(e.getMessage(), e); + } + } + + private void notifyOnData(TICCoreFrame frame, Collection subscriberList) + { + for (TICCoreSubscriber subscriber : subscriberList) + { + subscriber.onData(frame); + } + } + + private void notifyOnError(TICCoreError error, Collection subscriberList) + { + for (TICCoreSubscriber subscriber : subscriberList) + { + subscriber.onError(error); + } + } + + private void unsubscribe(Collection subscriberList) + { + for (TICCoreSubscriber subscriber : subscriberList) + { + this.unsubscribe(subscriber); + } + } +} diff --git a/src/main/java/enedis/tic/core/TICCoreError.java b/src/main/java/enedis/tic/core/TICCoreError.java new file mode 100644 index 0000000..2e9ad38 --- /dev/null +++ b/src/main/java/enedis/tic/core/TICCoreError.java @@ -0,0 +1,288 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import enedis.lab.types.datadictionary.KeyDescriptorNumber; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * TICCoreError class + * + * Generated + */ +public class TICCoreError extends DataDictionaryBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_IDENTIFIER = "identifier"; + protected static final String KEY_ERROR_CODE = "errorCode"; + protected static final String KEY_ERROR_MESSAGE = "errorMessage"; + protected static final String KEY_DATA = "data"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kIdentifier; + protected KeyDescriptorNumber kErrorCode; + protected KeyDescriptorString kErrorMessage; + protected KeyDescriptorDataDictionary kData; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected TICCoreError() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICCoreError(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICCoreError(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param identifier + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public TICCoreError(TICIdentifier identifier, Number errorCode, String errorMessage) throws DataDictionaryException + { + this(identifier, errorCode, errorMessage, null); + } + + /** + * Constructor setting parameters to specific values + * + * @param identifier + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public TICCoreError(TICIdentifier identifier, Number errorCode, String errorMessage, DataDictionary data) throws DataDictionaryException + { + this(); + + this.setIdentifier(identifier); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get identifier + * + * @return the identifier + */ + public TICIdentifier getIdentifier() + { + return (TICIdentifier) this.data.get(KEY_IDENTIFIER); + } + + /** + * Get error code + * + * @return the error code + */ + public Number getErrorCode() + { + return (Number) this.data.get(KEY_ERROR_CODE); + } + + /** + * Get error message + * + * @return the error message + */ + public String getErrorMessage() + { + return (String) this.data.get(KEY_ERROR_MESSAGE); + } + + /** + * Get data + * + * @return the data + */ + public DataDictionaryBase getData() + { + return (DataDictionaryBase) this.data.get(KEY_DATA); + } + + /** + * Set identifier + * + * @param identifier + * @throws DataDictionaryException + */ + public void setIdentifier(TICIdentifier identifier) throws DataDictionaryException + { + this.setIdentifier((Object) identifier); + } + + /** + * Set error code + * + * @param errorCode + * @throws DataDictionaryException + */ + public void setErrorCode(Number errorCode) throws DataDictionaryException + { + this.setErrorCode((Object) errorCode); + } + + /** + * Set error message + * + * @param errorMessage + * @throws DataDictionaryException + */ + public void setErrorMessage(String errorMessage) throws DataDictionaryException + { + this.setErrorMessage((Object) errorMessage); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(DataDictionary data) throws DataDictionaryException + { + this.setData((Object) data); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setIdentifier(Object identifier) throws DataDictionaryException + { + this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); + } + + protected void setErrorCode(Object errorCode) throws DataDictionaryException + { + this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); + } + + protected void setErrorMessage(Object errorMessage) throws DataDictionaryException + { + this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); + } + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kIdentifier = new KeyDescriptorDataDictionary(KEY_IDENTIFIER, true, TICIdentifier.class); + this.keys.add(this.kIdentifier); + + this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); + this.keys.add(this.kErrorCode); + + this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, true, false); + this.keys.add(this.kErrorMessage); + + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, DataDictionaryBase.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/core/TICCoreErrorCode.java b/src/main/java/enedis/tic/core/TICCoreErrorCode.java new file mode 100644 index 0000000..548a78f --- /dev/null +++ b/src/main/java/enedis/tic/core/TICCoreErrorCode.java @@ -0,0 +1,33 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +public enum TICCoreErrorCode +{ + NO_ERROR(0), + STREAM_PORT_ID_NOT_FOUND(1), + STREAM_PORT_NAME_NOT_FOUND(2), + STREAM_PORT_DESCRIPTOR_EMPTY(3), + STREAM_MODE_NOT_DEFINED(4), + STREAM_IDENTIFIER_NOT_FOUND(5), + STREAM_UNPLUGGED(6), + DATA_READ_TIMEOUT(7), + OTHER_REASON(99); + + private int code; + + private TICCoreErrorCode(int code) + { + this.code = code; + } + + public int getCode() + { + return this.code; + } +} diff --git a/src/main/java/enedis/tic/core/TICCoreException.java b/src/main/java/enedis/tic/core/TICCoreException.java new file mode 100644 index 0000000..aa0fa67 --- /dev/null +++ b/src/main/java/enedis/tic/core/TICCoreException.java @@ -0,0 +1,76 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.types.ExceptionBase; + +public class TICCoreException extends ExceptionBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -3285641164559292710L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCoreException(int code, String info) + { + super(code, info); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/tic/core/TICCoreFrame.java b/src/main/java/enedis/tic/core/TICCoreFrame.java new file mode 100644 index 0000000..eb6c7f2 --- /dev/null +++ b/src/main/java/enedis/tic/core/TICCoreFrame.java @@ -0,0 +1,283 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; + +/** + * TICCoreFrame class + * + * Generated + */ +public class TICCoreFrame extends DataDictionaryBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_IDENTIFIER = "identifier"; + protected static final String KEY_MODE = "mode"; + protected static final String KEY_CAPTURE_DATE_TIME = "captureDateTime"; + protected static final String KEY_CONTENT = "content"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kIdentifier; + protected KeyDescriptorEnum kMode; + protected KeyDescriptorLocalDateTime kCaptureDateTime; + protected KeyDescriptorDataDictionary kContent; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected TICCoreFrame() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICCoreFrame(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICCoreFrame(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param identifier + * @param mode + * @param captureDateTime + * @param content + * @throws DataDictionaryException + */ + public TICCoreFrame(TICIdentifier identifier, TICMode mode, LocalDateTime captureDateTime, DataDictionaryBase content) throws DataDictionaryException + { + this(); + + this.setIdentifier(identifier); + this.setMode(mode); + this.setCaptureDateTime(captureDateTime); + this.setContent(content); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get identifier + * + * @return the identifier + */ + public TICIdentifier getIdentifier() + { + return (TICIdentifier) this.data.get(KEY_IDENTIFIER); + } + + /** + * Get mode + * + * @return the mode + */ + public TICMode getMode() + { + return (TICMode) this.data.get(KEY_MODE); + } + + /** + * Get capture date time + * + * @return the capture date time + */ + public LocalDateTime getCaptureDateTime() + { + return (LocalDateTime) this.data.get(KEY_CAPTURE_DATE_TIME); + } + + /** + * Get content + * + * @return the content + */ + public DataDictionaryBase getContent() + { + return (DataDictionaryBase) this.data.get(KEY_CONTENT); + } + + /** + * Set identifier + * + * @param identifier + * @throws DataDictionaryException + */ + public void setIdentifier(TICIdentifier identifier) throws DataDictionaryException + { + this.setIdentifier((Object) identifier); + } + + /** + * Set mode + * + * @param mode + * @throws DataDictionaryException + */ + public void setMode(TICMode mode) throws DataDictionaryException + { + this.setMode((Object) mode); + } + + /** + * Set capture date time + * + * @param captureDateTime + * @throws DataDictionaryException + */ + public void setCaptureDateTime(LocalDateTime captureDateTime) throws DataDictionaryException + { + this.setCaptureDateTime((Object) captureDateTime); + } + + /** + * Set content + * + * @param content + * @throws DataDictionaryException + */ + public void setContent(DataDictionaryBase content) throws DataDictionaryException + { + this.setContent((Object) content); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setIdentifier(Object identifier) throws DataDictionaryException + { + this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); + } + + protected void setMode(Object mode) throws DataDictionaryException + { + this.data.put(KEY_MODE, this.kMode.convert(mode)); + } + + protected void setCaptureDateTime(Object captureDateTime) throws DataDictionaryException + { + this.data.put(KEY_CAPTURE_DATE_TIME, this.kCaptureDateTime.convert(captureDateTime)); + } + + protected void setContent(Object content) throws DataDictionaryException + { + this.data.put(KEY_CONTENT, this.kContent.convert(content)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kIdentifier = new KeyDescriptorDataDictionary(KEY_IDENTIFIER, true, TICIdentifier.class); + this.keys.add(this.kIdentifier); + + this.kMode = new KeyDescriptorEnum(KEY_MODE, true, TICMode.class); + this.keys.add(this.kMode); + + this.kCaptureDateTime = new KeyDescriptorLocalDateTime(KEY_CAPTURE_DATE_TIME, true); + this.keys.add(this.kCaptureDateTime); + + this.kContent = new KeyDescriptorDataDictionary(KEY_CONTENT, true, DataDictionaryBase.class); + this.keys.add(this.kContent); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/core/TICCoreStream.java b/src/main/java/enedis/tic/core/TICCoreStream.java new file mode 100644 index 0000000..8a72a48 --- /dev/null +++ b/src/main/java/enedis/tic/core/TICCoreStream.java @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.util.task.Notifier; +import enedis.lab.util.task.Task; + +/** + * TICCore stream interface + */ +public interface TICCoreStream extends Notifier, Task +{ + /** + * Get identifier + * + * @return The TICIdentifier associated with the stream + */ + public TICIdentifier getIdentifier(); +} diff --git a/src/main/java/enedis/tic/core/TICCoreStreamBase.java b/src/main/java/enedis/tic/core/TICCoreStreamBase.java new file mode 100644 index 0000000..21ff386 --- /dev/null +++ b/src/main/java/enedis/tic/core/TICCoreStreamBase.java @@ -0,0 +1,378 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.time.LocalDateTime; +import java.util.Collection; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import enedis.lab.io.channels.Channel; +import enedis.lab.io.channels.ChannelException; +import enedis.lab.io.datastreams.DataStreamBase; +import enedis.lab.io.datastreams.DataStreamDirection; +import enedis.lab.io.datastreams.DataStreamException; +import enedis.lab.io.datastreams.DataStreamListener; +import enedis.lab.io.datastreams.DataStreamStatus; +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.io.tic.TICPortFinderBase; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.protocol.tic.channels.ChannelTICSerialPort; +import enedis.lab.protocol.tic.channels.ChannelTICSerialPortConfiguration; +import enedis.lab.protocol.tic.datastreams.TICInputStream; +import enedis.lab.protocol.tic.datastreams.TICStreamConfiguration; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; +import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.util.task.Notifier; +import enedis.lab.util.task.NotifierBase; +import enedis.lab.util.task.Task; + +/** + * TICCore stream implementation + */ +public class TICCoreStreamBase implements TICCoreStream, Task, DataStreamListener +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private TICIdentifier identifier; + private DataStreamBase stream; + private Channel channel; + private Notifier notifier; + static private Logger logger = LogManager.getLogger(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCoreStreamBase(String portId, String portName, TICMode ticMode) throws TICCoreException + { + TICPortDescriptor descriptor = null; + + if (portId != null) + { + descriptor = TICPortFinderBase.getInstance().findByPortId(portId); + if (descriptor == null) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_ID_NOT_FOUND.getCode(), "TICCore stream port id " + portId + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + } + else if (portName != null) + { + descriptor = TICPortFinderBase.getInstance().findByPortName(portName); + if (descriptor == null) + { + descriptor = TICPortFinderBase.getInstance().findNative(portName); + if (descriptor == null) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_NAME_NOT_FOUND.getCode(), + "TICCore stream port name " + portName + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + } + } + else + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_DESCRIPTOR_EMPTY.getCode(), "TICCore stream port descriptor empty!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + if (ticMode == null) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_MODE_NOT_DEFINED.getCode(), "TICCore stream mode not defined!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + try + { + this.identifier = new TICIdentifier(portId, portName, null); + this.channel = new ChannelTICSerialPort(new ChannelTICSerialPortConfiguration("Channel@" + descriptor.getPortName(), portName, ticMode)); + this.stream = new TICInputStream( + new TICStreamConfiguration("Stream@" + descriptor.getPortName(), DataStreamDirection.INPUT, "Channel@" + descriptor.getPortName(), ticMode)); + this.stream.setChannel(this.channel); + this.notifier = new NotifierBase(); + logger = LogManager.getLogger(); + } + catch (DataDictionaryException | ChannelException | DataStreamException e) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.OTHER_REASON.getCode(), "TICCore stream instanciation failed : " + e.getMessage()); + logger.error(exception.getMessage(), exception); + throw exception; + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICCoreStream + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public TICIdentifier getIdentifier() + { + TICIdentifier identifier; + + synchronized (this.identifier) + { + identifier = (TICIdentifier) this.identifier.clone(); + } + + return identifier; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Notifier + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Collection getSubscribers() + { + return this.notifier.getSubscribers(); + } + + @Override + public boolean hasSubscriber(TICCoreSubscriber subscriber) + { + return this.notifier.hasSubscriber(subscriber); + } + + @Override + public void subscribe(TICCoreSubscriber subscriber) + { + this.notifier.subscribe(subscriber); + } + + @Override + public void unsubscribe(TICCoreSubscriber subscriber) + { + this.notifier.unsubscribe(subscriber); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataStreamListener + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onDataReceived(String dataStreamName, DataDictionary data) + { + TICCoreFrame frame = this.createFrame(data); + + if (frame != null) + { + this.notifyOnData(frame); + } + else + { + logger.debug("Frame creation skipped due to null TICFrame"); + } + } + + @Override + public void onDataSent(String dataStreamName, DataDictionary data) + { + } + + @Override + public void onStatusChanged(String dataStreamName, DataStreamStatus newStatus) + { + } + + @Override + public void onErrorDetected(String dataStreamName, int errorCode, String errorMessage, DataDictionary data) + { + TICCoreError error = this.createError(errorCode, errorMessage, data); + this.notifyOnError(error); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Task + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void start() + { + this.channel.start(); + try + { + this.stream.open(); + this.stream.subscribe(this); + } + catch (DataStreamException e) + { + logger.error(e.getMessage()); + } + } + + @Override + public void stop() + { + this.stream.unsubscribe(this); + try + { + this.stream.close(); + } + catch (DataStreamException e) + { + logger.error(e.getMessage()); + } + this.channel.stop(); + } + + @Override + public boolean isRunning() + { + return this.channel.isRunning(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private TICCoreFrame createFrame(DataDictionary data) + { + TICCoreFrame frame = new TICCoreFrame(); + try + { + frame.setCaptureDateTime(LocalDateTime.now()); + TICFrame ticFrame = (TICFrame) data.get(TICInputStream.KEY_DATA); + + if (ticFrame == null) + { + logger.warn("TICFrame is null. Skipping frame creation."); + return null; + } + + DataDictionaryBase content = new DataDictionaryBase(); + for (TICFrameDataSet frameDataSet : ticFrame.getDataSetList()) + { + String label = frameDataSet.getLabel(); + content.set(label, ticFrame.getData(label)); + } + frame.setContent(content); + String serialNumber = null; + if (ticFrame instanceof TICFrameStandard) + { + frame.setMode(TICMode.STANDARD); + serialNumber = (String) content.get("ADSC"); + } + else if (ticFrame instanceof TICFrameHistoric) + { + frame.setMode(TICMode.HISTORIC); + serialNumber = (String) content.get("ADCO"); + } + synchronized (this.identifier) + { + this.identifier.setSerialNumber(serialNumber); + frame.setIdentifier(this.identifier.clone()); + } + } + catch (DataDictionaryException e) + { + logger.error(e.getMessage()); + frame = null; + } + + return frame; + } + + private TICCoreError createError(int errorCode, String errorMessage, DataDictionary data) + { + TICCoreError error = null; + try + { + error = new TICCoreError(this.getIdentifier(), errorCode, errorMessage, data); + } + catch (DataDictionaryException e) + { + logger.error(e.getMessage()); + } + + return error; + } + + private void notifyOnData(TICCoreFrame frame) + { + if (frame == null) + { + return; + } + for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) + { + subscriber.onData(frame); + } + } + + private void notifyOnError(TICCoreError error) + { + if (error == null) + { + return; + } + for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) + { + subscriber.onError(error); + } + } +} diff --git a/src/main/java/enedis/tic/core/TICCoreSubscriber.java b/src/main/java/enedis/tic/core/TICCoreSubscriber.java new file mode 100644 index 0000000..4adc14e --- /dev/null +++ b/src/main/java/enedis/tic/core/TICCoreSubscriber.java @@ -0,0 +1,32 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.util.task.Subscriber; + +/** + * TICCore subscriber interface + */ +public interface TICCoreSubscriber extends Subscriber +{ + /** + * Notify when data + * + * @param frame + * the frame received + */ + public void onData(TICCoreFrame frame); + + /** + * Notify when error + * + * @param error + * the error detected + */ + public void onError(TICCoreError error); +} diff --git a/src/main/java/enedis/tic/core/TICIdentifier.java b/src/main/java/enedis/tic/core/TICIdentifier.java new file mode 100644 index 0000000..7f83c81 --- /dev/null +++ b/src/main/java/enedis/tic/core/TICIdentifier.java @@ -0,0 +1,282 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * TICIdentifier class + * + * Generated + */ +public class TICIdentifier extends DataDictionaryBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_PORT_ID = "portId"; + protected static final String KEY_PORT_NAME = "portName"; + protected static final String KEY_SERIAL_NUMBER = "serialNumber"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorString kPortId; + protected KeyDescriptorString kPortName; + protected KeyDescriptorString kSerialNumber; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected TICIdentifier() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICIdentifier(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICIdentifier(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param portId + * @param portName + * @param serialNumber + * @throws DataDictionaryException + */ + public TICIdentifier(String portId, String portName, String serialNumber) throws DataDictionaryException + { + this(); + + this.setPortId((Object) portId); + this.setPortName((Object) portName); + this.setSerialNumber((Object) serialNumber); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (this.getPortId() == null && this.getPortName() == null && this.getSerialNumber() == null) + { + throw new DataDictionaryException("Empty TICIdentifier not allowed!"); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public boolean matches(TICIdentifier identifier) + { + if (identifier != null) + { + if (this.getSerialNumber() != null && identifier.getSerialNumber() != null) + { + return this.getSerialNumber().equals(identifier.getSerialNumber()); + } + if (this.getPortId() != null && identifier.getPortId() != null) + { + return this.getPortId().equals(identifier.getPortId()); + } + if (this.getPortName() != null && identifier.getPortName() != null) + { + return this.getPortName().equals(identifier.getPortName()); + } + } + + return false; + } + + /** + * Get port id + * + * @return the port id + */ + public String getPortId() + { + return (String) this.data.get(KEY_PORT_ID); + } + + /** + * Get port name + * + * @return the port name + */ + public String getPortName() + { + return (String) this.data.get(KEY_PORT_NAME); + } + + /** + * Get serial number + * + * @return the serial number + */ + public String getSerialNumber() + { + return (String) this.data.get(KEY_SERIAL_NUMBER); + } + + /** + * Set port id + * + * @param portId + * @throws DataDictionaryException + */ + public void setPortId(String portId) throws DataDictionaryException + { + if (portId == null && this.getPortName() == null && this.getSerialNumber() == null) + { + throw new DataDictionaryException("Cannot set null portId because empty TICIdentifier not allowed!"); + } + this.setPortId((Object) portId); + } + + /** + * Set port name + * + * @param portName + * @throws DataDictionaryException + */ + public void setPortName(String portName) throws DataDictionaryException + { + if (this.getPortId() == null && portName == null && this.getSerialNumber() == null) + { + throw new DataDictionaryException("Cannot set null portName because empty TICIdentifier not allowed!"); + } + this.setPortName((Object) portName); + } + + /** + * Set serial number + * + * @param serialNumber + * @throws DataDictionaryException + */ + public void setSerialNumber(String serialNumber) throws DataDictionaryException + { + if (this.getPortId() == null && this.getPortName() == null && serialNumber == null) + { + throw new DataDictionaryException("Cannot set null serialNumber because empty TICIdentifier not allowed!"); + } + this.setSerialNumber((Object) serialNumber); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setPortId(Object portId) throws DataDictionaryException + { + this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); + } + + protected void setPortName(Object portName) throws DataDictionaryException + { + this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); + } + + protected void setSerialNumber(Object serialNumber) throws DataDictionaryException + { + this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); + this.keys.add(this.kPortId); + + this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); + this.keys.add(this.kPortName); + + this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, false); + this.keys.add(this.kSerialNumber); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java b/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java new file mode 100644 index 0000000..82dcd19 --- /dev/null +++ b/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java @@ -0,0 +1,424 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service; + +import java.io.File; +import java.io.InputStream; +import java.util.Properties; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; + +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.tic.core.TICCore; +import enedis.tic.core.TICCoreBase; +import enedis.tic.service.client.TIC2WebSocketClientPool; +import enedis.tic.service.client.TIC2WebSocketClientPoolBase; +import enedis.tic.service.config.TIC2WebSocketConfiguration; +import enedis.tic.service.netty.TIC2WebSocketServer; +import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; +import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandlerBase; +import picocli.CommandLine; +import picocli.CommandLine.IVersionProvider; +import picocli.CommandLine.ParseResult; + +/** + * Class used to handle the application + */ +public class TIC2WebSocketApplication +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Project properties (see file project.properties) + */ + public static final Properties PROJECT_PROPERTIES = new Properties(); + + static + { + try + { + InputStream stream = TIC2WebSocketApplication.class.getClassLoader().getResourceAsStream("TIC2WebSocket.properties"); + + if (stream == null) + { + System.err.println("Can't find projet property file!"); + } + else + { + TIC2WebSocketApplication.PROJECT_PROPERTIES.load(stream); + } + } + catch (Exception exception) + { + System.err.println("Can't load projet property file!"); + exception.printStackTrace(System.err); + } + } + + /** + * Project name ("project_name" from project.properties) + */ + public static final String NAME = PROJECT_PROPERTIES.getProperty("project_name", ""); + + /** + * Project version ("project_version" from project.properties) + */ + public static final String VERSION = PROJECT_PROPERTIES.getProperty("project_version", ""); + + /** + * Project description ("project_description" from project.properties) + */ + public static final String DESCRIPTION = PROJECT_PROPERTIES.getProperty("project_description", ""); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Program entry point + * + * @param args + * Command line arguments + */ + public static void main(String[] args) + { + try + { + TIC2WebSocketApplication application = new TIC2WebSocketApplication(args); + int result = application.run(); + System.exit(result); + } + catch (Exception exception) + { + exception.printStackTrace(System.err); + System.exit(-1); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private String[] commandLineArgs; + private CommandLine commandLineParser; + private TIC2WebSocketCommandLine commandLine; + private Logger logger; + private boolean started; + private TIC2WebSocketConfiguration configuration; + + private TIC2WebSocketClientPool clientPool; + private TIC2WebSocketServer server; + private TIC2WebSocketRequestHandler requestHandler; + private TICCore ticCore; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Application constructor + * + * @param args + * Command line arguments + */ + public TIC2WebSocketApplication(String[] args) + { + this.commandLineArgs = args; + this.commandLine = new TIC2WebSocketCommandLine(); + this.commandLineParser = new CommandLine(this.commandLine); + this.commandLineParser.setCommandName(TIC2WebSocketApplication.NAME); + this.commandLineParser.getCommandSpec().versionProvider(new IVersionProvider() + { + @Override + public String[] getVersion() throws Exception + { + return new String[] { TIC2WebSocketApplication.NAME + " v" + TIC2WebSocketApplication.VERSION }; + } + }); + this.commandLineParser.registerConverter(Level.class, new TIC2WebSocketCommandLine.VerboseLevelConverter()); + this.updateLoggerConfiguration(Level.ERROR); + this.started = false; + } + + /** + * Application execution + * + * @return 0 if success, else an error code + * @throws InterruptedException + * If the application thread gets interrupted + * @see TIC2WebSocketApplicationErrorCode + */ + public int run() throws InterruptedException + { + int result = this.init(); + + if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) + { + return result; + } + result = this.start(); + if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) + { + return result; + } + + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() + { + @Override + public void run() + { + TIC2WebSocketApplication.this.stop(); + } + })); + + while (this.isStarted()) + { + Thread.sleep(1000); + } + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + /** + * Application initialization + * + * @return 0 if success, else an error code + * @see TIC2WebSocketApplicationErrorCode + */ + public int init() + { + if (this.isStarted()) + { + this.logger.error("Application already started!"); + return TIC2WebSocketApplicationErrorCode.APPLICATION_ALREADY_STARTED.code(); + } + int result = this.parseCommandLine(); + if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) + { + return result; + } + result = this.updateLoggerConfiguration(this.commandLine.verboseLevel); + if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) + { + return result; + } + + result = this.loadConfiguration(); + if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) + { + return result; + } + + this.logger.info(TIC2WebSocketApplication.NAME + " initialized"); + + this.ticCore = new TICCoreBase(this.configuration.getTicMode(), this.configuration.getTicPortNames()); + this.clientPool = new TIC2WebSocketClientPoolBase(); + this.requestHandler = new TIC2WebSocketRequestHandlerBase(this.ticCore); + + this.server = new TIC2WebSocketServer("localhost", this.configuration.getServerPort().intValue(), + this.clientPool, this.requestHandler); + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + /** + * Application start-up + * + * @return 0 if success, else an error code + */ + public int start() + { + if (this.commandLineParser.isUsageHelpRequested()) + { + this.commandLineParser.usage(System.out); + } + if (this.commandLineParser.isVersionHelpRequested()) + { + this.commandLineParser.printVersionHelp(System.out); + } + this.logger.info(TIC2WebSocketApplication.NAME + " starting"); + + try + { + this.ticCore.start(); + this.server.start(); + } + catch (Exception exception) + { + this.logger.error(exception.getMessage(), exception); + return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); + } + + this.logger.info(TIC2WebSocketApplication.NAME + " started"); + this.started = true; + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + /** + * Application start-up indicator + * + * @return true if the application is started, false otherwise + */ + public boolean isStarted() + { + return this.started; + } + + /** + * Application stop + * + * @return 0 if success, else an error code + * @see TIC2WebSocketApplicationErrorCode + */ + public int stop() + { + if (!this.isStarted()) + { + this.logger.error("Application not started!"); + return TIC2WebSocketApplicationErrorCode.APPLICATION_NOT_STARTED.code(); + } + + try + { + this.server.stop(); + this.ticCore.stop(); + } + catch (Exception exception) + { + this.logger.error(exception.getMessage(), exception); + return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); + } + + this.logger.info(TIC2WebSocketApplication.NAME + " stopped"); + this.started = false; + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private int parseCommandLine() + { + try + { + ParseResult parseResult = this.commandLineParser.parseArgs(this.commandLineArgs); + if (parseResult.errors().size() > 0) + { + this.logger.error("Invalid command line!"); + for (int i = 0; i < parseResult.errors().size(); i++) + { + this.logger.error(parseResult.errors().get(i).getMessage()); + } + return TIC2WebSocketApplicationErrorCode.COMMAND_LINE_INVALID.code(); + } + } + catch (Exception exception) + { + this.logger.error("Invalid command line!"); + this.logger.error(exception.getMessage()); + return TIC2WebSocketApplicationErrorCode.COMMAND_LINE_INVALID.code(); + } + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + private int updateLoggerConfiguration(Level level) + { + try + { + System.setProperty("log4j.configurationFile", "log4j2-" + level.name().toLowerCase() + ".xml"); + LoggerContext context = (LoggerContext) LogManager.getContext(false); + context.reconfigure(); + this.logger = LogManager.getLogger(); + } + catch (Exception exception) + { + System.err.println("Log configuration update failure!"); + System.err.println(exception.getMessage()); + return TIC2WebSocketApplicationErrorCode.UPDATE_LOGGER_CONFIGURATION_FAILURE.code(); + } + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + private int loadConfiguration() + { + String configFile = null; + + if (this.commandLine.configFile != null) + { + configFile = this.commandLine.configFile; + } + else + { + configFile = System.getProperty("configFile", NAME + "Configuration.json"); + } + File configPath = new File(configFile); + if (!configPath.exists()) + { + this.logger.error("No configuration file path '" + configPath.getAbsolutePath() + "' not found"); + return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); + } + try + { + this.logger.info("Loading configuration file " + configPath); + this.configuration = (TIC2WebSocketConfiguration) DataDictionaryBase.fromFile(configPath, TIC2WebSocketConfiguration.class); + } + catch (Exception exception) + { + this.logger.error("Loading configuration file " + configPath + " failed", exception); + return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); + } + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + + } +} \ No newline at end of file diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java b/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java new file mode 100644 index 0000000..c2b970c --- /dev/null +++ b/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java @@ -0,0 +1,45 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service; + +/** + * Application error codes list + */ +public enum TIC2WebSocketApplicationErrorCode +{ + /** Success code */ + NO_ERROR(0), + /** Command line invalid code */ + COMMAND_LINE_INVALID(1), + /** Update logger configuration code */ + UPDATE_LOGGER_CONFIGURATION_FAILURE(2), + /** Application already started code */ + APPLICATION_ALREADY_STARTED(3), + /** Application not started code */ + APPLICATION_NOT_STARTED(4), + /** Load configuration failure code */ + LOAD_CONFIGURATION_FAILURE(5); + + private int code; + + private TIC2WebSocketApplicationErrorCode(int code) + { + this.code = code; + } + + /** + * Return error code + * + * @return code + */ + public int code() + { + return this.code; + } + +} diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java b/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java new file mode 100644 index 0000000..ac5474b --- /dev/null +++ b/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java @@ -0,0 +1,140 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service; + +import org.apache.logging.log4j.Level; + +import picocli.CommandLine.Command; +import picocli.CommandLine.ITypeConverter; +import picocli.CommandLine.Option; +import picocli.CommandLine.TypeConversionException; + +/** + * Class used for command line + */ +@Command() +public class TIC2WebSocketCommandLine +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + static class VerboseLevelConverter implements ITypeConverter + { + @Override + public Level convert(String value) throws Exception + { + try + { + switch (Integer.parseInt(value)) + { + case 0: + return Level.OFF; + case 1: + return Level.FATAL; + case 2: + return Level.ERROR; + case 3: + return Level.WARN; + case 4: + return Level.INFO; + case 5: + return Level.DEBUG; + case 6: + return Level.TRACE; + default: + throw new TypeConversionException(value); + } + } + catch (NumberFormatException exception) + { + Level level = Level.getLevel(value); + if (level == null) + { + throw new TypeConversionException(value); + } + return level; + } + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Option(names = { "-h", "--help" }, usageHelp = true, description = "Display help") + boolean helpRequested = false; + + @Option(names = { "--version" }, versionHelp = true, description = "Display version") + boolean versionRequested = false; + + //@formatter:off + @Option(names = { "-v", + "--verbose" }, defaultValue = "2", paramLabel = "LEVEL", + description = "Define verbosity level:\n" + + "0 ou OFF = muted\n" + + "1 ou FATAL = critical errors\n" + + "2 ou ERROR = error (default)\n" + + "3 ou WARN = warnings\n" + + "4 ou INFO = informations\n" + + "5 ou DEBUG = debugging\n" + + "6 ou TRACE = traces\n") + Level verboseLevel; + //@formatter:on + + @Option(names = { "--configFile" }, paramLabel = "PATH", description = "Set configuration file") + String configFile = null; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}; diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java new file mode 100644 index 0000000..b45040a --- /dev/null +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java @@ -0,0 +1,141 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.client; + +import java.time.LocalDateTime; + +import io.netty.channel.Channel; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.message.Event; +import enedis.tic.core.TICCoreError; +import enedis.tic.core.TICCoreFrame; +import enedis.tic.core.TICCoreSubscriber; +import enedis.tic.service.endpoint.EventSender; +import enedis.tic.service.message.EventOnError; +import enedis.tic.service.message.EventOnTICData; + +/** + * TIC2WebSocket client + */ +public class TIC2WebSocketClient implements TICCoreSubscriber +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Logger logger; + private Channel channel; + private EventSender eventSender; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor + * + * @param channel + * @param eventSender + */ + public TIC2WebSocketClient(Channel channel, EventSender eventSender) { + super(); + this.logger = LogManager.getLogger(this.getClass()); + this.channel = channel; + this.eventSender = eventSender; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICCoreSubscriber + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onData(TICCoreFrame frame) + { + try + { + Event event = new EventOnTICData(LocalDateTime.now(), frame); + this.eventSender.sendEvent(this.channel, event); + } + catch (DataDictionaryException e) + { + this.logger.error(e.getMessage(), e); + } + } + + @Override + public void onError(TICCoreError error) + { + try + { + Event event = new EventOnError(LocalDateTime.now(), error); + this.eventSender.sendEvent(this.channel, event); + } + catch (DataDictionaryException e) + { + this.logger.error(e.getMessage(), e); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get client websocket channel + * + * @return websocket channel + */ + public Channel getChannel() + { + return this.channel; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java new file mode 100644 index 0000000..1518ae4 --- /dev/null +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java @@ -0,0 +1,52 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.client; + +import java.util.Optional; + +import io.netty.channel.Channel; + +import enedis.tic.service.endpoint.EventSender; + +/** + * TIC2WebSocket client pool interface + */ +public interface TIC2WebSocketClientPool +{ + /** + * Get client from channel id + * + * @param channelId + * @return client + */ + public Optional getClient(String channelId); + + /** + * Check if a client with the given channel id exists + * + * @param channelId + * @return true if a client with the given channel id exists + */ + public boolean exists(String channelId); + + /** + * Create a new client + * + * @param channel + * @param sender + * @return new client + */ + public TIC2WebSocketClient createClient(Channel channel, EventSender sender); + + /** + * Remove client with the given channel id + * + * @param channelId + */ + public void remove(String channelId); +} diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java new file mode 100644 index 0000000..7cf68db --- /dev/null +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java @@ -0,0 +1,140 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.client; + +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import io.netty.channel.Channel; + +import enedis.tic.service.endpoint.EventSender; + +/** + * Client pool + */ +public class TIC2WebSocketClientPoolBase implements TIC2WebSocketClientPool +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Set clients; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + */ + public TIC2WebSocketClientPoolBase() + { + super(); + this.clients = new CopyOnWriteArraySet(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Optional getClient(String channelId) + { + // @formatter:off + return this.clients.stream() + .filter(c -> c.getChannel().id().asLongText().equals(channelId)) + .findAny(); + // @formatter:on + } + + @Override + public boolean exists(String channelId) + { + return this.getClient(channelId).isPresent(); + } + + @Override + public TIC2WebSocketClient createClient(Channel channel, EventSender sender) + { + this.checkArguments(channel, sender); + + String channelId = channel.id().asLongText(); + Optional client = this.getClient(channelId); + if (!client.isPresent()) + { + TIC2WebSocketClient newClient = new TIC2WebSocketClient(channel, sender); + this.clients.add(newClient); + return newClient; + } + else + { + return client.get(); + } + } + + @Override + public void remove(String channelId) + { + Optional client = this.getClient(channelId); + if (client.isPresent()) + { + this.clients.remove(client.get()); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void checkArguments(Channel channel, EventSender sender) + { + if (channel == null || sender == null) { + throw new IllegalArgumentException("Cannot create client with a null Channel or null EventSender"); + } + } +} diff --git a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java new file mode 100644 index 0000000..6fa8e44 --- /dev/null +++ b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java @@ -0,0 +1,275 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.config; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.configuration.ConfigurationBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import enedis.lab.types.datadictionary.KeyDescriptorListMinMaxSize; +import enedis.lab.types.datadictionary.KeyDescriptorNumberMinMax; + +/** + * TIC2WebSocketConfiguration class + * + * Generated + */ +public class TIC2WebSocketConfiguration extends ConfigurationBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_SERVER_PORT = "serverPort"; + protected static final String KEY_TIC_MODE = "ticMode"; + protected static final String KEY_TIC_PORT_NAMES = "ticPortNames"; + + private static final Number SERVER_PORT_MIN = 1; + private static final Number SERVER_PORT_MAX = 65535; + private static final int TIC_PORT_NAMES_MIN_SIZE = 1; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorNumberMinMax kServerPort; + protected KeyDescriptorEnum kTicMode; + protected KeyDescriptorListMinMaxSize kTicPortNames; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected TIC2WebSocketConfiguration() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TIC2WebSocketConfiguration(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TIC2WebSocketConfiguration(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * + * Constructor setting configuration name/file and parameters to default values + * + * @param name + * the configuration name + * @param file + * the configuration file + */ + public TIC2WebSocketConfiguration(String name, File file) + { + this(); + this.init(name, file); + } + + /** + * Constructor setting parameters to specific values + * + * @param serverPort + * @param ticMode + * @param ticPortNames + * @throws DataDictionaryException + */ + public TIC2WebSocketConfiguration(Number serverPort, TICMode ticMode, List ticPortNames) throws DataDictionaryException + { + this(); + + this.setServerPort(serverPort); + this.setTicMode(ticMode); + this.setTicPortNames(ticPortNames); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get server port + * + * @return the server port + */ + public Number getServerPort() + { + return (Number) this.data.get(KEY_SERVER_PORT); + } + + /** + * Get tic mode + * + * @return the tic mode + */ + public TICMode getTicMode() + { + return (TICMode) this.data.get(KEY_TIC_MODE); + } + + /** + * Get tic port names + * + * @return the tic port names + */ + @SuppressWarnings("unchecked") + public List getTicPortNames() + { + return (List) this.data.get(KEY_TIC_PORT_NAMES); + } + + /** + * Set server port + * + * @param serverPort + * @throws DataDictionaryException + */ + public void setServerPort(Number serverPort) throws DataDictionaryException + { + this.setServerPort((Object) serverPort); + } + + /** + * Set tic mode + * + * @param ticMode + * @throws DataDictionaryException + */ + public void setTicMode(TICMode ticMode) throws DataDictionaryException + { + this.setTicMode((Object) ticMode); + } + + /** + * Set tic port names + * + * @param ticPortNames + * @throws DataDictionaryException + */ + public void setTicPortNames(List ticPortNames) throws DataDictionaryException + { + this.setTicPortNames((Object) ticPortNames); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setServerPort(Object serverPort) throws DataDictionaryException + { + this.data.put(KEY_SERVER_PORT, this.kServerPort.convert(serverPort)); + } + + protected void setTicMode(Object ticMode) throws DataDictionaryException + { + this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); + } + + protected void setTicPortNames(Object ticPortNames) throws DataDictionaryException + { + List portNames = this.kTicPortNames.convert(ticPortNames); + for (int i = 0; i < portNames.size(); i++) + { + String portName = portNames.get(i); + if (portName == null) + { + throw new DataDictionaryException("Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + "(" + portName + ") cannot be null"); + } + else if (portName.isEmpty()) + { + throw new DataDictionaryException("Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + "(" + portName + ") cannot be empty"); + } + else if (i != portNames.lastIndexOf(portName)) + { + throw new DataDictionaryException( + "Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + " and " + portNames.lastIndexOf(portName) + "(" + portName + ") are identical"); + } + } + this.data.put(KEY_TIC_PORT_NAMES, portNames); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kServerPort = new KeyDescriptorNumberMinMax(KEY_SERVER_PORT, true, SERVER_PORT_MIN, SERVER_PORT_MAX); + this.keys.add(this.kServerPort); + + this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, false, TICMode.class); + this.keys.add(this.kTicMode); + + this.kTicPortNames = new KeyDescriptorListMinMaxSize(KEY_TIC_PORT_NAMES, false, String.class, TIC_PORT_NAMES_MIN_SIZE, null); + this.keys.add(this.kTicPortNames); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/endpoint/EventSender.java b/src/main/java/enedis/tic/service/endpoint/EventSender.java new file mode 100644 index 0000000..2437707 --- /dev/null +++ b/src/main/java/enedis/tic/service/endpoint/EventSender.java @@ -0,0 +1,26 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.endpoint; + +import io.netty.channel.Channel; + +import enedis.lab.util.message.Event; + +/** + * Event sender interface + */ +public interface EventSender +{ + /** + * Send event + * + * @param channel + * @param event + */ + public void sendEvent(Channel channel, Event event); +} diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java new file mode 100644 index 0000000..5eca674 --- /dev/null +++ b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java @@ -0,0 +1,57 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.endpoint; + +/** + * End point error codes list + */ +public enum TIC2WebSocketEndPointErrorCode +{ + /** Success code */ + NO_ERROR(0), + /** Unvalid message */ + INVALID_MESSAGE_FORMAT(-1), + /** Message type missing */ + MESSAGE_TYPE_MISSING(-2), + /** Message name missing */ + MESSAGE_NAME_MISSING(-3), + /** Message type invalid */ + MESSAGE_TYPE_INVALID(-4), + /** Unsupported message */ + UNSUPPORTED_MESSAGE(-5), + /** Invalid messge content */ + INVALID_MESSAGE_CONTENT(-6), + /** Internal error */ + INTERNAL_ERROR(-7), + /** Subscription fail */ + SUBSCRIPTION_FAIL(-10), + /** Unsubscription fail */ + UNSUBSCRIPTION_FAIL(-11), + /** TIC identifier not found */ + IDENTIFIER_NOT_FOUND(-12), + /** Read TIC timeout */ + READ_TIMEOUT(-13); + + private int value; + + private TIC2WebSocketEndPointErrorCode(int value) + { + this.value = value; + } + + /** + * Return error code + * + * @return code + */ + public int value() + { + return this.value; + } + +} diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java new file mode 100644 index 0000000..4e7b38c --- /dev/null +++ b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java @@ -0,0 +1,140 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.endpoint; + +/** + * @author Antoine + * + */ +/** + * TIC2WebSocket end point Exception + */ +public class TIC2WebSocketEndPointException extends Exception +{ + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final long serialVersionUID = -2263755971102386572L; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private TIC2WebSocketEndPointErrorCode code; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param code + */ + public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code) + { + super(); + this.code = code; + } + + /** + * Constructor using message and cause + * + * @param code + * + * @param message + * @param cause + */ + public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, String message, Throwable cause) + { + super(message, cause); + this.code = code; + } + + /** + * Constructor using message + * + * @param code + * + * @param message + */ + public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, String message) + { + super(message); + this.code = code; + } + + /** + * Constructor using cause + * + * @param code + * + * @param cause + */ + public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, Throwable cause) + { + super(cause); + this.code = code; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get error code + * + * @return error code + */ + public TIC2WebSocketEndPointErrorCode getCode() + { + return this.code; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/tic/service/message/EventOnError.java b/src/main/java/enedis/tic/service/message/EventOnError.java new file mode 100644 index 0000000..da39693 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/EventOnError.java @@ -0,0 +1,192 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import enedis.lab.util.message.Event; +import enedis.tic.core.TICCoreError; + +/** + * EventOnError class + * + * Generated + */ +public class EventOnError extends Event +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "OnError"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected EventOnError() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public EventOnError(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public EventOnError(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param data + * @throws DataDictionaryException + */ + public EventOnError(LocalDateTime dateTime, TICCoreError data) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setData(data); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Event + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get data + * + * @return the data + */ + public TICCoreError getData() + { + return (TICCoreError) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICCoreError data) throws DataDictionaryException + { + this.setData((Object) data); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreError.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/EventOnTICData.java b/src/main/java/enedis/tic/service/message/EventOnTICData.java new file mode 100644 index 0000000..c47d701 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/EventOnTICData.java @@ -0,0 +1,192 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import enedis.lab.util.message.Event; +import enedis.tic.core.TICCoreFrame; + +/** + * EventOnTICData class + * + * Generated + */ +public class EventOnTICData extends Event +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "OnTICData"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected EventOnTICData() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public EventOnTICData(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public EventOnTICData(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param data + * @throws DataDictionaryException + */ + public EventOnTICData(LocalDateTime dateTime, TICCoreFrame data) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setData(data); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Event + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get data + * + * @return the data + */ + public TICCoreFrame getData() + { + return (TICCoreFrame) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICCoreFrame data) throws DataDictionaryException + { + this.setData((Object) data); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, true, TICCoreFrame.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java new file mode 100644 index 0000000..0fdd485 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java @@ -0,0 +1,128 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.message.Request; + +/** + * RequestGetAvailableTICs class + * + * Generated + */ +public class RequestGetAvailableTICs extends Request +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Message name */ + public static final String NAME = "GetAvailableTICs"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @throws DataDictionaryException + */ + public RequestGetAvailableTICs() throws DataDictionaryException + { + super(); + + this.kName.setAcceptedValues(NAME); + + this.checkAndUpdate(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestGetAvailableTICs(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestGetAvailableTICs(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Request + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java b/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java new file mode 100644 index 0000000..250981a --- /dev/null +++ b/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java @@ -0,0 +1,123 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.message.Request; + +/** + * RequestGetModemsInfo class + * + * Generated + */ +public class RequestGetModemsInfo extends Request +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Message name */ + public static final String NAME = "GetModemsInfo"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public RequestGetModemsInfo() throws DataDictionaryException + { + super(); + + this.kName.setAcceptedValues(NAME); + + this.checkAndUpdate(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestGetModemsInfo(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestGetModemsInfo(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Request + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/tic/service/message/RequestReadTIC.java b/src/main/java/enedis/tic/service/message/RequestReadTIC.java new file mode 100644 index 0000000..95fb548 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/RequestReadTIC.java @@ -0,0 +1,189 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import enedis.lab.util.message.Request; +import enedis.tic.core.TICIdentifier; + +/** + * RequestReadTIC class + * + * Generated + */ +public class RequestReadTIC extends Request +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "ReadTIC"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected RequestReadTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestReadTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestReadTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param data + * @throws DataDictionaryException + */ + public RequestReadTIC(TICIdentifier data) throws DataDictionaryException + { + this(); + + this.setData(data); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Request + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get data + * + * @return the data + */ + public TICIdentifier getData() + { + return (TICIdentifier) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICIdentifier data) throws DataDictionaryException + { + this.setData((Object) data); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, true, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java new file mode 100644 index 0000000..499917a --- /dev/null +++ b/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java @@ -0,0 +1,190 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorList; +import enedis.lab.util.message.Request; +import enedis.tic.core.TICIdentifier; + +/** + * RequestSubscribeTIC class + * + * Generated + */ +public class RequestSubscribeTIC extends Request +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "SubscribeTIC"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected RequestSubscribeTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestSubscribeTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestSubscribeTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param data + * @throws DataDictionaryException + */ + public RequestSubscribeTIC(List data) throws DataDictionaryException + { + this(); + + this.setData(data); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Request + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() + { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException + { + this.setData((Object) data); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java new file mode 100644 index 0000000..6556d82 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java @@ -0,0 +1,190 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorList; +import enedis.lab.util.message.Request; +import enedis.tic.core.TICIdentifier; + +/** + * RequestUnsubscribeTIC class + * + * Generated + */ +public class RequestUnsubscribeTIC extends Request +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "UnsubscribeTIC"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected RequestUnsubscribeTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestUnsubscribeTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestUnsubscribeTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param data + * @throws DataDictionaryException + */ + public RequestUnsubscribeTIC(List data) throws DataDictionaryException + { + this(); + + this.setData(data); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Request + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() + { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException + { + this.setData((Object) data); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java new file mode 100644 index 0000000..8c92f4d --- /dev/null +++ b/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java @@ -0,0 +1,197 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorList; +import enedis.lab.util.message.Response; +import enedis.tic.core.TICIdentifier; + +/** + * ResponseGetAvailableTICs class + * + * Generated + */ +public class ResponseGetAvailableTICs extends Response +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "GetAvailableTICs"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected ResponseGetAvailableTICs() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseGetAvailableTICs(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseGetAvailableTICs(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseGetAvailableTICs(LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Response + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() + { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException + { + this.setData((Object) data); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java b/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java new file mode 100644 index 0000000..7a66ad1 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java @@ -0,0 +1,197 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorList; +import enedis.lab.util.message.Response; + +/** + * ResponseGetModemsInfo class + * + * Generated + */ +public class ResponseGetModemsInfo extends Response +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "GetModemsInfo"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected ResponseGetModemsInfo() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseGetModemsInfo(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseGetModemsInfo(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseGetModemsInfo(LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Response + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() + { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException + { + this.setData((Object) data); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICPortDescriptor.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java b/src/main/java/enedis/tic/service/message/ResponseReadTIC.java new file mode 100644 index 0000000..0a864cb --- /dev/null +++ b/src/main/java/enedis/tic/service/message/ResponseReadTIC.java @@ -0,0 +1,196 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import enedis.lab.util.message.Response; +import enedis.tic.core.TICCoreFrame; + +/** + * ResponseReadTIC class + * + * Generated + */ +public class ResponseReadTIC extends Response +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "ReadTIC"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected ResponseReadTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseReadTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseReadTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseReadTIC(LocalDateTime dateTime, Number errorCode, String errorMessage, TICCoreFrame data) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Response + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get data + * + * @return the data + */ + public TICCoreFrame getData() + { + return (TICCoreFrame) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICCoreFrame data) throws DataDictionaryException + { + this.setData((Object) data); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreFrame.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java new file mode 100644 index 0000000..9577924 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java @@ -0,0 +1,160 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.util.message.Response; + +/** + * ResponseSubscribeTIC class + * + * Generated + */ +public class ResponseSubscribeTIC extends Response +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Message name */ + public static final String NAME = "SubscribeTIC"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected ResponseSubscribeTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseSubscribeTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseSubscribeTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public ResponseSubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Response + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java new file mode 100644 index 0000000..b182e62 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java @@ -0,0 +1,160 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.util.message.Response; + +/** + * ResponseUnsubscribeTIC class + * + * Generated + */ +public class ResponseUnsubscribeTIC extends Response +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Message name */ + public static final String NAME = "UnsubscribeTIC"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected ResponseUnsubscribeTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseUnsubscribeTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseUnsubscribeTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public ResponseUnsubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + + this.checkAndUpdate(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Response + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() + { + try + { + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java new file mode 100644 index 0000000..834533b --- /dev/null +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java @@ -0,0 +1,28 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message.factory; + +import enedis.lab.util.message.factory.EventFactory; +import enedis.tic.service.message.EventOnError; +import enedis.tic.service.message.EventOnTICData; + +/** + * TIC2WebSocket request factory + */ +public class TIC2WebSocketEventFactory extends EventFactory +{ + /** + * Default constructor + */ + public TIC2WebSocketEventFactory() + { + super(); + this.addMessageClass(EventOnTICData.NAME, EventOnTICData.class); + this.addMessageClass(EventOnError.NAME, EventOnError.class); + } +} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java new file mode 100644 index 0000000..e501d05 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message.factory; + +import enedis.lab.util.message.factory.MessageFactory; + +/** + * TIC2WebSocket message factory + */ +public class TIC2WebSocketMessageFactory extends MessageFactory +{ + /** + * Default constructor + */ + public TIC2WebSocketMessageFactory() + { + super(new TIC2WebSocketRequestFactory(), new TIC2WebSocketResponseFactory(), new TIC2WebSocketEventFactory()); + } +} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java new file mode 100644 index 0000000..e76db79 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java @@ -0,0 +1,34 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message.factory; + +import enedis.lab.util.message.factory.RequestFactory; +import enedis.tic.service.message.RequestGetAvailableTICs; +import enedis.tic.service.message.RequestGetModemsInfo; +import enedis.tic.service.message.RequestReadTIC; +import enedis.tic.service.message.RequestSubscribeTIC; +import enedis.tic.service.message.RequestUnsubscribeTIC; + +/** + * TIC2WebSocket request factory + */ +public class TIC2WebSocketRequestFactory extends RequestFactory +{ + /** + * Default constructor + */ + public TIC2WebSocketRequestFactory() + { + super(); + this.addMessageClass(RequestGetAvailableTICs.NAME, RequestGetAvailableTICs.class); + this.addMessageClass(RequestGetModemsInfo.NAME, RequestGetModemsInfo.class); + this.addMessageClass(RequestReadTIC.NAME, RequestReadTIC.class); + this.addMessageClass(RequestSubscribeTIC.NAME, RequestSubscribeTIC.class); + this.addMessageClass(RequestUnsubscribeTIC.NAME, RequestUnsubscribeTIC.class); + } +} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java new file mode 100644 index 0000000..13390e3 --- /dev/null +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java @@ -0,0 +1,34 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message.factory; + +import enedis.lab.util.message.factory.ResponseFactory; +import enedis.tic.service.message.ResponseGetAvailableTICs; +import enedis.tic.service.message.ResponseGetModemsInfo; +import enedis.tic.service.message.ResponseReadTIC; +import enedis.tic.service.message.ResponseSubscribeTIC; +import enedis.tic.service.message.ResponseUnsubscribeTIC; + +/** + * TIC2WebSocket request factory + */ +public class TIC2WebSocketResponseFactory extends ResponseFactory +{ + /** + * Default constructor + */ + public TIC2WebSocketResponseFactory() + { + super(); + this.addMessageClass(ResponseGetAvailableTICs.NAME, ResponseGetAvailableTICs.class); + this.addMessageClass(ResponseGetModemsInfo.NAME, ResponseGetModemsInfo.class); + this.addMessageClass(ResponseReadTIC.NAME, ResponseReadTIC.class); + this.addMessageClass(ResponseSubscribeTIC.NAME, ResponseSubscribeTIC.class); + this.addMessageClass(ResponseUnsubscribeTIC.NAME, ResponseUnsubscribeTIC.class); + } +} \ No newline at end of file diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java new file mode 100644 index 0000000..16e5518 --- /dev/null +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java @@ -0,0 +1,121 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.netty; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; +import io.netty.handler.stream.ChunkedWriteHandler; + +import enedis.tic.service.client.TIC2WebSocketClientPool; +import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; + +/** + * Channel initializer for TIC2WebSocket server + */ +public class TIC2WebSocketChannelInitializer extends ChannelInitializer +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final String WEBSOCKET_PATH = "/"; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private final TIC2WebSocketClientPool clientPool; + private final TIC2WebSocketRequestHandler requestHandler; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor + * + * @param clientPool the client pool + * @param requestHandler the request handler + */ + public TIC2WebSocketChannelInitializer(TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) + { + this.clientPool = clientPool; + this.requestHandler = requestHandler; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void initChannel(SocketChannel ch) throws Exception + { + ChannelPipeline pipeline = ch.pipeline(); + + // HTTP codec + pipeline.addLast(new HttpServerCodec()); + + // HTTP object aggregator + pipeline.addLast(new HttpObjectAggregator(65536)); + + // Chunked write handler for large messages + pipeline.addLast(new ChunkedWriteHandler()); + + // WebSocket compression + pipeline.addLast(new WebSocketServerCompressionHandler()); + + // WebSocket protocol handler + pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true)); + + // Custom TIC2WebSocket handler + pipeline.addLast(new TIC2WebSocketHandler(clientPool, requestHandler)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java new file mode 100644 index 0000000..65ae2a9 --- /dev/null +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java @@ -0,0 +1,322 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.netty; + +import java.util.List; +import java.util.Optional; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.message.Event; +import enedis.lab.util.message.Message; +import enedis.lab.util.message.Request; +import enedis.lab.util.message.Response; +import enedis.lab.util.message.exception.MessageInvalidContentException; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.MessageInvalidTypeException; +import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; +import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; +import enedis.lab.util.message.exception.UnsupportedMessageException; +import enedis.tic.core.TICIdentifier; +import enedis.tic.service.client.TIC2WebSocketClient; +import enedis.tic.service.client.TIC2WebSocketClientPool; +import enedis.tic.service.endpoint.EventSender; +import enedis.tic.service.endpoint.TIC2WebSocketEndPointErrorCode; +import enedis.tic.service.message.RequestUnsubscribeTIC; +import enedis.tic.service.message.factory.TIC2WebSocketMessageFactory; +import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; + +/** + * Netty WebSocket handler for TIC2WebSocket + */ +public class TIC2WebSocketHandler extends SimpleChannelInboundHandler implements EventSender +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final Logger logger = LogManager.getLogger(TIC2WebSocketHandler.class); + + private final TIC2WebSocketClientPool clientPool; + private final TIC2WebSocketRequestHandler requestHandler; + private final TIC2WebSocketMessageFactory factory; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor + * + * @param clientPool the client pool + * @param requestHandler the request handler + */ + public TIC2WebSocketHandler(TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) + { + this.clientPool = clientPool; + this.requestHandler = requestHandler; + this.factory = new TIC2WebSocketMessageFactory(); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// EventSender + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void sendEvent(Channel channel, Event event) + { + this.sendMessage(channel, event); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception + { + Channel channel = ctx.channel(); + String channelId = channel.id().asLongText(); + + logger.info("Open channel " + channelId); + + if (!clientPool.exists(channelId)) + { + logger.info("Client create with channel id : " + channelId); + clientPool.createClient(channel, this); + } + else + { + logger.error("Client with channel id : " + channelId + " already exists ! "); + } + + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception + { + Channel channel = ctx.channel(); + String channelId = channel.id().asLongText(); + + logger.info("Close channel " + channelId); + + if (clientPool.exists(channelId)) + { + try + { + logger.info("Generate unsubscribe request"); + Request request = new RequestUnsubscribeTIC((List) null); + this.handleRequest(clientPool.getClient(channelId).get(), request); + + logger.info("Remove client with channel id : " + channelId); + clientPool.remove(channelId); + } + catch (DataDictionaryException e) + { + logger.error("Error during channel close", e); + } + } + else + { + logger.error("Client with channel id : " + channelId + " doesn't exist ! "); + } + + super.channelInactive(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception + { + Channel channel = ctx.channel(); + String channelId = channel.id().asLongText(); + + logger.error("Error on channel " + channelId, cause); + + ctx.close(); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception + { + if (frame instanceof TextWebSocketFrame) + { + TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; + String text = textFrame.text(); + Channel channel = ctx.channel(); + String channelId = channel.id().asLongText(); + + logger.info("Message on channel " + channelId); + + TIC2WebSocketClient client = this.getClient(channel); + + Optional message = this.getMessage(channel, text); + if (!message.isPresent()) + { + return; + } + + Optional request = this.getRequest(channel, message.get()); + if (!request.isPresent()) + { + return; + } + + Response response = this.handleRequest(client, request.get()); + + this.sendMessage(channel, response); + } + else + { + // Handle other frame types if needed + logger.warn("Unsupported frame type: " + frame.getClass().getSimpleName()); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private TIC2WebSocketClient getClient(Channel channel) + { + String channelId = channel.id().asLongText(); + Optional clientOpt = clientPool.getClient(channelId); + if (!clientOpt.isPresent()) + { + return clientPool.createClient(channel, this); + } + return clientOpt.get(); + } + + private Optional getMessage(Channel channel, String text) + { + Message message = null; + TIC2WebSocketEndPointErrorCode errorCode = TIC2WebSocketEndPointErrorCode.NO_ERROR; + String errorMessage = ""; + + try + { + message = this.factory.getMessage(text); + } + catch (MessageInvalidFormatException e) + { + errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_FORMAT; + errorMessage = e.getMessage(); + } + catch (MessageInvalidTypeException e) + { + errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_INVALID; + errorMessage = e.getMessage(); + } + catch (MessageKeyNameDoesntExistException e) + { + errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_NAME_MISSING; + errorMessage = e.getMessage(); + } + catch (MessageKeyTypeDoesntExistException e) + { + errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_MISSING; + errorMessage = e.getMessage(); + } + catch (MessageInvalidContentException e) + { + errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_CONTENT; + errorMessage = e.getMessage(); + } + catch (UnsupportedMessageException e) + { + errorCode = TIC2WebSocketEndPointErrorCode.UNSUPPORTED_MESSAGE; + errorMessage = e.getMessage(); + } + + if (errorCode != TIC2WebSocketEndPointErrorCode.NO_ERROR) + { + logger.error("Error parsing message: " + errorMessage); + // TODO: Send error event + return Optional.empty(); + } + + return Optional.of(message); + } + + private Optional getRequest(Channel channel, Message message) + { + if (!(message instanceof Request)) + { + logger.error("Message is not a request"); + // TODO: Send error event + return Optional.empty(); + } + + return Optional.of((Request) message); + } + + private Response handleRequest(TIC2WebSocketClient client, Request request) + { + return requestHandler.handle(request, client); + } + + private void sendMessage(Channel channel, Message message) + { + try + { + String json = message.toJSON().toString(); + TextWebSocketFrame frame = new TextWebSocketFrame(json); + channel.writeAndFlush(frame); + + logger.debug("Sent message to channel {}: {}", channel.id().asLongText(), json); + } + catch (Exception e) + { + logger.error("Error sending message to channel " + channel.id().asLongText(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java new file mode 100644 index 0000000..2c22b8d --- /dev/null +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java @@ -0,0 +1,190 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.netty; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import enedis.tic.service.client.TIC2WebSocketClientPool; +import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; + +/** + * TIC2WebSocket Netty WebSocket Server + */ +public class TIC2WebSocketServer +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final Logger logger = LogManager.getLogger(TIC2WebSocketServer.class); + + private final String host; + private final int port; + private final TIC2WebSocketClientPool clientPool; + private final TIC2WebSocketRequestHandler requestHandler; + + private EventLoopGroup bossGroup; + private EventLoopGroup workerGroup; + private Channel serverChannel; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor + * + * @param host the host to bind to + * @param port the port to bind to + * @param clientPool the client pool + * @param requestHandler the request handler + */ + public TIC2WebSocketServer(String host, int port, TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) + { + this.host = host; + this.port = port; + this.clientPool = clientPool; + this.requestHandler = requestHandler; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Start the WebSocket server + * + * @throws Exception if server startup fails + */ + public void start() throws Exception + { + logger.info("Starting TIC2WebSocket Netty server on {}:{}", host, port); + + bossGroup = new NioEventLoopGroup(1); + workerGroup = new NioEventLoopGroup(); + + try + { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new TIC2WebSocketChannelInitializer(clientPool, requestHandler)); + + ChannelFuture future = bootstrap.bind(host, port).sync(); + serverChannel = future.channel(); + + logger.info("TIC2WebSocket Netty server started successfully on {}:{}", host, port); + } + catch (Exception e) + { + logger.error("Failed to start TIC2WebSocket Netty server", e); + stop(); + throw e; + } + } + + /** + * Stop the WebSocket server + */ + public void stop() + { + logger.info("Stopping TIC2WebSocket Netty server"); + + try + { + if (serverChannel != null) + { + serverChannel.close().sync(); + } + } + catch (InterruptedException e) + { + logger.warn("Interrupted while closing server channel", e); + Thread.currentThread().interrupt(); + } + finally + { + if (bossGroup != null) + { + bossGroup.shutdownGracefully(); + } + if (workerGroup != null) + { + workerGroup.shutdownGracefully(); + } + } + + logger.info("TIC2WebSocket Netty server stopped"); + } + + /** + * Wait for the server to close + * + * @throws InterruptedException if interrupted while waiting + */ + public void waitForClose() throws InterruptedException + { + if (serverChannel != null) + { + serverChannel.closeFuture().sync(); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java new file mode 100644 index 0000000..da0f444 --- /dev/null +++ b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java @@ -0,0 +1,27 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.requesthandler; + +import enedis.lab.util.message.Request; +import enedis.lab.util.message.Response; +import enedis.tic.service.client.TIC2WebSocketClient; + +/** + * TIC2WebSocket request handler interface + */ +public interface TIC2WebSocketRequestHandler +{ + /** + * Handle all TIC2WebSocket request + * + * @param request + * @param client + * @return request response + */ + public Response handle(Request request, TIC2WebSocketClient client); +} diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java new file mode 100644 index 0000000..22f623b --- /dev/null +++ b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java @@ -0,0 +1,341 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.requesthandler; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.message.Request; +import enedis.lab.util.message.Response; +import enedis.lab.util.message.ResponseBase; +import enedis.tic.core.TICCore; +import enedis.tic.core.TICCoreErrorCode; +import enedis.tic.core.TICCoreException; +import enedis.tic.core.TICCoreFrame; +import enedis.tic.core.TICIdentifier; +import enedis.tic.service.client.TIC2WebSocketClient; +import enedis.tic.service.endpoint.TIC2WebSocketEndPointErrorCode; +import enedis.tic.service.message.RequestGetAvailableTICs; +import enedis.tic.service.message.RequestGetModemsInfo; +import enedis.tic.service.message.RequestReadTIC; +import enedis.tic.service.message.RequestSubscribeTIC; +import enedis.tic.service.message.RequestUnsubscribeTIC; +import enedis.tic.service.message.ResponseGetAvailableTICs; +import enedis.tic.service.message.ResponseGetModemsInfo; +import enedis.tic.service.message.ResponseReadTIC; +import enedis.tic.service.message.ResponseSubscribeTIC; +import enedis.tic.service.message.ResponseUnsubscribeTIC; + +/** + * TIC2WebSocket client pool interface + */ +public class TIC2WebSocketRequestHandlerBase implements TIC2WebSocketRequestHandler +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Logger logger; + private TICCore ticCore; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Default constructor + * + * @param ticCore + */ + public TIC2WebSocketRequestHandlerBase(TICCore ticCore) + { + super(); + this.logger = LogManager.getLogger(this.getClass()); + this.ticCore = ticCore; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Response handle(Request request, TIC2WebSocketClient client) + { + Response response = null; + + switch (request.getName()) + { + case RequestGetAvailableTICs.NAME: + response = this.handleGetAvailableTICsRequest((RequestGetAvailableTICs) request); + break; + case RequestGetModemsInfo.NAME: + response = this.handleGetModemsInfoRequest((RequestGetModemsInfo) request); + break; + case RequestReadTIC.NAME: + response = this.handleReadTICRequest((RequestReadTIC) request); + break; + case RequestSubscribeTIC.NAME: + response = this.handleSubscribeTICRequest((RequestSubscribeTIC) request, client); + break; + case RequestUnsubscribeTIC.NAME: + response = this.handleUnsubscribeTICRequest((RequestUnsubscribeTIC) request, client); + break; + default: + this.logger.error("Request " + request.getName() + " not supported"); + response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.UNSUPPORTED_MESSAGE, "Request " + request.getName() + " not supported"); + break; + } + + return response; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Response handleGetAvailableTICsRequest(RequestGetAvailableTICs request) + { + List ticIdentifiers = this.ticCore.getAvailableTICs(); + + Response response = null; + try + { + response = new ResponseGetAvailableTICs(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, ticIdentifiers); + } + catch (DataDictionaryException e) + { + this.logger.error(e.getMessage(), e); + response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); + } + + return response; + } + + private Response handleGetModemsInfoRequest(RequestGetModemsInfo request) + { + List modemsInfo = this.ticCore.getModemsInfo(); + + Response response = null; + try + { + response = new ResponseGetModemsInfo(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, modemsInfo); + } + catch (DataDictionaryException e) + { + this.logger.error(e.getMessage(), e); + response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); + } + + return response; + } + + private Response handleReadTICRequest(RequestReadTIC request) + { + Response response = null; + try + { + TICCoreFrame frame = this.ticCore.readNextFrame(request.getData()); + response = new ResponseReadTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, frame); + } + catch (DataDictionaryException e) + { + this.logger.error(e.getMessage(), e); + response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); + } + catch (TICCoreException e) + { + if(e.getErrorCode() == TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode()) + { + response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.IDENTIFIER_NOT_FOUND, e.getMessage()); + } + else if(e.getErrorCode() == TICCoreErrorCode.DATA_READ_TIMEOUT.getCode()) + { + response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.READ_TIMEOUT, e.getMessage()); + } + else + { + response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + return response; + } + + private Response handleSubscribeTICRequest(RequestSubscribeTIC request, TIC2WebSocketClient client) + { + Response response = null; + Optional> ticIdentifiers = Optional.ofNullable(request.getData()); + + if (ticIdentifiers.isPresent()) + { + List newSubscriptions = this.getNewSubcriptions(this.ticCore.getIndentifiers(client), ticIdentifiers.get()); + if(!newSubscriptions.isEmpty()) + { + for(TICIdentifier identifier : ticIdentifiers.get()) + { + try + { + this.ticCore.subscribe(identifier,client); + try + { + response = new ResponseSubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); + } + catch (DataDictionaryException e) + { + this.logger.error(e.getMessage(), e); + } + } + catch (TICCoreException e) + { + response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.SUBSCRIPTION_FAIL, e.getMessage()); + } + } + } + } + else + { + this.ticCore.subscribe(client); + try + { + response = new ResponseSubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); + } + catch (DataDictionaryException e) + { + this.logger.error(e.getMessage(), e); + } + } + + return response; + } + + private List getNewSubcriptions(List currentSubscriptions, List askedTicIdentifiers) + { + List newSubscriptions = new ArrayList(); + + for (TICIdentifier askedTicIdentifier : askedTicIdentifiers) + { + boolean newSub = true; + for (TICIdentifier currentSubscription : currentSubscriptions) + { + if (askedTicIdentifier.matches(currentSubscription)) + { + newSub = false; + break; + } + } + if (newSub) + { + newSubscriptions.add(askedTicIdentifier); + } + } + return newSubscriptions; + } + + private Response handleUnsubscribeTICRequest(RequestUnsubscribeTIC request, TIC2WebSocketClient client) + { + Response response = null; + Optional> ticIdentifiers = Optional.ofNullable(request.getData()); + + if (ticIdentifiers.isPresent()) + { + for(TICIdentifier identifier : ticIdentifiers.get()) + { + try + { + this.ticCore.unsubscribe(identifier,client); + try + { + response = new ResponseUnsubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); + } + catch (DataDictionaryException e) + { + this.logger.error(e.getMessage(), e); + } + } + catch (TICCoreException e) + { + response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.UNSUBSCRIPTION_FAIL, e.getMessage()); + } + } + } + else + { + this.ticCore.unsubscribe(client); + try + { + response = new ResponseUnsubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); + } + catch (DataDictionaryException e) + { + this.logger.error(e.getMessage(), e); + } + } + + return response; + } + + private Response createErrorResponse(String name, TIC2WebSocketEndPointErrorCode code, String message) + { + try + { + return new ResponseBase(name, LocalDateTime.now(), code.value(), message, null); + } + catch (DataDictionaryException e) + { + this.logger.error(e.getMessage(), e); + return null; + } + } + +} diff --git a/src/main/resources/config/TIC2WebSocketConfiguration.json b/src/main/resources/config/TIC2WebSocketConfiguration.json new file mode 100644 index 0000000..7ff3804 --- /dev/null +++ b/src/main/resources/config/TIC2WebSocketConfiguration.json @@ -0,0 +1,4 @@ +{ + "serverPort": 19584, + "ticMode": "AUTO" +} \ No newline at end of file diff --git a/src/main/resources/config/TIC2WebSocketConfiguration.json.license b/src/main/resources/config/TIC2WebSocketConfiguration.json.license new file mode 100644 index 0000000..18bf6d4 --- /dev/null +++ b/src/main/resources/config/TIC2WebSocketConfiguration.json.license @@ -0,0 +1,6 @@ +Copyright (C) 2025 Enedis Smarties team + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/main/resources/log4j2-debug.xml b/src/main/resources/log4j2-debug.xml new file mode 100644 index 0000000..8ad49b1 --- /dev/null +++ b/src/main/resources/log4j2-debug.xml @@ -0,0 +1,45 @@ + + + + + + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/log4j2-error.xml b/src/main/resources/log4j2-error.xml new file mode 100644 index 0000000..6b7a9c5 --- /dev/null +++ b/src/main/resources/log4j2-error.xml @@ -0,0 +1,45 @@ + + + + + + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/log4j2-fatal.xml b/src/main/resources/log4j2-fatal.xml new file mode 100644 index 0000000..6a936cd --- /dev/null +++ b/src/main/resources/log4j2-fatal.xml @@ -0,0 +1,45 @@ + + + + + + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/log4j2-info.xml b/src/main/resources/log4j2-info.xml new file mode 100644 index 0000000..8a0f971 --- /dev/null +++ b/src/main/resources/log4j2-info.xml @@ -0,0 +1,45 @@ + + + + + + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/log4j2-off.xml b/src/main/resources/log4j2-off.xml new file mode 100644 index 0000000..539ef56 --- /dev/null +++ b/src/main/resources/log4j2-off.xml @@ -0,0 +1,45 @@ + + + + + + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/log4j2-trace.xml b/src/main/resources/log4j2-trace.xml new file mode 100644 index 0000000..f0324fe --- /dev/null +++ b/src/main/resources/log4j2-trace.xml @@ -0,0 +1,45 @@ + + + + + + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/log4j2-warn.xml b/src/main/resources/log4j2-warn.xml new file mode 100644 index 0000000..8d42e66 --- /dev/null +++ b/src/main/resources/log4j2-warn.xml @@ -0,0 +1,45 @@ + + + + + + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/scripts/SerialPortPlugNotifier.bat b/src/main/scripts/SerialPortPlugNotifier.bat new file mode 100644 index 0000000..78d1907 --- /dev/null +++ b/src/main/scripts/SerialPortPlugNotifier.bat @@ -0,0 +1,14 @@ +REM Copyright (C) 2025 Enedis Smarties team +REM +REM SPDX-FileContributor: Jehan BOUSCH +REM SPDX-FileContributor: Mathieu SABARTHES +REM +REM SPDX-License-Identifier: Apache-2.0 + +@echo off + +REM Get script directory +set SCRIPT_DIRECTORY=%~dp0 + +REM Run executable +java -cp "%SCRIPT_DIRECTORY%/lib/*" enedis.lab.io.serialport.SerialPortPlugNotifier %* \ No newline at end of file diff --git a/src/main/scripts/SerialPortPlugNotifier.sh b/src/main/scripts/SerialPortPlugNotifier.sh new file mode 100644 index 0000000..7617085 --- /dev/null +++ b/src/main/scripts/SerialPortPlugNotifier.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +# Get script directory +SCRIPT_DIRECTORY=$(dirname "$0") + +# Run executable +java -cp "$SCRIPT_DIRECTORY/lib/*" enedis.lab.io.serialport.SerialPortPlugNotifier $* \ No newline at end of file diff --git a/src/main/scripts/TIC2WebSocket.bat b/src/main/scripts/TIC2WebSocket.bat new file mode 100644 index 0000000..f5316e0 --- /dev/null +++ b/src/main/scripts/TIC2WebSocket.bat @@ -0,0 +1,14 @@ +REM Copyright (C) 2025 Enedis Smarties team +REM +REM SPDX-FileContributor: Jehan BOUSCH +REM SPDX-FileContributor: Mathieu SABARTHES +REM +REM SPDX-License-Identifier: Apache-2.0 + +@echo off + +REM Get script directory +set SCRIPT_DIRECTORY=%~dp0 + +REM Run executable +java -DlogDir="%SCRIPT_DIRECTORY%var\log" -DconfigFile="%SCRIPT_DIRECTORY%var\config\TIC2WebSocketConfiguration.json" -cp "%SCRIPT_DIRECTORY%/lib/*" enedis.tic.service.TIC2WebSocketApplication %* \ No newline at end of file diff --git a/src/main/scripts/TIC2WebSocket.sh b/src/main/scripts/TIC2WebSocket.sh new file mode 100644 index 0000000..d69dc26 --- /dev/null +++ b/src/main/scripts/TIC2WebSocket.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +# Get script directory +SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) + +# Run executable +java -DlogDir="$SCRIPT_DIRECTORY/var/log" -DconfigFile="$SCRIPT_DIRECTORY/var/config/TIC2WebSocketConfiguration.json" -cp "$SCRIPT_DIRECTORY/lib/*" enedis.tic.service.TIC2WebSocketApplication $* \ No newline at end of file diff --git a/src/main/scripts/TIC2WebSocket_remote_debug.sh b/src/main/scripts/TIC2WebSocket_remote_debug.sh new file mode 100644 index 0000000..5ca9c68 --- /dev/null +++ b/src/main/scripts/TIC2WebSocket_remote_debug.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +# Get script directory +SCRIPT_DIRECTORY=$(dirname "$0") + +# Run executable +java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=0.0.0.0:8000 -DlogDir="$SCRIPT_DIRECTORY/var/log" -DconfigDir="$SCRIPT_DIRECTORY/var/config" -cp "$SCRIPT_DIRECTORY/lib/*" tic.service.app.TIC2WebSocketApplication $* \ No newline at end of file diff --git a/src/main/scripts/TICPortDump.bat b/src/main/scripts/TICPortDump.bat new file mode 100644 index 0000000..e3519bd --- /dev/null +++ b/src/main/scripts/TICPortDump.bat @@ -0,0 +1,16 @@ +REM Copyright (C) 2025 Enedis Smarties team +REM +REM SPDX-FileContributor: Jehan BOUSCH +REM SPDX-FileContributor: Mathieu SABARTHES +REM +REM SPDX-License-Identifier: Apache-2.0 + +@echo off + +REM Get script directory +set SCRIPT_DIRECTORY=%~dp0 +REM Get script name +set SCRIPT_NAME=%~n0 + +REM Run powershell script +powershell -File %SCRIPT_DIRECTORY%/%SCRIPT_NAME%.ps1 %* \ No newline at end of file diff --git a/src/main/scripts/TICPortDump.ps1 b/src/main/scripts/TICPortDump.ps1 new file mode 100644 index 0000000..6b6eb87 --- /dev/null +++ b/src/main/scripts/TICPortDump.ps1 @@ -0,0 +1,42 @@ +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +if($args.count -lt 1) +{ + Write-Error 'Usage: ' + $MyInvocation.MyCommand.Name + ' portName [ticMode]' -ErrorAction Stop +} +$portName = $args[0] +if($args.count -lt 2) +{ + $ticMode = "STANDARD" +} +else +{ + $ticMode = $args[1] +} + +if($ticMode -eq "STANDARD") +{ + $baudrate = 9600 +} +elseif($ticMode -eq "HISTORIC") +{ + $baudrate = 1200 +} +else +{ + Write-Error 'The tic mode "' + $ticMode + '" is unknown (only "STANDARD" or "HISTORIC" is allowed)!' -ErrorAction Stop +} + +$port = new-Object System.IO.Ports.SerialPort $portName,$baudrate,Even,7,one +$port.open() +while($port.IsOpen) +{ + $line = $port.ReadLine() + Write-Output $line +} +$port.close() \ No newline at end of file diff --git a/src/main/scripts/TICPortDump.sh b/src/main/scripts/TICPortDump.sh new file mode 100644 index 0000000..bd960a4 --- /dev/null +++ b/src/main/scripts/TICPortDump.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +if [ ! -n "$1" ] +then + echo "Usage: `basename $0` portName [ticMode]" + exit -1 +fi +portName="$1" + +if [ ! -n "$2" ]; then + ticMode="STANDARD" +else + ticMode="$2" +fi + +if [ "$ticMode" == "STANDARD" ]; then + baudrate=9600 +elif [ "$ticMode" == "HISTORIC" ]; then + baudrate=1200 +else + echo "The tic mode \"$ticMode\" is unknown (only \"STANDARD\" or \"HISTORIC\" is allowed)!" + exit -1 +fi + +# Configure serial port +stty $baudrate evenp igncr -F "$portName" +# Read serial port +cat "$portName" diff --git a/src/main/scripts/TICPortPlugNotifier.bat b/src/main/scripts/TICPortPlugNotifier.bat new file mode 100644 index 0000000..3121547 --- /dev/null +++ b/src/main/scripts/TICPortPlugNotifier.bat @@ -0,0 +1,14 @@ +REM Copyright (C) 2025 Enedis Smarties team +REM +REM SPDX-FileContributor: Jehan BOUSCH +REM SPDX-FileContributor: Mathieu SABARTHES +REM +REM SPDX-License-Identifier: Apache-2.0 + +@echo off + +REM Get script directory +set SCRIPT_DIRECTORY=%~dp0 + +REM Run executable +java -cp "%SCRIPT_DIRECTORY%/lib/*" enedis.lab.io.tic.TICPortPlugNotifier %* \ No newline at end of file diff --git a/src/main/scripts/TICPortPlugNotifier.sh b/src/main/scripts/TICPortPlugNotifier.sh new file mode 100644 index 0000000..2747dce --- /dev/null +++ b/src/main/scripts/TICPortPlugNotifier.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +# Get script directory +SCRIPT_DIRECTORY=$(dirname "$0") + +# Run executable +java -cp "$SCRIPT_DIRECTORY/lib/*" enedis.lab.io.tic.TICPortPlugNotifier $* \ No newline at end of file diff --git a/src/main/scripts/setup_TIC2WebSocket.sh b/src/main/scripts/setup_TIC2WebSocket.sh new file mode 100644 index 0000000..6a34c31 --- /dev/null +++ b/src/main/scripts/setup_TIC2WebSocket.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +############################################ VARIABLES ############################################ + +TIC2WebSocket_DIR=$(dirname "${BASH_SOURCE[0]}") +TIC2WebSocket_SCRIPT="${BASH_SOURCE[0]}" +TIC2WebSocket_PACKAGE="$TIC2WebSocket_DIR"/TIC2WebSocket-0.0-bin.zip +TIC2WebSocket_INSTALLDIR=/Applications/TIC2WebSocket + +############################################ FUNCTIONS ############################################ + +function TIC2WebSocket_install() +{ + sudo mkdir -p "$TIC2WebSocket_INSTALLDIR" + sudo unzip "$TIC2WebSocket_PACKAGE" -d "$TIC2WebSocket_INSTALLDIR" + sudo chmod -R 777 "$TIC2WebSocket_INSTALLDIR" +} + +function TIC2WebSocket_uninstall() +{ + sudo rm -Rf "$TIC2WebSocket_INSTALLDIR" +} + +function TIC2WebSocket_isInstalled() +{ + "$TIC2WebSocket_INSTALLDIR"/TIC2WebSocket.sh --version > /dev/null 2>&1 + if [ $? != 0 ]; then + echo false + return 0 + fi + echo true +} + +############################################## MAIN ############################################### + +option="$1" + +case "$option" in + install) + + if [ $(TIC2WebSocket_isInstalled) == "true" ]; then + echo "TIC2WebSocket already installed" + else + echo "Install TIC2WebSocket" + TIC2WebSocket_install + fi + ;; + + uninstall) + + if [ $(TIC2WebSocket_isInstalled) == "true" ]; then + echo "Uninstall TIC2WebSocket" + TIC2WebSocket_uninstall + else + echo "TIC2WebSocket already uninstalled" + fi + ;; + + reinstall) + + if [ $(TIC2WebSocket_isInstalled) == "true" ]; then + echo "Remove TIC2WebSocket" + TIC2WebSocket_uninstall + fi + echo "Install TIC2WebSocket" + TIC2WebSocket_install + ;; + + status) + + if [ $(TIC2WebSocket_isInstalled) == "true" ]; then + echo "TIC2WebSocket installed" + else + echo "TIC2WebSocket not installed" + fi + ;; + + *) + if [ "$option" != "" ]; then + echo "Usage: $(basename "$TIC2WebSocket_SCRIPT") {install|uninstallreinstall|status}" + exit 1 + fi + ;; + +esac diff --git a/src/test/java/enedis/lab/io/PortFinderMock.java b/src/test/java/enedis/lab/io/PortFinderMock.java new file mode 100644 index 0000000..4fb9c13 --- /dev/null +++ b/src/test/java/enedis/lab/io/PortFinderMock.java @@ -0,0 +1,73 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io; + +import java.util.ArrayList; +import java.util.List; + +import enedis.lab.mock.FunctionCall; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataList; + +@SuppressWarnings("javadoc") +public class PortFinderMock implements PortFinder +{ + public List findAllCalls = new ArrayList(); + public DataList descriptorList = new DataArrayList(); + + public PortFinderMock() + { + this.descriptorList = new DataArrayList(); + } + + @Override + public DataList findAll() + { + this.findAllCalls.add(new FunctionCall()); + return this.getDescriptors(); + } + + public DataList getDescriptors() + { + DataList descriptors = new DataArrayList(); + + synchronized (this.descriptorList) + { + descriptors.addAll(this.descriptorList); + } + + return descriptors; + } + + public DataList setDescriptors(DataList descriptors) + { + synchronized (this.descriptorList) + { + this.descriptorList.clear(); + this.descriptorList.addAll(descriptors); + } + + return descriptors; + } + + public void addDescriptor(T descriptor) + { + synchronized (this.descriptorList) + { + this.descriptorList.add(descriptor); + } + } + + public void removeDescriptor(T descriptor) + { + synchronized (this.descriptorList) + { + this.descriptorList.remove(descriptor); + } + } +} diff --git a/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java b/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java new file mode 100644 index 0000000..225f8f4 --- /dev/null +++ b/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java @@ -0,0 +1,102 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +import enedis.lab.io.PortFinderMock; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataList; + +@SuppressWarnings("javadoc") +public class TICPortFinderMock extends PortFinderMock implements TICPortFinder +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public DataList nativeDescriptorList = new DataArrayList(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICPortFinderMock() + { + super(); + } + + public TICPortFinderMock(TICPortDescriptor... descriptors) + { + this.setDescriptors(DataArrayList.asList(descriptors)); + } + + @Override + public TICPortDescriptor findNative(String portName) + { + for (TICPortDescriptor descriptor : this.nativeDescriptorList) + { + if (descriptor.getPortName() == null && portName == null) + { + return descriptor; + } + if (descriptor.getPortName().equals(portName)) + { + return descriptor; + } + } + + return null; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/test/java/enedis/lab/mock/FunctionCall.java b/src/test/java/enedis/lab/mock/FunctionCall.java new file mode 100644 index 0000000..6a5ba81 --- /dev/null +++ b/src/test/java/enedis/lab/mock/FunctionCall.java @@ -0,0 +1,26 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.mock; + +@SuppressWarnings("javadoc") +public class FunctionCall +{ + public long captureTime; + + public FunctionCall() + { + super(); + this.captureTime = System.nanoTime(); + } + + public FunctionCall(long captureTime) + { + super(); + this.captureTime = captureTime; + } +} diff --git a/src/test/java/enedis/tic/core/TICCoreBaseTest.java b/src/test/java/enedis/tic/core/TICCoreBaseTest.java new file mode 100644 index 0000000..f8d7fff --- /dev/null +++ b/src/test/java/enedis/tic/core/TICCoreBaseTest.java @@ -0,0 +1,498 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import enedis.lab.io.tic.TICModemType; +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.io.tic.TICPortFinderMock; +import enedis.lab.mock.FunctionCall; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.util.time.Time; +import enedis.lab.util.task.Task; + +@SuppressWarnings("javadoc") +public class TICCoreBaseTest +{ + private static final int NOTIFICATION_TIMEOUT = 1000; + + private TICPortFinderMock ticPortFinder; + private long plugNotifierPeriod; + private TICCoreBase ticCore; + + @Before + public void startTICCore() throws TICCoreException + { + this.ticPortFinder = new TICPortFinderMock(); + this.plugNotifierPeriod = 50; + this.ticCore = new TICCoreBase(this.ticPortFinder, this.plugNotifierPeriod, TICCoreStreamMock.class, TICMode.AUTO, null); + + this.ticCore.start(); + this.waitTaskRunning(this.ticCore); + } + + @After + public void stopTICCore() throws TICCoreException + { + this.ticCore.stop(); + TICCoreStreamMock.streams.clear(); + } + + @Test + public void test_getAvailableTICs() throws TICCoreException, DataDictionaryException + { + List availableTICs = this.ticCore.getAvailableTICs(); + Assert.assertNotNull(availableTICs); + Assert.assertEquals(0, availableTICs.size()); + + TICPortDescriptor descriptor1 = new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); + TICPortDescriptor descriptor2 = new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); + this.plugModem(descriptor1); + this.plugModem(descriptor2); + + availableTICs = this.ticCore.getAvailableTICs(); + Assert.assertNotNull(availableTICs); + Assert.assertEquals(2, availableTICs.size()); + Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); + Assert.assertTrue(availableTICs.contains(new TICIdentifier("2", "COM4", null))); + + this.unplugModem(descriptor2); + + availableTICs = this.ticCore.getAvailableTICs(); + Assert.assertNotNull(availableTICs); + Assert.assertEquals(1, availableTICs.size()); + Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); + } + + @Test + public void test_getModemsInfo() throws TICCoreException, DataDictionaryException + { + List modemsInfo = this.ticCore.getModemsInfo(); + Assert.assertNotNull(modemsInfo); + Assert.assertEquals(0, modemsInfo.size()); + + TICPortDescriptor descriptor1 = new TICPortDescriptor(null, "COM3", null, null, null, null, TICModemType.MICHAUD); + TICPortDescriptor descriptor2 = new TICPortDescriptor(null, "COM4", null, null, null, null, TICModemType.TELEINFO); + this.plugModem(descriptor1); + this.plugModem(descriptor2); + + modemsInfo = this.ticCore.getModemsInfo(); + Assert.assertNotNull(modemsInfo); + Assert.assertEquals(2, modemsInfo.size()); + Assert.assertTrue(modemsInfo.contains(descriptor1)); + Assert.assertTrue(modemsInfo.contains(descriptor2)); + + this.unplugModem(descriptor2); + modemsInfo = this.ticCore.getModemsInfo(); + Assert.assertNotNull(modemsInfo); + Assert.assertEquals(1, modemsInfo.size()); + Assert.assertTrue(modemsInfo.contains(descriptor1)); + } + + @Test + public void test_readNextFrame_ok() throws TICCoreException, DataDictionaryException + { + TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); + task.start(); + this.waitTaskRunning(task); + this.waitReadNextFrameSubsbription(); + TICCoreFrame frame = this.createFrame(identifier, TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + stream.notifyOnData(frame); + this.waitTaskTerminated(task); + Assert.assertNull(task.exception); + Assert.assertNotNull(task.frame); + Assert.assertTrue(task.frame == frame); + } + + @Test + public void test_readNextFrame_error_OTHER_REASON() throws TICCoreException, DataDictionaryException + { + TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); + task.start(); + this.waitTaskRunning(task); + this.waitReadNextFrameSubsbription(); + TICCoreError error = new TICCoreError(identifier, TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + stream.notifyOnError(error); + this.waitTaskTerminated(task); + Assert.assertNull(task.frame); + Assert.assertNotNull(task.exception); + Assert.assertTrue(task.exception instanceof TICCoreException); + TICCoreException exception = (TICCoreException) task.exception; + Assert.assertEquals(error.getErrorCode(), exception.getErrorCode()); + Assert.assertEquals(error.getErrorMessage(), exception.getErrorInfo()); + } + + @Test + public void test_readNextFrame_error_STREAM_IDENTIFIER_NOT_FOUND() throws TICCoreException, DataDictionaryException + { + TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, new TICIdentifier("2", "COM3", null)); + task.start(); + this.waitTaskRunning(task); + this.waitTaskTerminated(task); + Assert.assertNull(task.frame); + Assert.assertNotNull(task.exception); + Assert.assertTrue(task.exception instanceof TICCoreException); + TICCoreException exception = (TICCoreException) task.exception; + Assert.assertEquals(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); + } + + @Test + public void test_readNextFrame_timeout() throws TICCoreException, DataDictionaryException + { + TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier, 200); + task.start(); + this.waitTaskRunning(task); + this.waitReadNextFrameSubsbription(); + this.waitTaskTerminated(task); + Assert.assertNull(task.frame); + Assert.assertNotNull(task.exception); + Assert.assertTrue(task.exception instanceof TICCoreException); + TICCoreException exception = (TICCoreException) task.exception; + Assert.assertEquals(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), exception.getErrorCode()); + } + + @Test + public void test_subscribe_any() throws TICCoreException, DataDictionaryException + { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + } + + @Test + public void test_unsubscribe_any() throws TICCoreException, DataDictionaryException + { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + this.ticCore.unsubscribe(subscriber); + } + + @Test + public void test_subscribe_withIdentifier_ok() throws TICCoreException, DataDictionaryException + { + TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + } + + @Test + public void test_unsubscribe_withIdentifier_ok() throws TICCoreException, DataDictionaryException + { + TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + this.ticCore.unsubscribe(identifier, subscriber); + } + + @Test + public void test_subscribe_withIdentifier_notFound() throws TICCoreException, DataDictionaryException + { + this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + TICIdentifier identifier = new TICIdentifier(null, "COM4", null); + try + { + this.ticCore.subscribe(identifier, subscriber); + Assert.fail("Subscribe should have trhown an exception since stream identifier does not match !"); + } + catch (Exception e) + { + Assert.assertTrue(e instanceof TICCoreException); + TICCoreException exception = (TICCoreException) e; + Assert.assertEquals(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); + } + } + + @Test + public void test_onError_whenUnplugModem() throws TICCoreException, DataDictionaryException + { + TICPortDescriptor descriptor1 = new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); + TICPortDescriptor descriptor2 = new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); + TICIdentifier identifier1 = this.plugModem(descriptor1); + TICIdentifier identifier2 = this.plugModem(descriptor2); + + TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber4 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber5 = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber1); + this.ticCore.subscribe(identifier1, subscriber2); + this.ticCore.subscribe(identifier1, subscriber3); + this.ticCore.subscribe(identifier2, subscriber4); + this.ticCore.subscribe(identifier2, subscriber5); + + this.unplugModem(descriptor1); + this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); + Assert.assertEquals(1, subscriber1.onErrorCalls.size()); + Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber1.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber2.onErrorCalls, 1); + Assert.assertEquals(1, subscriber2.onErrorCalls.size()); + Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber2.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber3.onErrorCalls, 1); + Assert.assertEquals(1, subscriber3.onErrorCalls.size()); + Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber3.onErrorCalls.get(0).error.getErrorCode()); + Assert.assertEquals(0, subscriber4.onErrorCalls.size()); + Assert.assertEquals(0, subscriber5.onErrorCalls.size()); + + this.unplugModem(descriptor2); + this.waitSubscriberNotification(subscriber4.onErrorCalls, 1); + Assert.assertEquals(1, subscriber4.onErrorCalls.size()); + Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber4.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber5.onErrorCalls, 1); + Assert.assertEquals(1, subscriber5.onErrorCalls.size()); + Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber5.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); + Assert.assertEquals(2, subscriber1.onErrorCalls.size()); + Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber1.onErrorCalls.get(1).error.getErrorCode()); + Assert.assertEquals(1, subscriber2.onErrorCalls.size()); + Assert.assertEquals(1, subscriber3.onErrorCalls.size()); + } + + @Test + public void test_onData_any() throws TICCoreException, DataDictionaryException + { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + + this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreFrame frame1 = this.createFrame(stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + stream1.notifyOnData(frame1); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); + Assert.assertEquals(1, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + + this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreFrame frame2 = this.createFrame(stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); + stream2.notifyOnData(frame2); + this.waitSubscriberNotification(subscriber.onDataCalls, 2); + Assert.assertEquals(2, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + Assert.assertTrue(frame2 == subscriber.onDataCalls.get(1).frame); + } + + @Test + public void test_onData_withIdentifier() throws TICCoreException, DataDictionaryException + { + TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreFrame frame1 = this.createFrame(stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + stream1.notifyOnData(frame1); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); + Assert.assertEquals(1, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + + this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.MICHAUD)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreFrame frame2 = this.createFrame(stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); + stream2.notifyOnData(frame2); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); + Assert.assertEquals(1, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + } + + @Test + public void test_onError_any() throws TICCoreException, DataDictionaryException + { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + + this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreError error1 = new TICCoreError(stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + stream1.notifyOnError(error1); + this.waitSubscriberNotification(subscriber.onErrorCalls, 1); + Assert.assertEquals(1, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + + this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreError error2 = new TICCoreError(stream2.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Error reading stream COM4"); + stream2.notifyOnError(error2); + this.waitSubscriberNotification(subscriber.onErrorCalls, 2); + Assert.assertEquals(2, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + Assert.assertTrue(error2 == subscriber.onErrorCalls.get(1).error); + } + + @Test + public void test_onError_withIdentifier() throws TICCoreException, DataDictionaryException + { + TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreError error1 = new TICCoreError(stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + stream1.notifyOnError(error1); + this.waitSubscriberNotification(subscriber.onErrorCalls, 1); + Assert.assertEquals(1, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + + this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreError error2 = new TICCoreError(stream2.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Error reading stream COM4"); + stream2.notifyOnError(error2); + this.waitSubscriberNotification(subscriber.onErrorCalls, 1); + Assert.assertEquals(1, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + } + + @Test + public void test_getIdentifiers() throws TICCoreException, DataDictionaryException + { + TICIdentifier identifier1 = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + TICIdentifier identifier2 = this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + + TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); + + this.ticCore.subscribe(subscriber1); + List indentifierList1 = this.ticCore.getIndentifiers(subscriber1); + Assert.assertNotNull(indentifierList1); + Assert.assertEquals(2, indentifierList1.size()); + Assert.assertTrue(indentifierList1.contains(identifier1)); + Assert.assertTrue(indentifierList1.contains(identifier2)); + + this.ticCore.subscribe(identifier1, subscriber2); + List indentifierList2 = this.ticCore.getIndentifiers(subscriber2); + Assert.assertNotNull(indentifierList2); + Assert.assertEquals(1, indentifierList2.size()); + Assert.assertTrue(indentifierList2.contains(identifier1)); + + this.ticCore.subscribe(identifier2, subscriber3); + List indentifierList3 = this.ticCore.getIndentifiers(subscriber3); + Assert.assertNotNull(indentifierList3); + Assert.assertEquals(1, indentifierList3.size()); + Assert.assertTrue(indentifierList3.contains(identifier2)); + + this.ticCore.unsubscribe(subscriber1); + indentifierList1 = this.ticCore.getIndentifiers(subscriber1); + Assert.assertNotNull(indentifierList1); + Assert.assertEquals(0, indentifierList1.size()); + + this.ticCore.unsubscribe(identifier1, subscriber2); + indentifierList2 = this.ticCore.getIndentifiers(subscriber2); + Assert.assertNotNull(indentifierList2); + Assert.assertEquals(0, indentifierList2.size()); + + this.ticCore.unsubscribe(identifier2, subscriber3); + indentifierList3 = this.ticCore.getIndentifiers(subscriber3); + Assert.assertNotNull(indentifierList3); + Assert.assertEquals(0, indentifierList3.size()); + } + + private TICIdentifier plugModem(TICPortDescriptor descriptor) throws DataDictionaryException + { + this.ticPortFinder.addDescriptor(descriptor); + this.waitPlugNotifierUpdate(); + + return new TICIdentifier(descriptor.getPortId(), descriptor.getPortName(), null); + } + + private void unplugModem(TICPortDescriptor descriptor) + { + this.ticPortFinder.removeDescriptor(descriptor); + this.waitPlugNotifierUpdate(); + } + + private TICCoreFrame createFrame(TICIdentifier identifier, TICMode mode, LocalDateTime localDateTime) throws DataDictionaryException + { + DataDictionaryBase content = new DataDictionaryBase(); + if (mode == TICMode.STANDARD) + { + content.set("ADSC", identifier.getSerialNumber()); + } + else + { + content.set("ADCO", identifier.getSerialNumber()); + } + TICCoreFrame frame = new TICCoreFrame(identifier, mode, localDateTime, content); + + return frame; + } + + private void waitPlugNotifierUpdate() + { + Time.sleep(2 * this.plugNotifierPeriod); + } + + private void waitTaskRunning(Task task) + { + while (!task.isRunning()) + { + Time.sleep(50); + } + } + + private void waitTaskTerminated(Task task) + { + while (task.isRunning()) + { + Time.sleep(50); + } + } + + private void waitReadNextFrameSubsbription() + { + Time.sleep(100); + } + + private void waitSubscriberNotification(List onCalls, int expectedSize) + { + long begin = System.nanoTime(); + long elapsed = 0; + while (onCalls.size() < expectedSize && elapsed < NOTIFICATION_TIMEOUT) + { + Time.sleep(50); + elapsed = (System.nanoTime() - begin) / 1000000; + } + } +} diff --git a/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java b/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java new file mode 100644 index 0000000..d31601d --- /dev/null +++ b/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java @@ -0,0 +1,109 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.util.task.TaskBase; + +public class TICCoreReadNextFrameTask extends TaskBase +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCore ticCore; + public TICIdentifier identifier; + public TICCoreFrame frame; + public Exception exception; + public int timeout; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier) + { + this(ticCore, identifier, -1); + } + + public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier, int timeout) + { + super(); + this.ticCore = ticCore; + this.identifier = identifier; + this.timeout = timeout; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TaskBase + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void process() + { + try + { + if (this.timeout > 0) + { + this.frame = this.ticCore.readNextFrame(this.identifier, this.timeout); + } + else + { + this.frame = this.ticCore.readNextFrame(this.identifier); + } + } + catch (Exception exception) + { + this.exception = exception; + } + + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/test/java/enedis/tic/core/TICCoreStreamMock.java b/src/test/java/enedis/tic/core/TICCoreStreamMock.java new file mode 100644 index 0000000..4606620 --- /dev/null +++ b/src/test/java/enedis/tic/core/TICCoreStreamMock.java @@ -0,0 +1,167 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionaryException; + +public class TICCoreStreamMock implements TICCoreStream +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public static List streams = new ArrayList(); + public Collection subscribers; + public boolean running; + public TICIdentifier identifier; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCoreStreamMock(String portId, String portName, TICMode mode) throws DataDictionaryException + { + super(); + this.subscribers = new HashSet(); + this.running = false; + this.identifier = new TICIdentifier(portId, portName, null); + streams.add(this); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICCoreStream + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public TICIdentifier getIdentifier() + { + return this.identifier; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Task + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean isRunning() + { + return this.running; + } + + @Override + public void start() + { + this.running = true; + } + + @Override + public void stop() + { + this.running = false; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Notifier + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Collection getSubscribers() + { + return this.subscribers; + } + + @Override + public boolean hasSubscriber(TICCoreSubscriber subscriber) + { + return this.subscribers.contains(subscriber); + } + + @Override + public void subscribe(TICCoreSubscriber subscriber) + { + this.subscribers.add(subscriber); + } + + @Override + public void unsubscribe(TICCoreSubscriber subscriber) + { + this.subscribers.remove(subscriber); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public void notifyOnData(TICCoreFrame frame) + { + for (TICCoreSubscriber subscriber : this.subscribers) + { + subscriber.onData(frame); + } + } + + public void notifyOnError(TICCoreError error) + { + for (TICCoreSubscriber subscriber : this.subscribers) + { + subscriber.onError(error); + } + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java b/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java new file mode 100644 index 0000000..c826a11 --- /dev/null +++ b/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java @@ -0,0 +1,85 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.util.ArrayList; +import java.util.List; + +public class TICCoreSubscriberMock implements TICCoreSubscriber +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public List onDataCalls = new ArrayList(); + public List onErrorCalls = new ArrayList(); + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICCoreSubscriber + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onData(TICCoreFrame frame) + { + this.onDataCalls.add(new TICCoreSubscriberOnDataCall(frame)); + } + + @Override + public void onError(TICCoreError error) + { + this.onErrorCalls.add(new TICCoreSubscriberOnErrorCall(error)); + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java b/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java new file mode 100644 index 0000000..55555a2 --- /dev/null +++ b/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java @@ -0,0 +1,78 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.mock.FunctionCall; + +@SuppressWarnings("javadoc") +public class TICCoreSubscriberOnDataCall extends FunctionCall +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCoreFrame frame; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCoreSubscriberOnDataCall(TICCoreFrame frame) + { + super(); + this.frame = frame; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java b/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java new file mode 100644 index 0000000..862a016 --- /dev/null +++ b/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java @@ -0,0 +1,78 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.mock.FunctionCall; + +@SuppressWarnings("javadoc") +public class TICCoreSubscriberOnErrorCall extends FunctionCall +{ + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCoreError error; + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public TICCoreSubscriberOnErrorCall(TICCoreError error) + { + super(); + this.error = error; + } + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// interfaceName + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/test/java/enedis/tic/core/TICIdentifierTest.java b/src/test/java/enedis/tic/core/TICIdentifierTest.java new file mode 100644 index 0000000..af522a6 --- /dev/null +++ b/src/test/java/enedis/tic/core/TICIdentifierTest.java @@ -0,0 +1,187 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import org.junit.Assert; +import org.junit.Test; + +import enedis.lab.types.DataDictionaryException; + +public class TICIdentifierTest +{ + @Test + public void test_constructor_full() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier("{1-1}", "COM3", "021976551632"); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + Assert.assertEquals("COM3", identifier.getPortName()); + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlyPortId() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + Assert.assertNull(identifier.getPortName()); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlyPortName() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier(null, "COM3", null); + + Assert.assertNull(identifier.getPortId()); + Assert.assertEquals("COM3", identifier.getPortName()); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlySerialNumber() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); + + Assert.assertNull(identifier.getPortId()); + Assert.assertNull(identifier.getPortName()); + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + } + + @Test(expected = DataDictionaryException.class) + public void test_constructor_empty() throws DataDictionaryException + { + new TICIdentifier(null, null, null); + } + + @Test + public void test_setPortId_null_notEmpty() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier("{1-1}", null, "021976551632"); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + identifier.setPortId(null); + Assert.assertNull(identifier.getPortId()); + } + + @Test(expected = DataDictionaryException.class) + public void test_setPortId_null_empty() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + identifier.setPortId(null); + } + + @Test + public void test_setPortName_null_notEmpty() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); + + Assert.assertEquals("COM3", identifier.getPortName()); + identifier.setPortName(null); + Assert.assertNull(identifier.getPortName()); + } + + @Test(expected = DataDictionaryException.class) + public void test_setPortName_null_empty() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier(null, "COM3", null); + + Assert.assertEquals("COM3", identifier.getPortName()); + identifier.setPortName(null); + } + + @Test + public void test_setSerialNumber_null_notEmpty() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); + + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + identifier.setSerialNumber(null); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test(expected = DataDictionaryException.class) + public void test_setSerialNumber_null_empty() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); + + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + identifier.setSerialNumber(null); + } + + @Test + public void test_matches_null() throws DataDictionaryException + { + TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); + + Assert.assertFalse(identifier.matches(null)); + } + + @Test + public void test_matches_same_serialNumber() throws DataDictionaryException + { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM4", "021976551632"); + + Assert.assertTrue(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_serialNumber() throws DataDictionaryException + { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM3", "021976551638"); + + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_same_portId() throws DataDictionaryException + { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM4", "021976551633"); + TICIdentifier identifier3 = new TICIdentifier("{1-1}", "COM5", null); + + Assert.assertTrue(identifier1.matches(identifier3)); + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_portId() throws DataDictionaryException + { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", null, null); + TICIdentifier identifier2 = new TICIdentifier("{1-7}", null, null); + + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_same_portName() throws DataDictionaryException + { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM3", "021976551633"); + TICIdentifier identifier3 = new TICIdentifier("{1-3}", "COM3", null); + TICIdentifier identifier4 = new TICIdentifier(null, "COM3", null); + + Assert.assertTrue(identifier1.matches(identifier4)); + Assert.assertFalse(identifier1.matches(identifier3)); + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_portName() throws DataDictionaryException + { + TICIdentifier identifier1 = new TICIdentifier(null, "COM3", null); + TICIdentifier identifier2 = new TICIdentifier(null, "COM8", null); + + Assert.assertFalse(identifier1.matches(identifier2)); + } +} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java new file mode 100644 index 0000000..2974e57 --- /dev/null +++ b/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java @@ -0,0 +1,75 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import org.junit.Assert; +import org.junit.Test; + +import enedis.lab.util.message.Message; +import enedis.lab.util.message.MessageType; +import enedis.lab.util.message.exception.MessageInvalidContentException; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.MessageInvalidTypeException; +import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; +import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; +import enedis.lab.util.message.exception.UnsupportedMessageException; +import enedis.tic.service.message.factory.TIC2WebSocketEventFactory; + +@SuppressWarnings("javadoc") +public class TIC2WebSocketEventFactoryTest +{ + @Test + public void testGetMessage_EventOnTICData() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketEventFactory factory = new TIC2WebSocketEventFactory(); + + String text = "{\"type\":\"" + MessageType.EVENT.name() + "\",\"name\":\"" + EventOnTICData.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"data\":{\"captureDateTime\":\"01/01/2000 00:00:00\",\"mode\":\"STANDARD\",\"identifier\":{\"serialNumber\":\"010203040506\"},\"content\":{\"URMS\":230}}}"; + + Message message = factory.getMessage(text, EventOnTICData.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof EventOnTICData); + + EventOnTICData event = (EventOnTICData) message; + + Assert.assertEquals(MessageType.EVENT, event.getType()); + Assert.assertEquals(EventOnTICData.NAME, event.getName()); + Assert.assertNotNull(event.getData()); + + Message messageDecoded = factory.getMessage(event.toString(), EventOnTICData.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(event)); + } + + @Test + public void testGetMessage_EventOnError() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketEventFactory factory = new TIC2WebSocketEventFactory(); + + String text = "{\"type\":\"" + MessageType.EVENT.name() + "\",\"name\":\"" + EventOnError.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"data\":{\"errorCode\":\"-1\",\"errorMessage\":\"une erreur\",\"identifier\":{\"serialNumber\":\"010203040506\"}}}"; + + Message message = factory.getMessage(text, EventOnError.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof EventOnError); + + EventOnError event = (EventOnError) message; + + Assert.assertEquals(MessageType.EVENT, event.getType()); + Assert.assertEquals(EventOnError.NAME, event.getName()); + Assert.assertNotNull(event.getData()); + + Message messageDecoded = factory.getMessage(event.toString(), EventOnError.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(event)); + } +} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java new file mode 100644 index 0000000..0431191 --- /dev/null +++ b/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java @@ -0,0 +1,34 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import org.junit.Test; + +import enedis.lab.util.message.MessageType; +import enedis.lab.util.message.exception.MessageInvalidContentException; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.MessageInvalidTypeException; +import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; +import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; +import enedis.lab.util.message.exception.UnsupportedMessageException; +import enedis.tic.service.message.factory.TIC2WebSocketMessageFactory; + +@SuppressWarnings("javadoc") +public class TIC2WebSocketMessageFactoryTest +{ + @Test(expected = UnsupportedMessageException.class) + public void testGetMessage_UnsupportedRequest() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketMessageFactory factory = new TIC2WebSocketMessageFactory(); + + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"coucou\"}"; + + factory.getMessage(text); + } +} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java new file mode 100644 index 0000000..2ddd38c --- /dev/null +++ b/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java @@ -0,0 +1,239 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import org.junit.Assert; +import org.junit.Test; + +import enedis.lab.util.message.Message; +import enedis.lab.util.message.MessageType; +import enedis.lab.util.message.exception.MessageInvalidContentException; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.MessageInvalidTypeException; +import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; +import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; +import enedis.lab.util.message.exception.UnsupportedMessageException; +import enedis.tic.service.message.factory.TIC2WebSocketRequestFactory; + +@SuppressWarnings("javadoc") +public class TIC2WebSocketRequestFactoryTest +{ + @Test + public void testGetMessage_RequestGetAvailableTics() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestGetAvailableTICs.NAME + "\"}"; + + Message message = factory.getMessage(text, RequestGetAvailableTICs.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestGetAvailableTICs); + + RequestGetAvailableTICs request = (RequestGetAvailableTICs) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestGetAvailableTICs.NAME, request.getName()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestGetAvailableTICs.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestGetModemsInfo() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestGetModemsInfo.NAME + "\"}"; + + Message message = factory.getMessage(text, RequestGetModemsInfo.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestGetModemsInfo); + + RequestGetModemsInfo request = (RequestGetModemsInfo) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestGetModemsInfo.NAME, request.getName()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestGetModemsInfo.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestReadTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestReadTIC.NAME + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; + + Message message = factory.getMessage(text, RequestReadTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestReadTIC); + + RequestReadTIC request = (RequestReadTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestReadTIC.NAME, request.getName()); + Assert.assertNotNull(request.getData()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestReadTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestSubscribeTIC_allTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestSubscribeTIC.NAME + "\"}"; + + Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestSubscribeTIC); + + RequestSubscribeTIC request = (RequestSubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestSubscribeTIC_oneTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestSubscribeTIC.NAME + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; + + Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestSubscribeTIC); + + RequestSubscribeTIC request = (RequestSubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); + Assert.assertNotNull(request.getData()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestSubscribeTIC_severalTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestSubscribeTIC.NAME + + "\",\"data\":[{\"serialNumber\":\"010203040506\"},{\"portId\":\"1-1\"}]}"; + + Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestSubscribeTIC); + + RequestSubscribeTIC request = (RequestSubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); + Assert.assertNotNull(request.getData()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestUnsubscribeTIC_allTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestUnsubscribeTIC.NAME + "\"}"; + + Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestUnsubscribeTIC); + + RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestUnsubscribeTIC_oneTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestUnsubscribeTIC.NAME + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; + + Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestUnsubscribeTIC); + + RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); + Assert.assertNotNull(request.getData()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestUnsubscribeTIC_severalTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestUnsubscribeTIC.NAME + + "\",\"data\":[{\"serialNumber\":\"010203040506\"},{\"portId\":\"1-1\"}]}"; + + Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestUnsubscribeTIC); + + RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); + Assert.assertNotNull(request.getData()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } +} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java new file mode 100644 index 0000000..dc8068e --- /dev/null +++ b/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java @@ -0,0 +1,161 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import org.junit.Assert; +import org.junit.Test; + +import enedis.lab.util.message.Message; +import enedis.lab.util.message.MessageType; +import enedis.lab.util.message.exception.MessageInvalidContentException; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.MessageInvalidTypeException; +import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; +import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; +import enedis.lab.util.message.exception.UnsupportedMessageException; +import enedis.tic.service.message.factory.TIC2WebSocketResponseFactory; + +@SuppressWarnings("javadoc") +public class TIC2WebSocketResponseFactoryTest +{ + @Test + public void testGetMessage_ResponseGetAvailableTics() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); + + String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseGetAvailableTICs.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"data\":[{\"serialNumber\":\"010203040506\"}]}"; + + Message message = factory.getMessage(text, ResponseGetAvailableTICs.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof ResponseGetAvailableTICs); + + ResponseGetAvailableTICs response = (ResponseGetAvailableTICs) message; + + Assert.assertEquals(MessageType.RESPONSE, response.getType()); + Assert.assertEquals(ResponseGetAvailableTICs.NAME, response.getName()); + Assert.assertNotNull(response.getDateTime()); + Assert.assertEquals(0, response.getErrorCode().intValue()); + Assert.assertNull(response.getErrorMessage()); + Assert.assertNotNull(response.getData()); + + Message messageDecoded = factory.getMessage(response.toString(), ResponseGetAvailableTICs.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(response)); + } + + @Test + public void testGetMessage_ResponseGetModemsInfo() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); + + String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseGetModemsInfo.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"errorMessage\":\"error\"}"; + + Message message = factory.getMessage(text, ResponseGetModemsInfo.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof ResponseGetModemsInfo); + + ResponseGetModemsInfo response = (ResponseGetModemsInfo) message; + + Assert.assertEquals(MessageType.RESPONSE, response.getType()); + Assert.assertEquals(ResponseGetModemsInfo.NAME, response.getName()); + Assert.assertNotNull(response.getDateTime()); + Assert.assertEquals(0, response.getErrorCode().intValue()); + Assert.assertEquals("error", response.getErrorMessage()); + Assert.assertNull(response.getData()); + + Message messageDecoded = factory.getMessage(response.toString(), ResponseGetModemsInfo.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(response)); + } + + @Test + public void testGetMessage_ResponseReadTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); + + String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseReadTIC.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"data\":{\"captureDateTime\":\"01/01/2000 00:00:00\",\"mode\":\"STANDARD\",\"identifier\":{\"serialNumber\":\"010203040506\"},\"content\":{\"URMS\":230}}}"; + + Message message = factory.getMessage(text, ResponseReadTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof ResponseReadTIC); + + ResponseReadTIC response = (ResponseReadTIC) message; + + Assert.assertEquals(MessageType.RESPONSE, response.getType()); + Assert.assertEquals(ResponseReadTIC.NAME, response.getName()); + Assert.assertNotNull(response.getDateTime()); + Assert.assertEquals(0, response.getErrorCode().intValue()); + Assert.assertNull(response.getErrorMessage()); + Assert.assertNotNull(response.getData()); + + Message messageDecoded = factory.getMessage(response.toString(), ResponseReadTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(response)); + } + + @Test + public void testGetMessage_ResponseSubscribeTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); + + String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseSubscribeTIC.NAME + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0}"; + + Message message = factory.getMessage(text, ResponseSubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof ResponseSubscribeTIC); + + ResponseSubscribeTIC response = (ResponseSubscribeTIC) message; + + Assert.assertEquals(MessageType.RESPONSE, response.getType()); + Assert.assertEquals(ResponseSubscribeTIC.NAME, response.getName()); + Assert.assertNotNull(response.getDateTime()); + Assert.assertEquals(0, response.getErrorCode().intValue()); + Assert.assertNull(response.getErrorMessage()); + + Message messageDecoded = factory.getMessage(response.toString(), ResponseSubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(response)); + } + + @Test + public void testGetMessage_ResponseUnsubscribeTIC_allTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); + + String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseUnsubscribeTIC.NAME + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0}"; + + Message message = factory.getMessage(text, ResponseUnsubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof ResponseUnsubscribeTIC); + + ResponseUnsubscribeTIC response = (ResponseUnsubscribeTIC) message; + + Assert.assertEquals(MessageType.RESPONSE, response.getType()); + Assert.assertEquals(ResponseUnsubscribeTIC.NAME, response.getName()); + Assert.assertNotNull(response.getDateTime()); + Assert.assertEquals(0, response.getErrorCode().intValue()); + Assert.assertNull(response.getErrorMessage()); + + Message messageDecoded = factory.getMessage(response.toString(), ResponseUnsubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(response)); + } +} diff --git a/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java b/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java new file mode 100644 index 0000000..948283a --- /dev/null +++ b/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java @@ -0,0 +1,39 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.netty; + +import org.junit.Test; +import static org.junit.Assert.*; + +import enedis.tic.service.client.TIC2WebSocketClientPool; +import enedis.tic.service.client.TIC2WebSocketClientPoolBase; +import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; +import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandlerBase; +import enedis.tic.core.TICCoreBase; +import enedis.lab.protocol.tic.TICMode; +import java.util.Arrays; + +/** + * Test for TIC2WebSocketServer + */ +public class TIC2WebSocketServerTest +{ + @Test + public void testServerInstantiation() + { + // Create mock dependencies + TIC2WebSocketClientPool clientPool = new TIC2WebSocketClientPoolBase(); + TIC2WebSocketRequestHandler requestHandler = new TIC2WebSocketRequestHandlerBase( + new TICCoreBase(TICMode.HISTORIC, Arrays.asList("COM1"))); + + // Test server creation + TIC2WebSocketServer server = new TIC2WebSocketServer("localhost", 8080, clientPool, requestHandler); + + assertNotNull("Server should not be null", server); + } +} diff --git a/src/uml/TIC2WebSocket.vpp b/src/uml/TIC2WebSocket.vpp new file mode 100644 index 0000000000000000000000000000000000000000..8f95581974cdfd57fb4a5c68caa4b4f7a0d29616 GIT binary patch literal 2255872 zcmeEv2Vhgx_wc*#c0(zvEl@@&8@iJ2LV@m?j-;DW(l%|=rcIlqEiKC=WynyL3@w1NElV72iXEAinUE$n zmm~}dhPoM=SP+@%DX-GVN@dU>e?X^1N-_(e#mxfJ#fhRwah_*#Y#yNjM2$-m$0o+7 z5=?({eFu1oW8-3>xzVu_F6Bs~0b$tAUDU6o9^+8DK)`urMwd>B?A`nHL6dtjJ(w;| zONx!oEJ#d^OJhINoc?2nwA6hJZB2bA3<_d*ZA?@_cC1)Jh|P3HC_B)U`5(tfkIalt zC`e6X)0@?Cri`Mp5>qV0NFZbvmKhr<%1IL^dlo8{3YoNumAvH;p3CV)ISw*lbb0KgtQ14L*e1^>11!{>Z@cI$?^IcTpAnLA7?&CX0N$PNmQ zkF1Hx%FW10$xMuv$W(RmBAL)%AQbougN6P=fB#^Ce;z!*yU?GXB;3oA$Sb^Br7V{f zX*|a{J2|Eso@3XlEQ>Yl~&3k;z-JBnMy5JYh+bLvdo5RS%iPf*%_)qQS@OK z6G?6iT`*~K4ZnSs)>$Wsz+b>m(jrNqIQVa*xx^+1WPZL{xJcRbr#s}&l>WcaA(}|F zT3IBQlCct7Rh%tX%M0c3t!Rkwq8>9i6077Ixm1x;5?NKOQp$@n$u~ojrjpC5G)!KP zPo7m(1!JffBF533kSR)uQ5MB1t2701 z(n`4k>JRfkp)7q&+IW?;x=dcAj;tbWkxSL`s?vzemWMEz5t%A!l^WWvghk*P@@=-k zR{+|D;9u}vd=p>6m*MMt2A{yk@L~KB{s8aB@8GR?BYqvP!7K0*yb#aDv+y*m#}jb_ z)?g(r$1*$ykHA?t4JYDg9Dzfy01v`_a1Y!Wcfiir9wYi8eUJWuzDi%9&(L4dN9a%J z{q%17ZF&p6fqs=h0~VJHj*ARp8Z^+X=Xm08<>8Yi1X08ca}0Q{;c2H=-X!vTKLGz8%B zrci)Kn*srT-sA`Hv!+1+4>$D#_-RuwfQOoT0Q{uM1K`I^?f^e(Y7g*W6Ip%_G&us? z-$Xup@9QT4+@~K0aF4zo;BLJN;4Xa?z@2(Iz_;}!0C(t%0B+Zh2DnY12XL!C6X09= zG=N+5NdPzMV*$RQ9}aMXelWn-^}ztw=>-5^(|ZGaRZr@FWb(^n~0-UQG32=@s2jFa- z7~u0dLdoZJgpxCL(Ew-Yh60?f3j_G9j?i$Lj?i$5ZXm!WT_1otohLx8j!^Fzog2VO zx^@63>RJOFud@d@P6taP)u^2SaIAJLz?JLM`dpG1~3`3$)~eI$GNi z;3%yNz!6#}fO%Rnr*gFbs2tLz*>wQ2Y5-&^07yy!hzkLvlWt5M0YH=sAcfQ`Sppy_ z13+RbfCN&%ctW}u(yP%?0HTHgh$PhgX z#6Bpbk-crmt?;&jEFVOWh1{6(fdr}IBFhF?L6-Knf-LDrkSW~cvc6W3#eE2}ksGt9 zw+RUpTDYA$p01?5J>(T_C5R++Z?qOGrQrcN3US7Ky1Ox3oZZt!1I;CWJJQkYr9dMK!1# zLXMCSwJ$qOc??Y!s&7d#1>EM;heCjoB#Fz;tZRr1j?9w!7v*OJ*W>HZoDC#Ilf}-g zBoW<#G-)Ixm|KBH1ZxF-k%Xjhh*KDc$PDgGLZZ28iV-9^MFQlUzLpiD(~#s%5>g~% zSD-wKhNaC$LqaN2(@O>BopW^|O_tDkBqW1VOko`&%i$^#lFg}_R6%_QqwRYVQX&ma z^p6XUi_gswhKi#qX*jP_BS}akH@8aE7iRY%5)#76tuP(A*y6)*3fK)vZT2%?`V;Ww z{|L_K$H)nt^S3cts+Jjd=2>c)CwcWWz6%tMHLSK(RfKyri{cdyC{`YxyAcd$#xh0Ru?lB2!^ZDeV0ov-MZQ4k;ZnmX^xMAwi;PP#6ydB*eH^Ge$6;Ab=lX zX#(s(GnLWG%4(@fQwdv1k*u;%rZViw$bdt$FtYdYp#+OPGn4ZoE#sjgfuvGaK^h}b z&F^L^^HYR-HIL&JZpIn&`RjxR>cf#srID9Ni!{a)m+*hN2MG5lP7WtY7*uMBywP|F zG>-{GcQYf;ILOjfvO2k}K1QmMGCO(mAY^D`CN-Nl1*O2Vkt))qaGo$9RGCe=u^c$4 zq?19z8eF9+d2x=sSW^}uV1{9JWwc5r)ySY+S*9Egt6qX&VSpeI-=}Q-V9we`5-=PF zlr8!6cY~dZeQp@=)NCVw*>(&@vPO|F zGk+*V{xN_Ym{HwWZ9yhhMFY|`sxT$Vf{d<@B6)^o2B%m)wWTRT2vQK_ud*QJzJr;V zk}TFoIY`88A#w_X<;kWmwYai4jKoxn)R7isoII2u%R?h$YfLdSLb5{$GOM^e+Vqtx zudfXXB*>tUu}TZFL==E;pmuSId4-iEd9+-fB8klp#$71aB2BrT*em z1zDIHiT6=!8^$X9%}8-s)o?)0tIdk4WU55+%m|JN!ZDO^{7}f(Vni3WO79}f(#3Zk*I=< zU-*pD!tPKd*Sgr`FiRihB*c=K$;rX_=6Z@VLWU74@juDrFw>DgLdZrGQ=h{zmX5;v zsipAuWWT?`n}4zbAl6?f<0%LEpXVSOL=pyR3zxSHAp@RDT^Oms|ESc3%zs;|4@LDs zeNh$;!(*u+_~ZVx6K(9>Jv@+J!tghZ@;2i?wj5!M5!sR)#u!}i$D%QVaLBrV?1|(e zPbAPXmGTE-tv|aEN$u?22Mt0~8rbwQi6TujFiIvJt#3-NyA>d=f&GJH)e3K=V;t4e$-&K~T zaC*szZZ50j)*Gafj}=)yiJa3^17nXdlFkTwJNJx1ElE0gDaBgpQ&U=o@qqT|>9fee^3$ z(++eSx+C3%_N4pL-n5VorH9fnbP}CTXVW8rdss$S(Hgpueukbx&!p$li|A$aDtaBg zk={nXOYfr((ue8e^eOrreTlw7-=QDSzhN8fgxg{_+!cG_{@51>;&9Z2CK~=gLh%>> zo`m<02ou-tnBETHQ9ba0>^>*lZ@Ullz!0e+G&I@Jd;=+t>w(#B_k|wnZFhnQ1?*1hfzfUE zjUMW6cUA8W@Fo!-*!`gI4Dgn|3&7iY4}f>{T><_`L=1L!^}sf_yQl8~@FyaGu(#0z zpUd7w4<}Rm4tfv-*t_cC+hgxW1Qz!0dSJrachUpX&%U!B_@ee*^x*(Kh)~16s~*m{ z_TBVw+_mqn2gam*Pd%{l?S1vYGPe)ZrvMDr110T4^uS5C4<#ZJ`!GGQ{q4i`Kvny} zdNIHVJL z={x8oTPy}hL;(Ix#Gp z1k`{y@gUr@V}x0qnutW9eG||DLV*r|&!DjzWlx`n7yBMKi9U~u@kIJpkOBC^9CU*j zdJs7LH&7jQnBGLmPzrq-I0M~r41J59ilit6okhz*Mz9Y#qr1R2SVFDBUFZX}ih3I_ z!W+<5$k@RDPtUA~wVZGgf&}D<5}-^EWFgD_2{J{Iz(yAO5u~a}%0kAM`VeG;zbMa= zJT{V%DWq!h*)jcnnLGp4^_G~C$qfHLP!LyYMrMSKW%&PPzCt!KhvEMR38Pua(E45^ z&jL+hq$PQ%*oz>=GDSg_>2R14qV7qMd9}%Gq|}ojQ{_qNmYBhU9t4?`oyA7xbSKCn zg{Uakd=3;8bR)<{O}5&C^iSwYkcndh%m?F8aZDm3I+1dbDU9f(Oc2QC9v3pAlfcRX z7L}qh7}1HOF+plMg_Z}olY)Z8h3ungfEz)|0&DUtF-wzO39@3WuRJv;ei_P= zjs#h+%#5}mE6Y0&WL{BHs0FDAZcmWav8B1dBQ+*3O`tDQT?i+;JR_HNG7l@on+!B8ZdX(Q#N&*O3%4()e@>%OXI6P5{rlf`k-?NhKC;gr)*5#)}NOse^-Ahwa=F znykj>NgB0)BY+T)kxnlsApzy-{+8S_$D*AOvYv#9(rZ`@jxZ^zhLBw(BvVyt9?0eO zB}y~`W=$#y5yw>Lh8o9Hd3~@3k$D?PLLwUrSsa{{BATof2B}4PiZ6>DAcpT2Wpja~ zDNj_ICt-Pgxg6!#;!vtD{S(A+NC3S&>yrOjMyY=Tj%OtO88`)Z!LE1!e4hhwZ;vjb;bl>auNkBv>yHOcdDuXxnBQ~8tg?ME-?$CU0_BNq1j#IIF*ZSnM+%_#?y z-+T1ytgl~v`TVY6-*b^Q58AY!{Y%aVE0$+3dO35!f(5f@_qdyq*yX1wv*#c9X1shx zYC`9W$}ac!>z9k>wY482ytyiR@x2?h_C308Yya!Q4QbCf=J{lQvcLShL!(L;3v!pP zmrFVbPfYD%@9*~A2M<%`d^v_1H?DJ>-7xzc@1C}<&amSXh& zQ+B>5Tg4QA+V`1M?6TqzeC~?Ke&7({7b0*r$2_ba2KUPvMnE;%80N8p%sM{a_ z6Z_+dlv6(dwj^@jK6ozW(hD{KwqEdL>j}$2z6V}I*}_Hu_kcYCT?88^?AjIh^)A@} zoH`TEJ#4jTu{(f7HvmDfo1pu`#sd8g`!VW0vgzp82Eejw#&OHuq(8HMa6UE&sqD^XpwF(xDLQ~PCdBZ+wCqa`cZQfgVC zv-N1%iJWKwt*u9s)pMe0S`EhIkZ~0mZtXUR2BKEL5c>{R6DwYN=XqP>kx+P`9(#gi&9O+B*vsG8Ev;JvC=?oQ$h9v zaUL?Oe`-XKv%;`YrbF+MBI7^@RaUQ5RoD%}BTR)@desowC}cG*sY;bCFD}d#wblOp zqY(}X>2}sq@`VNbkpV|8x~=t&YS!)KpkhaD2c!T%@9^z3h}9$}az z0-q&5R%~XyaCj@Ake`;A3RNeT2i~5^iK!+%f3~)FWHh7TZ!iF02UzFXrN-tM4Gq`~ zy8-E~>{yFaBF+_0RvD|KPFG3Gw+FYrgF@{LZ62*!WqYJVlQ?g>Mq~$WvSXJS0(}?lIU}rn`v;i$m1TJ+71l;#_ zwQcO(Bl;n22gbOC%fqygjDv4)q&^I<*j%nLhLJhTP#NqRILSQwTk;z~=062YuBX4J z6nIL3|NkiPbpHSU*XK`L{FDMF3OK`N-;Vy8!e8Re^w(I5gE2ORJ^lA71^$;Q&<8vE zc(ijyU@@7Tz-CZX#27Wvv!ZFoB7vQwPna`Ab8xBQMvITC1%o=7S`BA@I8CXINB!T6 z2G~0K^dUJnOA=>FBCb`)qQtmpfQ_S1uMSL9UL|71a7>e{WP}YQj?KUzQKb4Z^>utq zeT%$kM;~`mcC%Q~kwk|+GMX;xkDyRP{X#-GEsr9+kyr&&XjD2~MA(CNSWf}h{wur> z^nDk>%hTUe3OuF2zl#Dy%*kJP?5rT;YzdOTh}c*`#$YSRXkhO7IP#YR8jYpgkqwa? zoCOQmQRr)AgZ9zWLDD}6J&X6!qv>IIC0P0v<5&!WLiz-a!D4rf!7>Qqr=Y=TAZQHz zj((tB&|$O>ZA0tODY}6!r$^A!@L-S;_JsP_)4$N);{-aG?oY>1kML+hUT53`MTwJT z4ZvhA4&%Iqa^6BXZ^4|mAkJGL=Pkha2CeMY2?x-L;0r?qwk%+Qks>dVrNHWLGX^S> zK>}kVv%%7%npkESnY;}&sFzUvd*NghB@(fh_Shaf{cygW-Y}S#*rK4MCwR(o%8^AEMT`I4Y>2ts$%>j$yl!#lS3;RwULaJ`YMY$g|HGIg@W#r=~Eh8nqe`Q>CHo46~4*d8Q_>Nz&0d8K65+wLK0GP!B;dO{kSF(2YtbC>$U z@KajN>K}%_CA=Dy_LA6dus=zM@@XR>i)6E7{=}=c|xgk3UdD&{nClE5! zYRGs939%Xi3WjK))evut58*9TD-sBU1W3i?&}_W)hn{4tz%#fr!yi->P+MzRQ@fxS z4QX<@T~a;R2^6H9(0d4Ude5Tw=#?-FHK50bXg_@NkN-%%Pk;Y83Xmag{<<4ar*SkL zN41JpNXdD%E$)MikBmp9Kt3YC4GR!!>tM`euyu5(b@UK=2Ic<5nS@){gL-jPFlCIFYG>hFT}a7?Zkjnq|pOc42pNRTD+O^rj{8C z==9jq&8e&wZVnyL&=JZWU@Ds{&}T{DxdELyMRUVgiAd4hK#trVW@kcP0NsLXOh&PR zo4P_p*ju?zU@>WqEB>ZT*^4pumaz*`sZ4_`O_9mLDXUyEzR(}1TG z`0u3vcA;E@YHG3q6ei>P;OGDwOrbae0tpyQKqvu2u#mE4Uc2|G65m@5;Z#z zz>J|dnKe;gelpW$b_WNXGt$k7VqB_(;a8hmU0J zfA~no+=Y*13}g8I6&U*;K9aHj;UjyI7V(jc{SP0>*#Gd6jQtNE$=Lt!k&OKhAIaGN z@R5xD55Jj=RSzG@nC0-1j6oD1$=Lt!3u5el_(;b7ho79W(c-HyGxk4xb!f)^hmU0J zfB49Dgz|i3TY|)$j4U3W(+w183gLSlLDOOI2appp-^U3$2O)+-HZQkYIG4{MN%%fU z&`6UB65qWD0@kM86l?VcrF|QQ?ZJ0_f?(dzjAt-@F*Gm`4DD=I=dU5RN>Vkx%M~;S za@%ROou>$chlaxg%>NAV%uW1qU%=nM$^SV141bLG<2`sM-i9~f_4rkg11yCVVjg}D zbcCD0GN2LH;%Z!hOF(a6B=G#iScDV6`{z*bBM=Ddz(DZ->4`nSlCT|a4IV&g@F)0y z{*k^39Knn9S+M;5g8m%5fF7Xtf<5qddNcR|T}!_LR>F(v`QQn3Iz1V<&*SJi@CB-% zOTnUe6rBs+KvU^NurnS;hk@53AG#millGupX&10iz~~YB3Ef85(RZ+NokYjbr|3hp z7wtq_(VM`_d=X7X)o28YLjvG*f*k~9iz%9ViS&-NC`)Aa)*==bs;nW$SVLx6Lo)vO z_<1spZum&X(G4GIFx}%-3Cy*jwl6YpF#{Jda3KR1Fz^Ki&S&5}2F_*R90ty2;PVW8 zj)AinIFo@h7&x7Q&oXct1E(@@3IiuI5UgFHCR*}EoJ7Dx0wxeJo`7)#G!oE2z*qw6 z38*8WmH-U_Y65Bqs3t&3KotQM1e6m{MnEY6B?J@`P((l>0a5}A2pCO3E&(|NhzZCb zAf12|0+I>dQ< z$(w-w1oR`I4*|Ui=tY1R0X+%uB%lWY-3jPMKvx3X32-C8m4J=}bReKT0qqEAOMnXj zZ3u8Cpfv%l2yi06kpKq*V2pPpokhcw%^328zJsR%=7}*^w!<$|VEK0&zYKnUKf$LV z#nazY3OuF2f1Uzea2PU)ow-U#c7&5_3L?rzwnR3|QYo?_VqPN$5TthQh>;4@ZE!C~ zmJSfn|3$P1MSFl3ZTS0lHhG-3|Km+n6Tu&*3S`X|Bh@XN-aPneM*n1=frk%$wk1n= zJRn`1@M-elw2uxYZ_jdat~s_fee?DUn{WH|M+0vi@;T@?Fwtl8mLu`7)jAlUT$2Fp zBYIYowB9Whot>T~O375oOonQIzHrA@4)^Xk4oDK+gIcWf-nQk)hiz$UY*FdO6l3wv zXnk8Mo^MX}=ZmlTC?&-&=}>Ljr^!A^-XHlv^GW&JXUYSN<DWbc3Z!ubz( z_vroNeBG4o5pxy|brB8ge%tQyglk5i`E z;FcIS{m{2peYfR@GZPp8*h$>4)5D7qBahrz7+Ue-rO)quRDbiGn;uukE&cdJiDt1f zYHzQD!sxv-lR}To=-ExRth{V{pI;(7mt9%YCw$FZ-{4_w6Gfj#JJ59pD<8!Aes7cf z(d75&%AZ%Sib{QM&t1p2&g^(5vF*5N^9mnU4(Z@_`dq-?AN4&4pK`ta;dWj3I`$m+)h*S4!p8T-tJVFj|b1#^7Xn&uinc!>hsaZpO)nK0$B4O^;m`N3 zSR8Whi;rJ=cl4}x(VM~Dcb;iHKj+t7`+5cCpW9TLdUbI7UGwT&&G_E7eA-U!w~?pk zY&-B)uWny#E_%K!;>NXt=!aju)aqJfoYRZ1|2F)n{K2~4?ZwW^AGK=@bAg%v)N|mV z9sP*y=qdls`EM+%34EAJriPPoHbv=R>o;qNOEW7^%W!%j`?Lzo)F=Fq<9{>d)d|z) z-F+=!Z}Bak3*X%MjUl#Sx z{h?@MUGJ1JpTClFuj|8i9K~%f3mjH%U9i0U-jDBgdv)ToXP@!Tbc>no&~e)RvmR|f z?Cx}a?~++jUf*?kcXsiqu%F@rM)>y=hA)pfKfw2$-d*NQ{@gEp#LfD$f|lB??-{&0YV_te1|DwP`IzT}$UMpItlBQi zUl{MH@Hrn48og?(>$;PHZ$)=mx+$^a>yKtXd>Fp9qvl4RnDO<;j}#nz1ZM*cPZbxm(R(`tZy{6l7C%?Oy{pmYR7f$R> z==#cQA0Db{H)fT@<@$3+I?%t&+TZ=$3%4kzv)NaVy|8-!SEs*=^17e2@w`So!TrOZqrO=G&aK-0BYS#G>(aaHh_v{ZPQ>s2wXkq+ z`KUh6U+s#HR=?e0!%n*rRnLv53XV?s;)S6XhuG{BddPO=z0>KP_?6SvjbC!w>F||` zFHgo)uKs56QB|w6euYb?yQ!-e?>^Ulz@b;SCV$)d@TOM7zHGZ+`r42;-`sF-{E+Q$ zcuz;uHm=&BfA#yu>pjLEZhUz6_Wet#(&s*N)0bAfU)=tBuZp=e7e4 z8MZn9p^Z=zm2O&jiOwNxFs=9t5zELX%(4J~I`4GV692YSet6h%!JFew%o@1WefV2_ zdX9ME%u#Q-?;!hHa?A___-M`rNjTQ0Hi5lB3Gt*zb*ZpMDfFX*JdnV2dTZ}?S zI3Hj6Y*?wgI519g)^0`bu!nAP0z+rRd;#}2p5HA>-vs* zUh~!YZ#Q3;e_iOjWp~adyNvhe-_|5$bSQgqxzCcSUcEn!EEqFn`D+`tUy2IRmwi2X z`xwWWB@6X?yEon67Mb&Xy`%l43DeFv97}lQ`0U0RKTi!fyJe5+#4)wX=cW68nT5mj zQFq7G-PyZ(^sdW?7XEZ%Q`o}z#hJ5{A6<9xxH&N@$Tj;N5ACI)KX-qWJY$AJ_xZh~ z*~wX*?%wuJIX2?CyGK2f5}n4E1% z3Dh0mwYPe~+lyDN{Z-{XdS}1cpKHGxzw*edOLO+WqPo#7Pyden+!v}(dn&%|yRF+h zgNH5LmF||>$L^=twTjNuhaFw~dv%{fJKO7)+^6RDk6!hAPF?y>F?H0<3nzE~>>+zk zaN-E?B@N$ya6SSz`pjj%$4KVW$0z75FFuEtt)sx#M+0v%#d5W;LR#ostC1^wHBxm2 zKYh5DwOHV+-k_+$Xs7&jNA<=3;u`^C6$*JZ+!F>~HnOUSQ~WBX-UjFX>IlXs{uA9C z0DRU8;JD5NA`e?s$M+h|CI(+3#3G+u>01omG~g~XnY2>P&xAv1Zq)zp$Zfi)f$_f2 zXm*?Ln&4m50Hc2z^orl|I=<6zcCYXSeB55q$cy-i`Mm*ATJ8lANP-4ED+qxIfC%OMK8x!B4@nHQEfkVMJs4w^kCH_H)x6roW5!4QD0sR%M2=CC} z(^udg&~NCI-~s$I`eV2WbPv4~{DNFcA9ej+(fUogT@Hy@czQ;Ym2YE;EMeYDX&R@Yd`7Q8Kei3|?p8%ibhroCF9`IrQ z7Wgt>3qH-4fN%5J;NzV56d#LJNP$Yw7?h7PQ7TG6QGaC#u&pB3OWHPS!KTs1S33%z zt9B&7Lt60TYV#?%cGBjSHV<}p8?@N~H<2qSZC=*Go(sRE6@v#!Ik}F~p+YNyHTjel z*z~rowMp>2TN@A04&;JL+wEFV;j+1-je(fcw9$YOYoWe&eYL{@Bhe0n`1iGg0Z!0@ zA83bqa*3sbNJ~6a=4k@~U#SH}FT37ae<*jN7L0Bju+|%(ExFv%!9m*>LVdNppcJ7N zO#AE?Yl$PuU$q|ayjBZ-A038i!R4fVBf0d_{!J|iRqzyTdwAZaZ3oYTwP5CC|2nx0 z(|(o~hL+7$EijpF?a9TMwh}FPptR2-*JIkx*Mj1SZT=)kbQ0D!+d}YBYjddqK#zLB zm1*GVm1;OBovebVwQyvz`B@I&ZW-VXl|qzj#Q?fV0Yn1X>92v_wykpEH5+J)=KyJK zmS(`yWpMLpqX4?v)B!vR;Atoj z&E|RlfDJ-O77Ns)ZvpY>2Y&E65(sD$?E_C3h-lk&5Cr@(5S}W)I1#_y55THE0GxY+ zh|K}0YMbo^ulosQpYw#*mw~#rle$AL1G~YKO;-qb321FMp$j~{3iQS?;Oi4_b%mVk zfXuWKs7zl0BGW$tjp_I{0D1wPaV(G-2LnS7M*yjDiX(s;AUBrU15kqv6MhZoZewo) zPbv&gjCtE21hL_T0Jz_C3upn9;>Gw=(Di#j2f!6ZhtYCyl9+=1;F_>T(C0f%-@+T{ z1K{m=6}pS{ba#3Qa;8=EDC&9iG4euEnCXjXM@mL?1J*FZjKEQmxl0=fl z=;|fbrLd5h`ON*F#iBwsvVzfskpz_F=a_DH%1kue|5+m_jx{4Cf+$AaEiNrA%5oP{ zavh_@7Mhi=FvrXk$1>_}(aC8-OU&pRM*lB2Q8LzoOsQn_|4Ophj>cmO82!Jo8L?p| zTf$JWT*c`B#VZ2XRZ=kee`T^1R_PgejQ*d{w}gchXEXYLs#0~frJj;lM*lA^Et-wY zX7vBW!gw|^gwg*iu1jMfGt)f?Dd-!ri#)3tI01>Sdij8M$wSxC0)Yk|M4867c%;PJm2TJjQ(G4 z9LM*$FQfk#%ypga&*=YUa6PZ9Tu9}UxsKcG+7M()HplOKy)!|E0rLak5%zI|*e>m^p_{f^5tlW%hA?$N4x5s;hw_INXZ?ZCl zwIOBhWDE)l4ipGOg#n?={;|3;vQ|^3RKeCWGB8dM=%31l*l0FhZo(_MuJNNy`20kUbA6c!&!>1ee`tjld8|U z%akm8gP?5F4&1S&n?8d=1A~NtVa)I`9Hh919BW4-W-$8u2l@wFjz*m0*96n39J4#3 zcFe_I=g5qY7#G96nYcKd+_W3cni;?>WhQ_lIYSl76>?2OL~sx(#L?Oz4;p248aP_3 zG~8+$6erX)njgb6CBsjBBBn ztw*!;GILw8p_i+hf1jAy1uIFAkkFJp|BJ{Pzw~oq zUJG2kO1aw1)&q$s+?QS2@K3Pylw2lUrs-49r*n7_H)2+c5&wDqUp2SroED1~a3kiL zb{`D?525K4nocKDO`%Pt%wVH>0>>G)_#$B-%OOlfj@f%aW@5BNrmB+{$@o#iy)0p@ ztLT|;Zb2pNo@FXBWD|=cyh^#bXETd_3rko~KyYwipfQHky+w=rhJe7}kYGbn_OR+t zHZSAKxOK=4_ae139t6lEQ*lQ}7<`6vOT-YqMVn#ShMkeDzJ)e9PMBcix3;)H2nuLY znTbEG`oD?z!EgzdAS^WC|0?kf?F(T(qt@28IF+(eA}3N5_CeMp4rqQh;vQ`C!qZc- zS(B);)HF%|IAwruCM#uC`je+j6?1M~lbPqcW&b}|H)i*97sw|?DOM_Esc`*+LFQ7c zmSrj#8B8>ICWYNJqdY;hTL>jc+-%DsfC(!CJrwY6XH;D$m%K!Hag`OAu~Se&C|M>Y zJ6yhQLwGQE%iF-rDpF{g3LKTmibb^w4SdZ&YNKfYCIVUM|mj!z6j2)nw?31}g~2Rg|ifwN*wQfo~dJQ(D7+yrY{TU)S`WtY~{uMIWIu0RSkBi_ou3<#@Z?b552%)l&A zm>e@2n+EN*Gzi9KmAYE0f;(8)%{2C}ueo7=h4A3DQKC^PD~ywsp|)vmS|4<5tYzk? znL)>V|7q$BMV$fpS_+*))`LIsn>W$Lw(`}kF3qm*;$Fj*z`vr%qE7n#sO#!eJKTPs z_`B2di%+JBiV`QL;=zG$mh>%t)loj~hlS61biMWE7`L7Bos%A2lj@6ihz@occJNu} zE1mM$RkU|IdoS0fq%v8yu4%*QJE!e~JBEz!yhrOi)#u`uUkJzU zRj;mnYul-JLZyqxz5D#&+nzV?+DDAH_g=Te+jY^#*7nYh(chk1b$i6s&(D4CmhK{2 zd~TGu!@IMGj@&qKzV^#?HP>pd$WspY{%QF-QQ15P#p#oNt3Iy83yvz?Jg&ahE$J7z z>y(>Y)@GNjbDfxP<5B+F4o`JTg=ojux;a7DX3amot#D4xu#N)`d@&$ob@aA#k`cP@ z6>Dn-1{AN(&zce(KWqHxjz502{YdW#jj6AGu=C@#L$(B6YV(%-i=Cw}{2sdEc-YS8 zzgZz(llR7uwzId+`+4>$d}jHbfzy(l`##urWX~6Ep1Ym1<-5WqqB|drjhXaluf}fS z53xVJd%xAEF8A*p0fAqU-lYw6NWPU0;a^*3>5y7+26RZn`%Gj zLD|69UpcjPRpszs?|rl31N+V=qkp)0`lku?Uv2)je#5+7n$dal+BF{Q^=zQWh=CGW z@gwobv-5iHE_~f{>096Km^Zn0tHZJ_i*K#iuzqGraQ29WbLNb@QhO`;;e@$1{brtZ zwEyM9na>=FTvzU~a*%zi(%sJttaHwF+C8dj^1|8DoR_mB^#c;iZP(=QZP)JbJ9C`S zi|Xv$4yQ)=ue95>sr{+!-nXyW3zK>}#m|cCKSgo)(zf}HD`h^STx!r)>x7Gz1WteT z-BmlRe4hQO&hP!E{m!c3@&x~Xy^L*@_kzuI?O!3Z)nr~_B|pS+S#nOyP&vw ztNZcM%eHMEy?(*GBX2EAJ@DYG9y4=}{WkKQ%UcVQ*YtUJKIsuB)Q9Q;4n5$%$vPJK(=K^S$UJJ7x9I64Pp?+tVi$mS2gZQZ|shoBIU^Gk6Zo{x9nubD4wA&Ml3=p* z2Cxw#K(M6~I290V8saqc85fZQHRWWQ3kXi8qX5ClbTlB?nc8&VH6LsF2ptb$Q^7;v zCO%>^A2EfGn96Id4dG02D%OU05(YQSKqGLlOuXhtlf%AJ)iAQfkWnp$jBYWczzZi@ zF9*J!hC|>8BHc4q#axSAYsfrn$oy`2ShM8j%yXW=-E_ti#1TJ190|k!1BZVwiVm+-zY3X`ANkU|LYytmzJa3b$Kla^! z-rRfLxIxirqVzOLVrHz-r<{pOkufpiSc$|F$P$y~_gDh6FKaZqk7J|I1IT0apg}a; zt7)Q*<%UM4&6Zd6OLK}?Lakk`2ud`ln|nY2tvB3l$YjX%X=eq6er;Y3H&IK3H#j?N zPWbd@*AZH~g^+X*`|?5VorB(nAz<~YLC7tW+^gtmya1b7yXQWgF68nXlJ@T?M`Y7Q(?tPYn3#Nd=!hx!}8}5PbM3 zz?V-Q`1E-OeEUoXA3yWK*UvI~6}^t$2-<(|!o9->>BFECcnbCzm*^XyDfoc?4cmY} z!nWW?s4MmY|AfBaT__w6$8oR+5#wAu8oZU2gD0XoJRa-tH1JO}AHRr~<2B&5Xfv#9 z@8J)?m(k~N$I;i|-{=ayiSOcH9v=$y@B=j|${$pxC;_NbQ9@9$q5?qU3e|uX7Tm!O zAP97{P&VjoQNf_gMTLNp7ZutMaJirdMhyl9F%(I-_CtgKB0!~#x(M2M|H7Py5`~h; zApoL=0*D69HY#Q~fcQwL%WF~aln@P1{xJZ`Kn)K0#sP?m2cRZOCy9vwl9J%{6|x~o zhD`|C02>d~3H0cwlym?hP^&{PiUF*V0Eh!sJG43r0#b7Tq=OC~dMOV;ml5zP2E9Dg z3REIdIjHGTk}&`>i{aG;lt)ml9A2|30o<+ukX;KPXDoo+@c{BBK@>+Vg2xeB8+a}u zx`WgxqDM%L)`GShDkS=a)McV^h@?dOkSZX$iPRWvCxC@o(lvL979&+m^cpFdmh?_H zE$N<8tq;IUL?@Ce(~{1SYYF+vwV(<}DYQT_s*-41Qa@@5X{v}`CUVviLR1s2O{#|I zaZ)NRX>vQF>q)7J4k%Ttl>)5O76JZ>Rt7L$OPV`GTL$=gZ8<#m(JJ71thO3pqgD;@ zp0*C~i?yHtNsZHv12|qg5ikNBJR+nc6mZhn!^=b+w4IuygTY8WqiYXPt8)c-O$V(; zYjvPLNa=K-Q;6Jkr0q>QFMvPk`T(4w>kse;9ihN9oiFqKM>*g$3ErRn{wWH~>W8-Fz+{W!l`w&|(dVn-@S`>p(=#FoYn*DHe8rbrrijPDPl^%*g~r7h z6qKwgW`qfa0YRYw0!Go^x_pI13G@#Y806*6N~^k+1ObGqi~%Um+~! zh;cj;CN?ACU)N{hP|dnH$4HX^hV|TnHVKz**3}GH`YgN#B|To#fm_EGYXC~dlREz*fdjYcIe ztkuZkWg23J!$5;q8|@J1~mynjYyQKU3dQNr;58S)t^m9CNziD(SiDHkwO z2cl9&$Xf~>sRBi_Ae}f>is1`0{s)UvlOTfK>QFLY0tV%34O830PG1a+d zeZTr(jh8`W;gu;*Pq5&HDJDGcei?cgYlDEP0X{vxJkg~ z(u0+`xi2ECido+ZShozx8R>%IiXYgm*q?w+fL#Ube{73kD}<-R&Hx{TxlPYQR8AGT&GJOAgfGa|Z?0}y5cWs<*4Btx#>1!hVGkJA%WZh`Jd*=&R`p;KwKbfRm(d&ZF=eu2D>wnsObWrV-G5t2?M(au@uKcZ+ zyYR~XO}^hb3KME3qhZ^#&#v!t$05@7lh4mzYFdA#aoT$Sz^%)a-IsZ8I$*nOoYMl= zqO>__!m#(JkC=Y5e#4mG_g6o%^Iv!IgL$y;ESfyp(KcvCN0*{%&|EH-NgAri>Z8$s zG7PI3FDy8%I%RN)(LH?L&aOMIKa)Nsx@_?_E!{xvAFW@r`C?K9ExmK|h1PY2BZiOr zaO3a4H=G>xL)6Xf&pHGzch1@TRr%Skx6-p$Oy4o`UVhpqx9=_dGU8Fp6nS`NJMTK- zk~L*pr_5FE@%p^T_ndTwXSbdw{O3fzS-YhwLv_7f`sVb-ZT9})bnN)gL)s_BwcV03 zrbot)ms?BUPF~@!diL<;dGim*onCzV(B)?qFX-{T$NpDbCO*G;)uBZtj-FSJJ^yAx z|ITG@`!d_u29(&W7-L`e?Jo7<4lmYk*${HgJ7%?C&v!p=f44*RhLx>mPFefbu!G)j zk9i}p|Id5-EJ7b7y&p5hYsQ!lf|?G7q~Ct|NVlGkPPpCc_{u2ZyPK1`7ro)!I5B?b zg7{H6+L>d!PP%+F=TP{Ey*7M@&lgJ$Ul}-i-i8gO7v#$BOAk)%z5mSc*qv+cMbDla z*1KGNafkBGbeFGE-tf#{_uH1G!`;sJ9KHNTy6%-n&o7)iv13H%ozwf)t@~cM_`q&q z-?4{!Hoo;k?$QaPMTyO%TYMUwqNmG&>Lf&7ud9$iAsJOmk^rfLEJ3c(HcJ}2?!}g3hepRu@XZra1NBS?e z^@wmg4_q{}KhpDf8 z7r6sH^Xy{zw4Z~OuZ;KsueS03+4b{F<=5mR>(a&F%;?kmwK2I1F5VB$O^rWxF8o|D z^=zeLqsy7Oldfy-uiLn5)P$ z;PO?w9hWZsQa<@i#30`bBigKc=Z5^gc>Mk4?l+E|KiqB1j4q1xO%cD%__dR)uKr|( z*e}5Cx02T{9a_?1XQcAChYwoa{jGfc+i;93GR*&23dKJ0)PEY0Pl3syPPXHw(|>TjwNdQc1sT5^yE{ku`Dtg5AqkKy@*zCj-Is9&+g5} z4t?j%?Q?FUZt`i@*7*U;rbp(!acujrO+j0HUS3^2toz*;WvyVao*aC7QrTzQY&%81 zRlK;}2kS;U{P?0WYSX@i9f6;<>eHdu!G!|@{PG5M_lQg{{`Emc>4C8s(qFHM)_dkh zowE6H%%w4Jihi5(QRSXvFHjLuO>o>bSR3B~jDUZIGUF5yXbw=k; zqn4fT>a|R$EqpgF&?g`}bM^DLH}!aD#o#sx6Q^dp>o90hXlrNL`2l&}@+6<#I7SfE zd-v`p=cV$m_I~u~gXv9W{RN|Lw(b4QvV^ZI9KKt$dC-~{CU0&R+oyNd)%AN> z^)C%KhUt#1P3ZHe$|GyRR|79E_1==?|Ev24-w&<5cslUi)@KG>d${g&UBTM^=ReJg zKDtX+<}~>iw03Rs;vZw8hV%{>S6(=umG@H0v^(o6byGha|H}mb@Q4k5bi|Mor9*#z z?ahO4pOlu5Q}(FZXUqyyuWP{Mi2bX@@pdI+mxcTc8b%d~4_F z(#z$VqX&Py`0c!(R)4cI=Em>OmANjP(>20nMe&fHeFlm~&YdKlzHrXOes&oLa$=WW zdFAcr#4*oLM!P#+n)y)n&hU=09e??;m*B^~wbb2Pzdzhrk>lg`!I!_D3pqbdcen<2 z3e5b+)Om`&gRa3p{|A3MTTFBsyHT$C-sC7AmYH0T9b~cq%1JZrP6)4+9~SOKuwLP& zILyhr1r0?(P>_F6Fz<|QQnxaqqluyv*$>GWV|Fi-HG4oIuvv1kZNjUVS@&2?Kw1pQ#-*vDIEgC0t}65!3xCs_T1E< zzeoqhw!#Wigr)J$&|#mh6*ADlLqCv=hiqsPR7e{$uJQMc;+_fARm|^oJ(tu$`gDJLtVgkC$_MTx{xbI8^e3 z!o2|bq&*Ge{;@`Yk019Te{KG=)a6^U^fHcnds_n)-1U746=Jwl$S_f%M4^;w_<`YG z4Eo6_@po22!y@{p$9xFGys2#Fa-{hR;Amr@L}z_}E0jn#QKDE`3vx{!DbmBe4EXkkjWkU5>t;WlvSntuy8MeebU|z@(=tQ zdjP}o@ux@rldb{p>+BskYXGmUnI<}bLa9o_4-EHW(Emp$Ve9y~5s_u0M4eJz%p*ir zxEI0x2|E0Z-Oyhg5iHk8kNqmV?@T+x_rE8|7pb1~Dm;P$Suf|O9o32uswd^F8%qLm zxl<>{qI<^=Autrfo#sxO@zW%-~?|RCVYE6U} z^QnB?wm7t z2n-7{SakoD&0uKDWO*ztp7>(`-Mi4x*V>Z5-PeD16>G7}CY&13ng~<-QT?=Asz|*8 zxP6qHBZ*0|sL!!q_%Xx1IB~tgIj_tZWw~k?7s5A8v1-!h=*X%J7^`J) ztCJt@6`Puwn3)(WF%-)fDG?65vFJZ29P|T{$@iZ|?7D&0-1BDCUFF5Xl)4P&L+qnxJ@T^oKbai(0%Lz|x+Sz((+ z;t%2>%~J6Y@g}n}9zHHy!KJy`A4s!^+1!YUE#}CJnABoKUdJXsX+*ONyk>Pw<<`;c z0?(@K{W0t5;|9k+GAvB2soZ#Co^2Q~mVW+g8)_p(ZA5aq6a5JW z?XZ8#PwU^2GllrwV;a}Iel*NKs+&(QEwdfrkCPUX#hJZeHd`~2!@XF6f0xiLvZ4Oh z*15mdJ#jIzYK5{vR?P3ta4%EH-=%PiOS4YsN=8%&<=jM+0x4bunaXn44pU$%^;SFIg>~5DoKxEk&(G zBS3%mGn8bU{W{;ygqi_BO^_3U{v@|9;&c(Xj!>!$*0=^*@}n4N*(_Q!N;XT!Lr8#< zT7NfEojyLUhA%Fk*zC^b(=k@6Zs5mFlEURTjyL<+YXk6yaV5{6A6c7Dt3Oypy5YJ+wp#m*4RjkE^zsmb@O!fa!0Vd;ikP z|CfzgIPy9U;n6}I9>$$YX3rwcBF1ndX8q;)AIPXhwe7r}{^ThmB0q$pKnHm95WU8l>>$n z50w*6xJ+bN7)kP%R{p@pRJ8}Es(k90%jMN8Tj=Z0j~ng<$bVf`oq>`zRBwvvjeODf z^m@4WyxC7@?@5{y1miE11oB2}9&3!7M<73rVFa2-wK-n1G1x3250P)$R`Kw8;XH)& zudkN>(b@j@7poQzd)oi)wN{tWi;P1b*ph(nR#sS!;SkXL!W^trj~Jh%1;vR#SZo7WX@&g^74NClSQ&~XEmS;{ zI17@K+gNzR5}ejr!v0m2cA~xq+1~3E^%Z#g=#2bOEE<7o&{VV(eT2@UyVUEnBi#dR zd6Q`=T~E(~n>n`Ahv@UvUbxpI9hZQ`?Od?0J&vz&{`P>a?R9*exGDEl8Fk5+my}YY zNAw(Fs5rNr-pXKe)%9kt_Y!pm9?M|!8zsra<-gRwC|_KkMR5Lkv9XMIeuB#gjj?eg zSV4R=E2dg%1GkMp!OhA-TPQc*Cn^hXV{$N6opBbP?@PUB1sh7eYXuud?XrR`pmr0i ze+XN>PL_f`v4Rz$Lsqb<=u=SBaO2s7CEzqBZgQEb!9=l)iUPcVc%Sf3VV9N{9c2X> z6V0SAWXCK@MvMn~e=ar!C0mIrLMc|TsYt|N%hhsouZfdluz?CS?8|FP8))WQ1b+_A z^8)Ib+H#SBW33>A8c0zB|F8;6QPHvZAcGCAW?>_PV8n(~-hvR{e9Ki2S@m#VO*rKy z0C($_!B{;OYnU98goPH;R@LE!crC=uh>o6|9oF;)MJu59$_0-9mQA1C4~a zKLzf{*^Cat{W*7OCpy?X^WTPhdcLC{;&!+%+!8VY&%J^#rWI^GwUNQ9edU&Av?>|@MzDO>k~A}NdA{zd!B@j8lv_Y@nZR;sMUDX6LP3}D zMeD7ZY)zfbteC~6Xt@<^8CqclD@Q9Ctc=|`<5e3})Y(kxAcR;&N9IU)qbo zatK+Nk2+hyN>LXp*h0h%g#hl(RTaRrmy1*f!T|FYD&i$F(|sNpngW|{WY`6A7M4r_ z_90~ja##IGw8siI3hlLmjYj+Mb;>(1EQ4i4E(pM>fOQLumlT+XqeNDXWlRpq85x#t zPgP?0##63=oOzZTJe1l0Q&%YJ3ffAK$4_npsB7&pv(>rcT585QPWF2;zwDoN#c6^_drm`&CvSrDZO0IhOnCa+IsE6v_hqzj>cN>6`I4*60YO|HUaE(t9(X=DoRR-updXT!`f+e7;aCv1fO6i0d*u|B5JhPZ$ooGdHvm`-`)~SVE{yiyUDvp|7Qiy_*)eWL zJtAshzOnCUcy&3Rm`KVT-u3fb>>Qg|Nx3(L{kC`f4ZkB1a=~RJ`ust8#JA3F3MEV` z6ZVMONGTI8i3t~#2`9ybOUi`D#e~bsgbm_su%b*jB__;;+S#CYn}Qo4yZHxfgPcQDV%`0ZM>GTR6CvQ*AA{Py?RxTlACM4Cqh&FSFYx)TG?alr_0L9gfL~5ANku?V1GtzLJf1 zAp*oqlVj_Ktj4QWZGW+IbvQt*13EJiD)XeG7p z61Aed;@F^-OKVN7T)e)IPg0Vo38hweAFmz{%jwO|4~##Oem3GNX?TT#b#cC}iS~Z7 ze)#-vkhc)_-{dXyV}J6!|NJ_$HqSMEdF7c-U7NvGIW5#=5Db8FSqhEf*$^TnwepgD zTD?A{shd=bAIe->FSK%bV$TUF&1LhTg6dd2C8tN-a7HouDPt5U$>Y!KQ*Mwp=x2Ap z)K7N)mzPpX{lsI@RXOp&y_{NvX1pc)|9E{H8g{ z5p~haXdUf?xY&2oS3}(F2f;7!47QgYg1FoBfY|vD5T*MQ>`QPG_)CKtXB(Fq*BSN3 ze;7X${=L=sH@Fk{Cfo`9%E$sOcf=-PKhFcRH)#M94{D?t+GjkS!zec{CgHj~+u$>~&bx|Ez_N=|ncb=%fVvh|az!kcy&duXn!dW3Ja!Z)w*%_n^G3*Q35w>sfl zz3}az@a+bUJD)2{l4FSeMPnp30Y?vD=p2qs@Yo;VQ1c$Ldn5V%#&aFW#t^)uWkYK4 z$s^>zMlt}aI=$q%RmSzKkq5^Ce6z$HfUAWEf>ZQ+&g6%-vGd3^ zvC44gdAU4?xS|jyk=>F1cxV3OUHOmC%YS@h{^Lt?AC1rCKC+kOKfZxQLEo?`Ny24s z?&BNs30|B3_<`I<la!G2Cw&G&u)KE}+C55UKe`SBt6ID#LIi`iGm zndZkn_7Ur2fBxe@{^Ot#A(xwJNAuanfSP?nUQvJoeb}Prg*QH%E2Qzc{KwDdKmKR_ zqd&4 z2W&H}4v@LD#G9Ytl;p$h z_3Q8a_uOEvZZBOw;7~1VxoJvX{l~)L3BJmg1YJA0*Wzfea^Y;GWb6|-$Sdt^MECsf z!qt?v0ByIT0T5J0;w-AJ^7<{iSb~vRR8Y?l!-(a)Mgc%E{#Z+Wb zp;Pa<^~UU2^#X*Bmw!H2Hd!nBWzA{@H1WzEpb838Km7>%7{QDD&6fZwZsu?!FLhx5 z73-b6E;ksRIgJJ4pmAs2&E_jEe^zsehRu%>O^EI7_A0+I5D4yB*WE_SaqQEh=SPDYE zsc>~1&`hdR)8Xpm;`j`}jN9@Zz^P97k;8irwGVW4-gv07yLt4+L-X)f9!>KWIG{{S zMPmC?@%e+*HMi`&QBjKLxNqW$>mWN`j6Tt# zsUTu?xI-J>ED}Z=tc8|HA31(-P@Hlal?ml$rkk)?nXpUz=@w3Y`CEd`)I2zIYuwu~b3Lj&0GD_0Rpk#QtCXXU&zW zxe~_jL)XvWc<6fb2d~F*8%Oj_3|O+}4W-;1mAa(WzB$^)+DIFK|I{rK#I zr(uw-E`w0U9}tC%P#1rk#vfbw<8=NwgFi6cHvZ{M{@BhRJNV;#{~b~2a`dlVP4IkytC=5v$w6Zb^r6^5{fcAS2aLZW?XFwl+30G7 z=ONc592f3#K>#nZ-8BKvEv^tywz$Ue88o_qejCz{!{;q79{g*I3mq(Ei*}3-Dibj} zs7xG#mo)m8hgb48*X^V1+d!?_K;?+t8>!#t_R11)PU~4t#Iv{2e*|dkzbEkD2gw)7 z!<5h}x(|B$%jgKblQzRsc1#;}q(0Y?nlKKLeH%3S zT%cBMpcbuP`3=VNoXTL_=Scl9Qdb9;@?J8&Uc)SdOf~&NWGc3n2APV6{u`)2as0KP zI#Pe;Nd37Z^$|xZo~&;u!>^1lkUJY!qBdWU=>*(`e}PnOpdNIj-rz_*;C|u0XWvz`Mc|^7IT4r={+gFOP9Ann=J5i zz-Gt$A@~tQJxa)#5AL-%rR>lFue}=f>wrF0J-%8UjKLOtBC-^jScHu`Y|m|b_tbPS zRULv9lZ%O1BstC5^NBd5g*PPP6W~Ugh3-X3lOAAdH zf-0PlGs8(mmh(r@0(irNo|d>d{S;idPwWG^C&CH-ArTIRBTFW!dOjGLz*7!K{qV4R zqK0w@C1oCX6j`aKUYugr|8O-`#*ubumQj9P%P7SEr{pn09%Jt&H|q=lVw#NmsM!&A zuiwl&Lce$_(y7%E+JoZ|TTaS(a4(WK>jJ7h7hEF*xZ%{@)vadXF2DCYd91qIWgS%f z#W?2`aZr&T$9mxE9)J$yynx$iGBzu`a0Hk;XN+warXU(2#EFCR$K-4h2DJ%3Kutzc z)yt98w4%#ZUjt(o_$$E`emoT6y?WEsFu385Z1|0*f}Lw9xCn=xscCo}{5Q9lg3&a( z3XE77u=FCqXj%RHNhogd{lB(!t8ss6_kVuvn^ANAf0)pR!K?p6V&C5M+g&@4n0@Q1 z_e6RBiHP0zs8+}4kRZ^AQq}0|C%?jk1j+M@?@iTB@x(j_vk05Nz%RS2!z<>GLMxsK zhoaECV7;)I0$wZ(XZBIBB&=Qp1Fi%ZP{na6k&Ms5nLZrwM`0l`iJfjf9G{2f2EPQb zx8brn!znK9ox3`+HFozrudybv#*S#!SW8xUrat9Y06>VquRx92Y6}7{@)ZaSdwdwQ z>aMaXFZ^@G_J4r+F1`N;O7o8h=-Q5;{|}{egwD|qlKZ3{zuw=&Tg=k|T<03j3W_bJ z7g~(nlul`TE9Wz>nBe-M72Y1$|J3z9-X5g|K$li~gzKjSz^ilMm&HU_eg#L_vGX@; zkDDzfN?*X?I>Z7T{PURkWE(RbM)>0Uf7bP@l=J_9X*wVoa$p?g&Ai3}N^~X3q$S(` zKS<~Y*~_HfU%9V$dV2Xda&-Uo&#K}*T^bMi3XCIC^4@+3xe-di^c^;h{U@XZeQ=B* zC&8Y-*}Vg{2tuD92LjkZazZZd8o@wv-??SM6)WUIi@#?CX8P)U z>UkcYo3D))7WuhCJcf&>>bW2fD-O#iIDFyTiCFbSBsmZ9#$lrZ8;aC&I2@}E0Wvq3 z&x0L3968|bDp`&yD_@vm`@gTIw&aLV-qNB%diA41;`*OmOTfBc?Ef^1J@$t672ae0 zVyhY`uE+kl|D!RHww;gF|BD=Pifx%-nOa0-%2T@-)LTg6AZ<6Bkw*UBgA$?7vdWR@ zy$7Wh%`xT?t-Dad99s8=gl%FU8C1S;PE0tYOgJni9L`w|6}uj&Q|*2GisL_V^LoG@!w>YRAPz`h~3L>RROO^H5oa7i3Ab9mt!5>AQ< zOU4WLxs}@f^SHV9M(uHvn?U3EtU8H-hE*bhX~BgDS9{_Bmc0$q6Y0Me1Nx_w+mW`utlEMH@qCqW3zVn4TNj- zMW$^)ze{}v;(7W6z{g;anC4v~b)0+^-z!o>$>tOk&t~bdXtnDo5qE+6t@%bElXJ2RBg_Yy?+Lag%P_#Dki}0Os zia5Ley8ecol~`X##CQ{z*WZvz<}2rDJ$nOEnRE^yP|f3x?}rn!xqWbS1}AFpKhrD+Zsfr{2WqLhMq=ufnUJ&^d{O3UI5Ra&!=ysAEaLa z?nlo$>hW;(X4}jQ;pc|YG~7eH6YCp*plWsCjG>Gm&Y z*uQMGf7y1naXGW$xy(r4XQl7U()ShV`$-sFcjn(`Tq5#~XI@~`vX%x84~S>zJrMpi zoJg&*tt!TuD#msdV~2`ywu-S+#W+XBI8VhmU&W|WF?OpM7pNE)su+7zjO$g58WqE> zVt7=HS{1{qV)#@Hzlu?>VjNU4Zov7U&JsEc-hclJ{|f!xwd)o>f5Iex&+SjpB=0zb z_H!E@n+Gmf>m7nsac4e&VXT*!kaEUt7d2i+KJAM@x7j zQsjOg*1DSbpLqYz!114Ul;izRNcuM?Z#xUdg{#Tc>yK3NL1bCnbWXus#$DlC0hBD5 z628bOG3?CXe%!652)K5hCm+UqJjT-ab`?yh<4lcnU8Uypk$>0*$O~Qtu zIy)%BB|aZE#pPN=wx~}WC`n3wKH16N>w{kp0ERaXgXEW1_f!Q zTAS9AR?bw8w5PNo@1T!C{NG)KJPz))XMF1UzmIk#I7-JYxyF#QX86*pC;qgq1UuBWwc%uw06yR$C)rS9lCoErlgl zo%L)kIS6|(-^Y<1>D#&Fwi-*X%vwr8JRuuvaHMxMp`{c#Q#nfMh(UbJV6-P0UYv;c z12WSo(@HuPwM6Is{&uZ!(UYZ!o-9Z7Ba1N%X?m)f0pI_-$hCxAYwTnf(7jw}=fCt~ z!ct9wrYWI#I6$1Ql6Yit-N${#i}dpnNsLjuh!4W znbbdb0_;OK-*WJ}f*%dR&7@w>RE25Tv}Z0XSyIn%ElWMqqb0SRsa&bs7w3YpQztWs zIb~n0NR?LWD6^cXu#D;XWlOcwT($BW9Dhby>QBj;3jb_4D#nA#?_sSe?$l}4oc}K* z?d)JyJHuf%QH1cbS zEoUlM>@Ha0fHQJTV-6@u=0EPRtv8NlS2!))u%R=O1lxmfqVSwwlK5-4TUCxbA*uH2 zN~!X?a;tnGG8ax1DZI+af3x<^`@cbV66pQgjV*K#`+j8}>7_KW6r19&3<=ADu?8*0 z%9+YlEUu;aLUQAlA-(*J9qHARR+-m$nUg}9N9(o9EN3dW%!7bK^han5>E$~t+3_$$ z65~KAvL7f{_Lf)*jLH)cxUGeOzwij4-wEuieyDlAzSzGb<0Sd&XcoYCpi~}xTmimXP#?36^xW{OP#&jV*lnqdv&hN##i$nzm@;^gZ#%w^B;eg z`{>%jzDAqzhh10kA2vVw@*f-XA8&=wOcPS6o)5GhiT0O3J0#KW<6)g7+KYg8odNrM1OEYWy$0eXt zm?p;(%NsvAQ}Ggr8^7G1+Ih7S2~Y4v;^Dlvm`^F?#ucD_@>@+Cmh`yhkpywBuL9CH zmn%I#+2z|jec_9^+O(bS-kx?@4H%m}G~nYyIhfk97Okq6GnFem{HS$^bc*HQM!!L- z=|zOzLKA@O{R;X|^yBn@Xbs!S_Ano9g;;+3Uz1Q(fYBxZ${6I zs|%>H`c;A2?>b7pOseVz1V4^O*Fi|z*dS1k23++(-B;(0^yS{$yRzcC0;pAWgFVsV zR)51_QzX(k(AVNR3yU@}Ii4@t!kmFVA9aHZHfn6j_#RJNH-FS7#>|@W9Zn6ThLR&W zY~h+C?0MY1Bd}&~WFs@zIS^;};af$olVSQr2kHp@k^^;=ei@*$tLly%U(I>%^toe3 z7yq$3(Kn-`9bX`rH-$#&hy!(ujyh0-bPTCd?;&?OP+Q4e4%9Ysw*$4EJPWC7c9)!) zdKQAtT2=4wKb{kN5RD{(=UfDo0l8 zF{zBUJ%`jmyBa6k=_5G*lV1?>3;J{R7-_*Vu-He2rMFsx=ysmDNsxc+h}Iw~XDW9P z-LPvmhfT!DgTD+DnnXyBRNdNM}(46Ds@UqHFdc4#R=YO@{XDq`cY-zipu+5dk+2t@2)kJ6jCCba&b&!1)K z5Bq#X?DO4P`ja!2t3PYi*c?fQ5)cP09)q<7ob*9y)6Ymhsm3V{gNE9YT0SUZn^kjL zcwI}4_^4LR$(hQnIXst@tF5q2M-eIr0GdXfY4(-bl0MT?0is$+(sY)hg3fYPz`MF- z-cW)j^zFcGn&AE41rflFE#w-uTW$XXhxxm*8q45e{;JsB`?WMCXDU}?0}zF-AqJqn zXpdg_a9G~}DB!6LplR^Q^g{YPOLDvtl!O>8Me@ONBp;3>!jsYXvX*(ESKs<4Q17p) z_nAgdv*~j!q0#175_+H%p#h_)bUT>F9=%In{VI05-Tt~7(+qSeeU2qAw(fqhb@yqB zD`zT4Tr?$t@gN31*?Pv#wfd6V4u8!4e=RY#vWw_t+z?Rpm!3~wVCe|0BNjv*jcVyg z&Qy+$8WX`-Xu1phTDpSsdYz#NgB!GhTS@P>WJXiiqoT|sS~AO-%8|JVuEW9b4;-ID z01RKbfxy#|DL#B39sosWEeBgB zA}MfPSwD6sqW13D@4ov2Uc$9jdqHMmxmU?0?3rFof)`&jU%Xt_JKcA7nqs#-3vY2& z#Fx$~U)mo|ECHfNbze9b&E9|J-!H!f#NRA=F=_~#ssSYb2*(~Vzs*BD1_&(@%QC?W z3oeHH0p%o1P+s$RYhit#S2dS5dHC31C!%EotZ)e86(=E-^Tb4WVm}0PUIcH(NkE89 z!f!#yhk5hNJq!pakT{7ZMEf)&7ZyfJG1 zrDN#}t??3FC8U{sOl!Q9GnF%5^6~K$Pl%UT661sz6c+;_Es5n!S;FS2BI zYSck3nWaV@EJtQo^YpKR^<&|akRoc?AApTsfGgzN z;Qg77{+9bJ+CY$3s9CR6vtFfUy=F4->X^wpk}>A&3dbv^^a=S+tjRof;sV7&JFvYffaRTamy#b zZ+7Yq?*NvwF*)J3Q5QSsdY8I{FHy5zs%E{+-2Z!saSpqPc<3hke<&6HrBms>))=)}<&kJ$J+^UCjYceNAnx zx$K!uS6gD^Do>Iihhrm5Z?ZL%oT(hK8++Ok@x}SVHLP#?oIl+*`+p-u9H$<3xv`Us zVkcKTpbuPaDd#qxW-f6QXipJnQWMw1!jLnSr!edniz$dLQ&?+73Q~C0C#I(fKx01sHWdQ$3UnPhORc<#Jm~)(|WO3z8<%`=7CbSwyfklh! z(8BHF#!3-4R-U+R0C)hQr3yD?ePKQRTEqxCxs9GrE+a1_@ZTHB`^bNiACkvu10AO8 z^rQ5v^byvEBc*yMw+J-XH>G&$?kb82(@!fYPe*^_e3u+!lU3;z0C8Ty7pE==1f zs3F)uK7j8Psg2}=4%8;{A@&;5*qCg#g|eURF^tylN4q_FF!-hgV>hpOIrwM?zMyw- zIS-azGh=)dZ11a@26~11^ds^&I-DlhS9FMAiJ z6%fRyTG{zKj6U%Hzm3p-)=zGOFI{|EFYzzEoc3hr3ix;}|Co-a_gmA)B!8_(R6`C4 zaMR_KoT8%%UdI)LoTzwl zmG>fmeeYE@;_UhV9%7uvwvjz_7q2hdA9P=@RhQU(7sa-$hXW&LDqgs}XH0V|U)*c1 z{`5^E0&sHrF6e8XpnjOQCt>Gc4A+_D09U;qdP&ai+DR(Q-RN$%xzttB@) z!FG#Ptp|N0XDUZ-=qM1!0Kz)=g_DcXRDW1wH>WSTyUy$Od3eVF6u8{HW%~b0Qs`k@ z$(br5Iv9Fj+=s&02VUFVXZD&oe}!sdZdyQ=UO4NhtLM9gZ2t$?&j7uKU16LTG-wk(sOCQ#YLPXslDr=(8!s}(O7dUHW!_nA^kRD8x@w*0ZU-)9a7ny zA!wHEI_n*sk~5VhaN|HC0)Y+_`b`8cxkO6@RTBx+*Vfniu!(Hm5_-qm<;&H$#z|HYk_PHMzB)AYG|&HK-T1a@j7dN^m2z=3iE z?ukcNr!>$pe7uS44ggjYNl&K_S>lfHMsYwG)yDJya^y_qh}#X;I|Ua(rO7&%K5WT~ zZCNU|9{!7*sT^6s007;hDk-ne3xUPW{tvJKWG?}`_8GXv|10CItEEYe^s+K`K^Cb=X#%-^?o($18UX>)vOPxSszxjKB8uQRL%OBn)UB$ z*2mSXPpDa+RI@&%W_?=C`iz?OSvBi(YSsg4*5}o%|4_63Q_Xr%&H93x^$s=bood!5 zrUCC(=X#Gh|6c>hEA$d}uCY}$>4P=cjn=@3)?m^Yrw0xtXDV}GY}ExvE@CdIt?|@C z$Tr*-Cet@rGUE-nRD3;HC^=J^GB?B~x*;Z55U#%pPYL=qaBgqF?e&`Hmx=Vvme^6= z9^|V9#{(>RJtt=>Q|#`~vFB`1PxJ*OpYT2lXRoDpUAu zh4lne)0%RNky$*4_vaV1dYc>jAb%p=Y_$ixt&pVC0|1gUm8lX4%Y3)5A6(S!r+Xj*$QhgBe?fQeuyo}#F6iNWNQ23da&*<$-x9@W ze1%(%zHx-#Q|k}7&C{TT^bt#9*dzhqojAbh`O3?g%96OJ3(VgE4_?a#4o~p(`b6F) z4Ay!_EunF(CzW5%J6_IIj?m_%Po*Um0>Pxx=sd+CK=IZ`Dn6f93eRM6rgCI%?CFOP z%{*-7sg!#g`4G8;`~!jizD#}!h@UmI6|le-=-u??^lg9%f;eDy6fol)<0|6@V-mdk zUYNxIwcGuepTYWvoG?uMuf7B~J!l=QwZVDy&%4m9qCI5~7vH?*x(3sBHx1>%xcY}& z=ODG6TYWT(h)unxQ5RS{*zAYudKXt*m!Y-8m|z((!J3d7Z0~By*@D#8um|YB!y81S znuzN*Oe^+wH~EzV)k7Y2pw^OKV{t`vr@>1ch(nh;5QkBO7SW=nnSPl6ltgWz9|54R zeJza#Y-m`MA!CZee>odmwFrhFleO$@eD8puumB4pb+9>*2SH2ujhCC$aJ^;oRM$5X zVrXyF612Zzx|LmlX+@Ko270ptwUIVBP@8C@1GSkpv4=pm&_a72|LbV2F^Fj=LcQ)B zg5N@rrHmfX&eTwEu1)nP!r-3T18^oR-r!F7EYadDEyD zYS@JLupnBGB+`rN9!nDm-U1|{oc&njUB8^E5}M$Syn1TTw^s56{PlHy)30MK-D?Ss zcyN*ode|Rwrb-EprvIlOR>158Wa1L9$<)^sT{?E z?oCBtIo}ZWG_^|62$k{cnNYe`)xi zFlfNB+L)zAJXZ6Gon6m!TFz9K8WW-ENEf(HG-eSV5Ov-0oFJXPJw22yy=k&5)u$fT zjhv~R(%Z+Wo?s#s2}S0EF^z*keJe1#um2tCVM~4&p8}*&R}be%&QvM+o8ohGi!mO% zTqgpEPhSJI{@R+FdbERivQ1VS!QV@h+wA`ap$0pX{SNH^UskCV0GI_m-5L?ll%iD} z5hk=wROC$Mw5gt!z5xdixr{4)i`Dpda_OZ}NDtRT&QxweJA)eGzWj*xlsgU|06?_l zd@uorMyrS2HAO-}J!vXzW7{|NURkV&W_?A?`l_1sH8tz&YSuT@tZ%AW-%_)_t!8~k&HApI^*uG~AvNp2 z)vWKUSwB#-eyC>sNX>d!&HAyL^%FJgr)t*E)U2PYS&yh$zfiM&sb>9;n)NF+>rpl9 z*J{>paQ-Jh;rQQx|IZ%h!(a)2fGK_48cGpUS{h0xOBqThD?OAxIZD}_2rg^Hy7cSY z@^V*~`m6=OY3bXnib7|vsJJTAgSU_~l~Yj-);?_@9GZ^Bqw%R#@KcEf$Kzn+q_f~J zLJ_r|0N{P%u^-_72Q5XM#ap~2lODW@oTM ze{TuB-=2lX)0)gGHiIVtZn1^w0Y=D~Dxz5HB!C-K!&nfq>K{j}A+eZxa6WRTiWD>7 z5n$BR0=vVLY|RURd>2TcOpjYCV7v~c)~tu*BxkCy3iOQMOH{z0+W_`|MqvyvnBfA{ z-*$UvF8(u|qr|P=kB9y#v0E)^Ed=CD6=_r4PmV_zbs9+_DA0^mY`g?ICKh`^tJrd; ziWJ)#v@~(P{Q7uCexJX#9tMGkFbGIO%$Fkhd_^V4S#UC_VFOpJ@Obs{f41uL_J0>b z3~;s$y#FuE^?0}sp0|1g-Um;JJz`m_UC5a#(k{BfL5{2K)Q^R5nEoQ}gKO)(?ttkG z{^T6NN|hW30qh6Kbj+%7G=-1`;YF}OaChL0QER;oi7Kt?0u=NT~C>^rhxkLcMq z=Hdv1RUKlS&jwcq6twY(0LsS_S$z9XU$VbXOqP^&ruD>fs-fvD>|UWj(l9&hdd zjYCq;oI4MeH0Wk;;|1^V4%ZyzAwh#Rp|QTcS>qp&Dp=%A_~^enQ2q4X4%7gB4}CfO zT0>(?&T6f1WX|wm+Q!aAj@sWmV)W8qLfZa`<-FBeYSdW6w1Pd|cn$SBP~Fsgub6g? z*0K(G?}YGcA)^o9AjL^w-HLkKP?=!=cQ4u3vD8_cmtkPccsfw4jsy;j<;~Ux=L`@3 zts~8LOTig0yVbZ7s&Z(sXV5av?DdbZ7XY>DNN{L2_ujto1tWlO5ZY6)n|#)R+CvbF zO)}5vB@Z~H?IWLep!SphFz$nb|3%rYXy)udcUliM@z7-lM*{ zW0q~?%<^o=crJhMQK998r|7pFsMGY@_=cA8SWe>pW4*>OuZg2Vq|edrWB7vJ#i@KV z?Mxa!MXFHkp*Wp(pw80>p8rusD5H z)MU2O&A9dT;$Wf&b|h!2NToN-&quSi4n>}W>g%DtuC~UD4lca^yU2NjoCgSCJLqoS z`Qd16#ZnF)jo}oYv#1X00lvtYDy6xe?q+5o7CyOMiSawG0+Lr>Nz_Jb2QQAFl%?QS|U`$k?m|-OJFFo9hdxOr=O!z=KggLXqKMA8rsZQm{|3+vflqP% zuc-acX81pksf`eRt7iSLn)N$1>-TEbAJnYJl`N{ZYob)0i>XYSx)*)^;^(hnjVknsv6CwNuSHN6p%$W}T~Mou_7bYblCh%0XVArNTv+*z>v_D(nJO|6;&!+{l?Vq5M?*s_ zU%eY~zrVIFYXp8GV}*s!;90OFhMwcQoT(zRHvtGlGTc4MfvYuu{z?;nIkVN8192Ig z5UXAf-a*b(8S#VjU=0iLHVcCvmnQmR<_t@8yzP>t=+HU@k~39AbZr2TfIa|7zR8>Z zzmE}iws9JH3~v7`X8&LAHX?m(#*E2s`KY#wtJH@^_q2H)ilwOSjgTl}==~TUx<$4~x;T z&Kbb;A~4NxDb$((o>33dMct6u;hV_VKAcM4;LuS24FF>(~=#h z18G>%bB32QRYdllD7Z8R7Ym;U=nL+tbJzK6OgFfN?4S!UIEZ&7$>2QbE(=5F=#-qP zB7(OC3x~ifqPFcquVi*vwT&%ZD!(2Ggq*1&LN|w_5%5=skntKPK>FwZZm*}NE{FeR zkjDtj{l8+5(ekW+GbD3fR*~!IP!$sz#<-prxtysYitOMK+;gr~U{ngjn+bsdEgdUe z`dK|F5jj&u$~qiR%=QFBvjFl{xSN+!=})G5(i!7R(t(jV-)gI9|1VXgp5MQmsUr0+ zo_y-wa|Mdi&)N7PBpcH3Gu+{e7G+j4fD^(f^&ttX`TLkoV%v3-d}IR4`x{AVylsE z=PIlfr61Q?gUOl7kv?}lAvoo8?9RscO2N^p2sPNQB|&b}INRE-NY&}=pe-lTkvbjA zTw=901h|x6qNQ&+lg=-zHid1`IG{Zy3LkqEp;~(j(sjW>!mho>3fuJB5Hz6zgzgU`3)^Bwqn zCqCbg&kx}9gZTUqK0l7nPvG;D`1~wBKZnl;@cBi2ehHsn#^=}Z`CWYe0G~g^=a2CD zFg|~b&!6D)r}+FCK7WqSNAUS;{$wtEo`KID_&f`ryYP7#KCi&%Ri{0t`_5~4v)r@4 z(5yh-VpK zRW%Ww1PArQ1N?3+IUSr2cZR3Jv5Dm2Yk;uz#GF{j+Jk#@H(3Y!^U3jTTel`ttI_b` zLvmQqrwog4@!rUPZR^%0OX9~YD!*^l_{&%30++gvIa12YY(?vM7$)_JB{BTqpmGWo8(1N-m$)wJePZ5fBgYSda!%vP8|Dzh#|7Bx;AOgrH z_v977y1Z0(#6pHOsALdDN_0HOs4J`P3}GniWv9>eQ@yHS3_7b%UC9h_C+*x{uI( z>~do#-9mAFNz${KtF5{8Rz4Co2>t})F|Cb|oJnH{-fSdniA@xqO7-1eJzkHu4lhkl z!dl~66xW(zrbKZ~_uuqF<|<1mcsMS}r-y7HXDU-Eoe|xOaSZt8xKPajh*<$*-GCSZ zF+=zS@qeu)L^HV6;&A|Ob7=VSQW}1|%sS={ugStqU;iq&7SO+HO#46DO0FPpA>ian3r)Zd{_XUWuw#D|?1J9HE@xdV#8%n!+3VT6+2`2z*l&%U5C`Cp(Fc3< zJ0No42aGQo58M86A3^m0Tg9%#y^r|+I>kkN8{OhSZKtP0v8uY7?ZE+B`&@3GwyS5M z1>MQGTV8MdxNDok88_t$Sc851#L@F+PG+kGCNYQ9nD(rdR;p;B!ZEPG5aB9-|Y}`s}|pGsRkw zDms&OQ*?go5~)2Do!=yCFGc6KuC^nyIX(6@EF1sE)zBrnn6)(HkBQL&ylkiOW+?7J z!0w|qZj4}BVdM;TQ*=`65~)43+aYZ)?Lq27ZNs?F-`X}Z?5XQrSY!8M!Gwl0>R)pp z22MB->u%>SJ=S+1a?Iy%KF|Pvz40(RiPXUsdp*pu2vUW@PBhSGI#3&F8mXZ*JI?+h zGt&x%4bGDvVp@^9Kz`&vO%Uug$HcTr?xIVojxEhk<=sK!-9{YWup+dO0ank|a7=9F zUFiNQ5s#g3-v5K!&u(J}+ey#mLmvFW?bvnJREM`?(uA1M8V2P|We$TyN6D(wzXP|= zc&nKMmh5O5)GM@yaXm+PIa8Ukx2&YX2?z}UcSGP42ti>ANB=8Adat|2?Z*3HFpJv1 zneck<@RIPUGKELu|38NC@ytF;c-;R>awN4{y_~5`;Z58ZumUK&04S!pyX*L^X*T}X z1%zBcFMu{Iwo47Dz1Pu>{BaKS>5XKXy4&d4l&X@_XjFNL1qvP~QZ=2luP_{(l8w zj{*k3Ij|Mj_?Mo^_${T}!r5`LBY3n%MLAPhN@?WhrG>k_zC)xx;C15xD&Oz=ENSuN zqeGO|ttG9TsXS>1wCum|Wz^b!wgGNbnr!^(Rz1uL*&GmmRSe6)Pu}kRHTp0!a(({>s z)nqU{q|~)*wVI5asT}2X_jPe_KQIW^Sr!*DD)<8RwRlVdM}G$`nepgvR2&ubJmcj| z<;dItVS*#Us1_PPeG%42{y-q$ar2erNw9{gw}i&oP%6KkTfCg99HCpmGV&BlTqj5@ zwNgFg3prCc5+4Dpj#DYIdH%nf(A{j8ah9>GK;O@@;dtPXr6)WdkSbZvZC=h)j-ImJ zp(hz$oQU^B0ENP%Y5gYd(%=ztgC!s?{-v_&;eW`P$`Oz|{w;z3TWbV1!l$xD{C{7! z(I{SXdK+L{u=|BflO-;$ z>{~=}o3+H1GnFSUtX~o-J>dI#eW&l*`dV+D58eErL6}|@mc4_$x?t_oY-t47K6d6v zZ>U8}BXXwlG=et(bK!{yEbJ<-7=NI~SI@WBe8f}qPLbOI#|6_UgElX zpBGO&&Go;VP&e5BoC6j>r<+TB7zH{kP2eanD0YiH7DPIlX}40|#xqMP^`H~vOy%jU znWLsHrsfwN!Ttrz=pqIU1dIl8+cp!oU8{*m;Uv0 zRGxC0rsIild@2!~pI&u{?zu5oWWD}1X*!kZwB$upxEgU3)Puv2GnFUrKzJq95R1jl zAd!U^MfxqmwkVp-bXjua=r4(&2Z$kODo<{$fT%^(tv&ql|FLH6cDwxm0GPl3y99iI zAqdbV^lW?22X}~Gs}-O*L{e=1dSDQ8rt(@rQ!pB}j=~DJ{UWY_eb$k8pCvLb0HxCF zK|#ow$`iRO3?85wPyqG%()xWqf4$$d?>&*}u_V5O*S<75>p`8!naY!xSF(LrKa~}J zd!{=pz8U#ll0*;XM$S~G__i6Xf06rmYuS1fG4}cW9=DHQ3T5qo;M(tMKme?w7u);4 z?K9l@^;@k3@BE}Du7{Q(XDUZc#eiht=1%Wm!0WE9^Z864yXDM~B{ME|LgMf@thLgY zGnFGVwuA{l!_~j-EJpO&S~u(qaUVRF8MH*l*zJ-Odf*sxrgB6d2qz&Dc2eV3N8cL2 zUe}A60ZU-Cc#_Jlhg>6PDo0>j`^Ftc;mNKf!MD*S@)U9(ArF%;P>L~tMrj;k0KJ^t zN8d)?&m#emH(@LwA3Mr|Y?(cmy_S6zJpisUZZLX`N#pj7KKeX5kZt^ck5#>Xe|^}B z?b0_g>WaW`S9J>^eL4fKo4HAI_lnnMiGpcejxRP_X>-FewYI}LQA=n)3Ei5dDje6L8IrT05f zqxAJi6=WL^(Muet6ZBFCYM5Te&DMKFv-K1T(d$`jvBK=kEJr7edyJjDK`q&>;oU}p zzqVKKftaYFZ+D=&={p>#9{NsfsGaWlymC5|>_@y{y@F@NNX;P!;@S=P*3RWvzIr@! z2Krm{cG;_E@)-R!rtS8OX_|OTEd(@j&{tDd+h&o{Zd5fhTwEvU@ z6%s@I?u6rsn4GB`?Q}(A6J5c19V-ZbO%b+Fxb&ILn9U9x0P*NUD!6mAldr$zOyx++ zcSG^QC)veFnr{i)$lsB@&+P>y_`yje#dP?Tv73-86gSvLKITAeC;#q1?I0g_pdKMOkoS9pUu$3o=W{@Tg0 zoXgih$FyrZP^jR;||mr8gih{(g~!_3vQGlHf=;eGI1@|VO)-Bg<2cyA0!2^JPsrIy{NI6qE!(vNMQ>&(Nl)pwFbf&h(Q|qfScfrX_*qZn- zGT^Exb5u)aIa8T3k97BS&jTWCDqhe7&|jl(9N_i_++MGF2AarBSYo3~fK+RN4nu?c;#_1n0uvbRAO-%G&oWro~UHU`ip@qg$o zYWt+8sac!kdb&E-t!mbBHS0DtE2w6Tt63p6YeLNmt67t3)|8qxt!72ktQj?HR?Uj4 zS#xSuOwEd`S@UYvf|`|3vyy67O3hkSvzFAXWi@L>&01Bn*3_&MYS!&||8Ls={?wrK zQHTIi_%CyIW-2>Yu3tscCoCp0ROZMDuEO!eT#yIL5d)}zgZrda zTJ-Lh=4U++A30N*rL}uA4}xP)p~K#yW_Gu`2C#h4+f%GKsAx$JJy<6>Q<=iU#LDlt zIEHXxxe?we=K+T{{GP-6eO_Z+D0wTicN@Oy!!K}p$;GieIyWZ{f zd2;ywE^;FQ@4r3dMp{$2^V?H^z2vObnee)NLG1c^Fd=fLa@tc*7aX01HP90LxW?Dh zm)%rkHWRfZ#^WKW@Olsta;9=5?rzc>G4$<#JoSP4I-lwHIGZt{x2@AKX_z*F(L$-# z$Y$1Xa;9>`?dj?VjN!rqg}%7?!GqiXnnEv-`R#g z)BbM{p?lbQ5dZfKd+)YBgB9EY?Ehm{yT|>%)P(gwapX+pXr{T9Tluy|;{|~L{AlW= z*VWy2Z?IGb{C?*wvC%s~l0Xj*N6u7^*o{3|dmuf@^}B)1`tSc{UB|78MoZ72IFl`D z^?W%~c@^Ccao{-AlU_~hTeaHTxY_@!2t@zdYA~ZuYyEEx48s3Iq>g1vgBYo!P1K+s zEP$Lz&|rEovt<1QT4_kB^^i#9Ou|o?(->GjE?Pf?mXA^%JrEE%lfw_0cV*Ql043fD zo<4se*xYEfhQbqpz9C4hx7G_tka+L)$I`8~$PM7=yqb(8Io6BbRiD1#zJR~J&TnG= zoceZaUmI!EwOw%Zd^?5GoAEy%CG2#A62AL?nb4`9z%PKw`wZ-Z)*ZE598Zorj>vK% z=RTLdJ#%}uf16gH!{WR3@C)Qj&c8Jgj%w67SoA@hw2sn&{Eo_&Z@uMAI|1T0l0rBt)AiH4MA$;dI$(|rgGF_?-@eS zse%(>5gMuW*VfczgMg?05@% zUtC!oVe%harwt68s5vtFrYy-Lmc8}t6}4}|=I8Q=r>d(y!t{SyAtC)T%S=sL9)4}4%D z(xBK_%1GK+(UBBxjH1EuIM~ndm`YE&0>hBKRT+-1w%+g2Ujyf+fb{A*6Xw5^IR7r&1wOROD_GU9_EXjsiFl% z^qa|GsG#?ozgGVg*zZd}W8Gy*j$xnH#46GQ`jInLUh<$80$y>lXP3YKcPl@u0NeTh zjuOxRVG@PTewNVL$HkVUhYKQSsz^)f3qz#ZWEjwaamFt+f!5+4DxF;4Zpnkjpb1eP zJ=_mDQ$^(Av7jLSLBYMSDTsNg&@vH8aR_lURIg|~<37AUHyNZC)^}K9<87x@BYLnO za;A!iJrsnnC#Oc}|IgZPaaF#JJx0z0KU~^GvwpwyZQSRV{!;S!<$gar?$=|^zP{Yw zH}}tq*PV^r4gLt+C82H2-qu40w8!S*I^;K~BQ@wq9e1RL9I5Du(mL+9c_zmIqCjYe!HNS&XkI7-SoP%%O z&HZ8I7*|zJM2L7rPnT0d7##A8ZprQ2*bmg$1}aC&8reX-9;l5QsNAY<-v+81s8t)N z9^;J;)CR|2YjpgzCdXfEcKo##bN~NSLVikr&3;D*AXPbkuogJiT2kT(KtP;0^>9Mu zOcj|y8s_JtnbX$Kwu*;P=h9rIhxQ_8s#x(NAzY-0XB2uX7OhdgzOJ^$i*|ibPdhC| zU_D7P>7o9}nJTY{x%qeu)^3F-e*JR~n-a*nc?$|rK+y0_YIl10R&u6FD?xwzUy2U4 zk#+F@nS%>o@>23v@=@|tKmy-FFQzrLm0EE?O)ucfSs)h$^nO?k00s!e0Ob)tdHm1! zfd?>;`Dyzn4kYKFcRnA<2Sogg6zk`~GY1yzb9i6M^)yT?_-qCb5S%{;L~4NiuS428 z@;e7=J^4L)Oo|v2qtyOsl-*9Zqwk)2G8DBrw*uQm1bpP!FG@Ys;v6IZW5T<5GP*-q7v; zQfJBcpf^@YX`|#JKL5K2kN=7H|5lexFR!0(?F13~s$HDNhqTUw z{r_u1e$5E`jnP**`#9+2wZ4YJ8lG|-x({VaAOrI z9C$7!z|AYM7)k-spZ?K*Y4!gnT}$S6sNmwi2+4Ctof+5q#a08@$veK(066L<2UOqO zu}RK!3L1d+^1lczNd4bM0QU1@dM-N~5Ww*-omjtY2Q-aKHtWE8xKEObLKRU_a{@g4 z3gcg3hy%SL(NkMjlbZzG>3-=~aTO6zDpjnQY*`|11X=jpfUBaE_h z*yYSk{+is)8rTRxNG`HxlD}p5vX`*evwvdmXP;)@h1g&e&I9%s2LKnW(|DRO12Mwx zF@yBgTIk-!UFBy+pSeyV)Rzv^HLl{dl%_b9$|s^*S}{el_d$YS!PXS#MCY-l%51NzHmQps-P^aU$NqZKCSv52Wuul)nE+ z`u;QVJ9~!o{a)$&%c_j)q3Twn;r?(c2TRP(ILEk>4TeWT;b=I)u^8yX`8s@l{-f0Z zpe_96cH?4b7GaL^%i{Jg3-&Kb`lNbHr-)y#6u(|2 ze!W`!dX2gNzla#8lZ)7ycKm<148Fn|6!9`xMu)Z9-KMVJa;Az5ifzI9!ZU%sJG7_H zU1!24@VWm=OK6KyW;H?q_`Uj;U~aFcrVgPXK=!9>Cc7T) zmn8daMPzRdNBM$JCk(pZt6%Xp*%#OMTC$__L#?>xi)b}=Ia5Vs?}YVc9190XYA^!4klLgTq#gIH(_=AlrbW$7qT^kG_$9h<=Se%677ASu?wp zt*{rezh@s~-(tTt&VyZkyD<)tKmOYIN8^(aMEnW)yU+;aw!gwQWL(J!DLE5LPFTsA zRC1=2oM|N|qU6jdIkQSmRLPlBa$-tOT*;YNau$@Fgp!j~a#BjpqLQ>IYEN0B_LL=QzWz7JiwXNJeHHxQpUPjxSod0!6WXWAQC&30 zWI0=<$7FEL?+h=6qa86VkS2_|hHJm<>a5-io9}v+;nvB_$a-y74R?BZ-AHrHnx-1$ zY?V<1pJQ<`+@DIsXT#0G&3xFG`s%)3L#&iKV`q9+^X664=hFx zT0Ih-#X7{5vxN{-Xyq(tt4veOZgxenBB`aB0}55*8UvXMW<5IN(bL(URIhn(>_yWMLf#4r}! zxS)seqZES;O4Lq@R@4%;i*Cn~C8kF6Cd`xb#^-r+7#GdBBiCZmwMBcZq2mTdX9|gp zY>+<4(@NAK`US4uP~B8VF5*%@#9X|c|5!+DknL#f9O`MbwY~a21C6LdLmhQiY3usB z8rU20$A;Zq7IkJh-fpC!e^iCW?cu2U6ULR8R;abmARTa^4$(me>M$L0ppMXCq=xME zGdW2AV6OjH3HuCvEm^hq*i-yDbLIL?)+l_8|JJBbMH3dhUrRL1XUEofj3b|iqw?{g zoXs{0%USW@HN+#-9p1ljT_n!Wlkj`|-1+d{{$MQGA5KIjaU$aM{>WPRusO-**B!+k zQ~6X!vRm;d_>DTQT~B7NUcb>&RG)*QEIcw<+5(C?@I)!9_@hmq`bLk}hD}x1uOGHl zg>f9?Vwk6q#&h|%B6$+YfQ%%tD6Tv8$RfNLxsbUU6wNIXPWWX0EcpYYH?S4`_g}K{e|H*Jf&WjhEdsF3`$vlZdpdSF9h{%YglL;9O+FgGV}u(KC&{0t}IQFNN2Bq z&_F-XVR0BA#=&y9x4kJpG%wW|pX7t(us8@Gpm#V>>*(zc)OvansgaO>1o=A$YLfgt^MN9h_9et@tI>jK zlk>Bd9_spz9AU5J)Dh3BePJ=e9x@&z`$k&3LL<3ocJD{tZ$Em5mS6DhKKa z-R(dfr57M|)n08=!|W2?Oh?4FJMVTNE_ir;X`(tn-{(L*PT!BzdXKF!99=d(gw)1` zp8QJJ<1^5?dbDG*VLVq63qJN-UJ;`Mtt)x=u695B4!&W?F2l?s8`x&N75xA2BlJG{ z7KrxwJ^DDi2%>%VuvvCDdkuRp`x4mo?J%w}nv7e)p6{=WxAN0|u=x8g_REcQ_+{0f z^5F8%N9f_GaD=Wj_akbALZU_}Bx;00qDCkrYJ@_fMkpj|ghHZ5C?slxLZU_}Bx;00 zqDCkrYJ@_fMkpj|ghHaWNF{27LZU_}Bx;00Fu*0m*)&@Gb?c4RI%|wmy}~*xY$3+AL^J;N1}h~x0ZA!2A|M>p z-E5`7gE}dVg}+*8)h1`tUA1}q0RC(Do>1)snj^tz+(h2VZv*grIlpAH4sG-fYgBuH zlRA3i?kPp;#EzvXXDw}aq@xq`VKPCyIwOMtRWhiit+8> zCdc~PZFBQ|rPP|5bX!wCeoXe(p`6y#0LW)ckyK&ZFCUtDsMV!B3^n`zBq5XZmF&~T zF|YtS1%H{^^=7M!qb+fZIAB;vb8?%M_poxdC#F&oq3K8$7-hoPH343Fh1)XPWT`7h zeMNQMdW)qt9A{@mwOP=Fh14czdxF%KiU5Y;sZ>|ZdaI=_oX8}(Ey&3t)z$WdsH?H7 zs|ar|4DYzLgeGS8|0rRfCsBI;Dd_sPS7yBH?N-OA7@c|QdWWUAA+AqJ0Y^&H+mR$>$eOIc21X1k`c zq_Ly@iBc9H1T@CX0_svuF=qcCgf>P0lD(haX6vr}W9IOBr=?Adj#4AeKo$_D*kvqo3y7)?fMutzoPK+g6=)j`V&_$ER$DsW^26iPUZE$n*iLqm*GI{UTjM=jlr@G6ir z-l?fOIolJZJ1w`}(zR7m``mboBx$SbiPDzU!oan~9uvCo60_9iX7+#Bn$Oc$u?J{` zw>Yi8%)a#ztDWA$bJU3=g9V%-jtt?Hf-KWF`EStC}rDl?I&k@qAIK}lAJAA zI~ZuLl+{`0!1{ott=qVgr5o!(&A~v<_C#q535R>n}iCtFf{uqVY)H<|12T1^yTaUZq?758TkISt{>-m+`s>d3}Pd$ ziv4ubTKx7cM#J1$t6$_#z@;UC*aX4lI+@K1BgT+WHPVP-0c#bi8adk&RW(Csm38W> zCgWZow)6!DG#f@(3y`Zwec2(ovz`nF-3kk zwH`D_h0E8k(t7}4ZEiJd zYj*}6+ho1SDwY+=oo}ttJ8NAL4jRek_ZJ@9Zyt8QpxJYBt(j>o(ry4&xaF}Fr*e% z6pqGDisH^SyJ?uvFwM}vq3@;t1s;FrvKm18ong;n_p=YOZ?OM0E--w6^BXswW4y`u znDJe*OlCAwa^>S6hib{1xPh2s|llER!@9WQdXZt8~>zzwREQ}938y7lBqYp$Q4>jbFJ zuJ{C?RfQ)G?{)hu^+`pKDl5O@qbq-i8KmtJ_<1EK6oJYfRJ_?j!Ys4XBw_vZZSOz$&+xwwt+%a*&HB6at{AUBNtxgyVPEY# z)SuDA;0OZ}*34cHZ&p zVeiS-;JwYEcfT-P)5O}Fzw^2`{r&T1zVyO((nr2@;$6G;OiY~pZ|{kJ^VeU0`lnjI z`P{2c{P4a1bk*xV@tJ4tium8z_qU(@!Eayx-uJ!XiaS0!)NnBJ;f1S@K77$Rho)-3 z-84LUVTXU{X)pf#hYoLF`_2Dj?@It9E6Rl5kpTiToWsaHO_<@F%%qd<&cS3L_kAZb zNjNf{?$=2teWj0FL(uMinHhx@RCE!AML|VS)J0uI1sPm-Jy!i$T+c-X-9-=<#RCuU z|EgZU*RQJUrC+CeG6PJDJ?ZaN)vNET>#Ohl$m17oeXRJIxkK^StoQ7V-g^I2`=9;h zV~2jU?_HBWYCU7qSHJnW`!`&C$v^zg>)5xr zbZ*bf|MtH(yz|R9t^dx6Kl$oU`S;xS)Kw4M*8cntBTqQ)yw|(q{Ga~eiJv%qU-{Q3 z{w{vMd+@}@-7{~y*Y&OWD-ZsBa_^y$7e82g>)gl{|G9s9$7z>7eQ1~b$J000#ZD{w z$fjp}zkcb0C$GQj^Z$JPC*Jp-@zL%}Tz`B1l&?SV>l?Sf`tpx=eC*qa`)-5Wimv1- zCmD>0G^e*rm7>Q+WNk(St$&_>kOS-AN8tW{E(!&l`ozbR0kq3+<#r^nD7D8%j8RpK z6ac>%7aU)bZ;v#N|J9gex^PWNMv;;l)WRK?CMVHrF67RA2r~opR7eK~O+|;K3)GaP z9-s|hUFsGj%8EKCCL}jL%E6n{8}QQKCIjBI4K zrUZuqFlb3>z~C4uE|vn|1gvBw#@$Uh(iej-{Xop`NAbTvh#_|;J<7p*(SccX@KUE= zFzdnF>kNZD*&A{OqIBJw65T_y$8QUH{D#o++Zzn}3Uu~jPi6z6EiEZxoqbdvW>6nG zonT~GnDc+-K4fL(>}3@d_z+(WD_3d7RdZ(L3_ZZ9^Z#+>{Qo1z{ZYI`UHET=e}z8( zeOpeM4Y|F}iLlcjnRJGv$d18|kvillbX&--;c8$MS&iKbh(&xRs9+)~u(^+mSN z`+ezC>4odAi9B7k>BgUp{i1y6z%Lu`Q8Vfx@G$anQ?LGf>a~rRyLK#I`$&c*Z``6hE9{A=@?_3Cf{FP6w`^ZI~n_DdT ze%skMTyX6H*N$iJeCOjW(sO@)kN2lT#k0fzdDqha{crI3k6rM$nVm1(U-QHh6=M%S zclqD{Z_D2YwtxT1fB5K&hwhoPJ@(8)AxGqYpLwkOZ=dPCVBpYoov;4e zFZ)I1*024f;d71u+w#Fm&WCD#_`cKMbJlE4~eEx)? zJ=e9bzx2@MTQ9!y)At|zkCWed$B(~z>CONBw$aPRKksn9?_YO)p=;)p$9xsPdvx8C z#b5i@<_lgHzwv_)bPjy+%P;)XKRkHjqrX1oQ#+dfWzWRib+H$o{K3zH<$wI?SN`_V zNACJ{+t*9~t$NQd7F?HK`vpjlvn#&))OC?W^C_)^U7gCM0^~U$eT#ZwR{0iZCer?A zf$1@~KBPl>&eZnQoqWi0>X$$8%7M?^Rs7s(pSk3!MCs?Y44(6&hX41<+-0A5(OX{e zrHThy?zrJg|IICa_M@&9mh?e<^5I(MPJjL?3<&pzB3q{ zj)k;?n=XJlm{CaSCAc+s6;9;BB<4iuYeJg4KtZZ41E9`Ypv!=CSbLVij~L$f?Pa#g z)g2PHGP_N=U$Us2R=}w&5~8f36aY77M0?%Q@mY`pJPtDUDTQ3;CPxu7(h(&1Lz|9V z;HN9iN9h7sl8zpjClm-qY^x=dBPCNQ2%(@clY}~SEbmdbcvE04Wd-R3t7jd@62eUZiwhZtE!;OC>SLRwiy;2 zBMY`07VIh`Ufp3>&`TEVG%V=T6+En4Bi1YvuCR$XstxB`{Nkh^&4~)wz~|9? za*!K%<(}pNmlSYWoh)ui#s9fMmlZr63+Q>A!y0EPq?ea)8S+QYfZ`A?8PF@3WpQko zjIyQ7vTOUc?aV$@GNDyW=uRfIo(XMWLUFXB4C-Jv{|sm|vjrUCCZjA)=a~U*W46%F zgmy5YolIyK6WYy$_AsHnOel^Vm4QV+v+Mv9iX-x5w16Y>WI%C{i3})?$ddsbX1;BN z31zP=oH{q7-D{aGT!+tp?q?kLGvN>Xi)cfBLwyn_rP!BJyOPVx4EsXUF2&9S!2`h3 z2BeO2?n**dx5vhr*H@i6o`kLA0Y2SH`iB5AE3WH1hWetdFXA%iu6 zZ;^{6Q`?gZ85&XiS0oFF(5<8ikuGzDNMXp2DeM0+j(d!MQg}glm010WQ<66*bgEX8 zBg~T1Wxx!bQ1+*=A4j*e)FIc|DEO^M=$2|q*yXa)3;MW7^id-F*k`7Xb)gWr@X>vw zTdFBxm&@A6!?Vf_g(QhHllv5sT$Kc^8^>8mlJuJ)2@HoW4XJMA=*IBKv5Oud!Wc5U zuZQ`3c;<>BNlxVd9W-x0PM0}Ep|Ha|EK(MhdBxy<1n_G*@kdic2!GI3&8yV2u{F#jY8X*Bapk=1~ z^QcuS=v)>5Wd^jG{|bgS)HP|r!ul#li;fdc1S(_@7ng0C7zXKdqczi($l7TA+d2~>13cB^PsnK49uX(|l(J`$L^YsDo z07ils;E`!J1L%_&K)(ldHdHlG*U{{#8*-MpM*G{sA`ZD0stQ)7BXZ4H#XE4>YHK7N zMy;b<_$Y?%(No+zE8v@IXj~7St`n5Ek?8Ib@Zt&io3(bQ=0Y~{?YLb6G3OxnpA6^_ z_apyI6GR1>YtedMKw0J)Bzt2_RJBw5m4Kv z48OG^hv@)`?@KNz_-su~q63niUS{yoamW$6eho5nSEZqm!-U0(f_%6I82SZ3=ARph z;dGJ1Ib;TXZLHKA5#5MxIMpIY8zJIvO}Jj?jWIyb#ByWkDgxnTcUUkSUTNBW4KFFy9lQ>w&Tpphp|JNd8~W z{hWjU{tx8;8~F41EBG3|4Wj=~@U#3fe;a=f+yfrtzsLWKf02JxSTCFl`M|0{8?Z<4 zKpt>ecn{AyXxEnkL5`@2rCxIUEGO>z)3l0Uo1pWWoo9`a`|`LmDw*-!o)Ab$>$KZnSlyUCw> z$e+XH&k^$HDEadm^5?bW&+Eva*ONcT$e(-3pHA}UIQi2>{?yM$n1s}9^=uTcNarV4 z)Y$w>XN&eUP{&O2zcqaFJHsb08$S8{27Vs$f8ECkyTnVm`@}Nt^mvVYGCqVsN5yi-02^{N8m5uA*AwZ}{J zYO$$K>sQJ&YoV3o9%7AYKUBIl@;lL-R7+|YjhXBxxZ3FI083ur-wFOd{5wU5=s>8wu0D_8Q8}viD*YZ{ zt*A7Vy~c;-QpKEX`gtje5Nv5n-l1sz%>$_z(lh)TJh`U(^1LI z+(@Dfm2?m#Jq~S$Q+LCb;4BKM2eI_Pj5ZAFXWbPX^nhII5X9QsYkQwC?iTNDKC2@67Sf=rmxY63w} z6u6zptHQFwQsbmztF1`kIrw4mDPSqoaEdYLyFVAODabdT2ue(iYt6>Cw}uw*B< z5&tjp`#G@p`Gat`h}nF!wH>}sJS<;?*CgjpG@{ouIrWmf$!2X$s&@rV)m#srbttuQ zTHi|C8t8`5+Kjtpi}JRy1Qu?T&FJ4Yv0(hR!7yb0OxrGM1?d*jTY%P4qpI|tppN6g z7WrIdpOS9DT1_iC^V4>@G&`L^BNT5tVu2IVqRhfbdhf!Gg3xF4yUvx*Q(o8iA$*Vx zlds#O;cE1{0hO5rxfIGxnSKe~RGK=*b!f7rZd1_Js&@>^=cg$19&EAP-$9I@FKbs3 zK`a+rAmm#B;vV>5@~BCK(w(LoO?TQ%ZG-NsrrjBGx8i-xBoh8rZER#40TUY=g(j@M zmz48=1b7+oo#J`oc?Dbd`>pbY%D}bU)QVb-kqghBb>MU-N_wQ|R?;KIES~$&bd%k5 z=*(E^T)>*_a@Ffy>1^JUJQY&MOH!pxN0sJS0Eef|g)mfe?Q2OGt-pn$%?wGK z4jpYk`sYG{i*vZEQLS~ZA!%UcPb=&H;~e*KEdBd5z5b7y-}?vTOO&ngnwzlWxW}Pq z1SjXS?m~L!9WAnYdUK@POLG=kD(1fD47CtlUmzWew7%ssji@FUO-v&i;heV~9bM9W zHnGaEVRAN(!$s1ltfr(T9E^n^GHWhmp<1!71zEIy=1q#iaU6!W;3x8p+&N(OpZ53> ziiORP0X*&V^F`rT{C0=|ewuiRSSt>Q?-d^q9~1ved_LoUlj4#R2Y~a@{y)h30R2B& zt#{dI3Kd~5h%n*E(bQXn;{ePMt{|=$A3!b4_{Rp)o*45X@e2x6zofkV0*)ClL-FL)qr3lEO# z9Gg0aigqHuq#3KbX%F%mY4CbG2dwqtjrdJu%selTbF)+7jHmc#7-gsVUt(x=*O*3w zzDP`bnSUAHM*9DZ7nLJ-mKnl(XQa1B_%0M3_4r(xA6EZ}@Cn>3N$@Z~&45OD9|IcY z{S4@u4%w)ayF>H>Zu1R#C#GrgcUIXniXJVJ28HZ4E@AF zIZ>oeXE;aKK1^1CRfbk&Af2pG@VPQ^F{1fsJ<-{;g+*GyN#oNqjuL@`mO{Dp!J!-u zI$e`=OAw%dU9fO^m=*Bsm&+AAFH3Ahc>2k4Ycerz+O-VFN2MFkG2!_6nqYOcT&7_4 zmIQWJsgQ`#YcPS;pf~E1=z738NH7x-KFd-*T& z-{GI(UlBG4B|@dpsdxb!p8jcm;64%i|BBCZXwO0WR$7is{;D4u?(?(=ZYES>Lcw0T zlpiAxwef2=BA--OXi;|vw}p$UnJtVlp?jH7Clfl(gu0l}0^r6#_&OF9u4e(q5YS1| ztFMm*>}LV72QXcIkX3Ss1;n9m#(S&U8&qy%puJD{6{6HQ>4-fmB$&{9#pn4GGRi)K zpc&)s5Km*wf=VW|iV3ZO&FRzbsR2*g4SF0B-vD$|1{CQdGNCg}XqX9&FroWc(4w=M z)OiLG-A$hV{pYZND6sdLbjkBJh`Yf4{|=7#ad!xVP~@oj#Jgp?vb%0goQitYOP-A^ zdlLF=Y?MM^D-RiaAxtY>9Ub=Rr)#m-!f9_KPWwg@BbEsejgczN0-cuLosMn^l60lS zbX37L!P6r<6g*R20g*}KTx8kF(0KO5qOPDHvVvh(2Ol!RaE|*xwnjAtY>h$5wZIf+ ziT0iq9MZx5M3dxP^ ze~LH|`+u5$r*OS!Sz+0&>7#f(yzcjS6?4-G^n>%Gv3z^KT&1k->389zxEKl{{gsgY zQ|Zuc=_`kF$;hyj(YsWEnU4k8&te~TY!*znn_lnQMRer`a@_+!8xsKeMzylDrR*=e z$@x3TxKYv_)yX&L+J{TNVd&+UZKobGfe-TRp*Hv-*c}V%?_<0^z$5EQWkgc`KBMi# zM!nkv1`Pinx|PH1k!6e@I{yXkZjQT~e+O5Fmf6Z5*&;V6>u=-$mgF>&5>yD$k%|XS zuRm?sq?DwVh9HWXsCcSrwdm%+!?aGJe~Ookk?$L4&_6&9(HuB(Bbu^3;R$EJyGT)zt`68S%hCIkTZ(um0-$Hpo0<|1F~x4l<#lT% zDBq_HW7U-mQ>%1|yb*rs@Zv6HSQrjOYD?PS0vCk@iHg0ECFrrh%^)O2m22HLixVeE zglw+xC)^ewlP)C9j(EqtKJbK;lq|wt9E4Kqazq!M$;eOX4I3yPByL@*yVnxt|M{PC z{7=F6@1yX)qw)hb!>xF2s5Zl$cHJ&4`{X8N<@6lHHog=3r+rf%!^h$0DtO0Eh$9 zrmVSC9FQOtGE!SrgOY1901+o97Sh%s}P6Z1x1FE=WC<&FcJ&`{GFaE<6RmjO!}hyB(H&^6QHuC*dZcBSiLe4!q( zcncLNw-Uq~MKIfD!H$iK#bIcid(h1B_fE63l;|C#@mcrl07(8?YVx5?eg0^N}~4uRs$ za=QYkOPqxO3*?qCtqr_xu@6NcV19aIpsNrY167uuKB~Knek_f-%kru+LuMOXIXe_g zI+V0VeiD8>b^dOi_P%AU7!wRe}1eimp&z0 zRgX`{fA>v=wS}(}1xGCX^2Kr=u2ed)K*Re^oTC`0!o|GN~Oy})p{FyfdCuv=hbT+_Qn zir;{JKp~(MnDvH(0mQ1{JI7>j4j2+|1g;yf>wpzlG-zoXw1C7zmz2S9{jY=79^^gn z&A*rTK;Ew~KhNLD%ltclBX}R=0QfL}ALIf1EdMb7RsQSzxA`adA3|1uU-HlKFY$lm z|0W2+3Bt+3M&TUD6L7Jx4f2Fl3A=@E-wCe>uZczCiQ=i^Ch$ls5ib?D!`-At zyc+hWPO)Dc7OxlG;*=N?XT=-Dgm@4<7jF~q5bqX0Dt-bYjeY^FkG>{;Q~Vy-DF0M^ zT6|V~QT)C5mn=`AG!Wq(*h%=N1X3H`oTvq=I1*RE-@n5uh<7H??dX<78^AA3bim)Y zCc5GKU5S49em4$C{8|E>Dfw#?*8t?U#P#s^xd|uyefU~_UqXWRZ^x0$-;d*1-;n@= z75=USGDZ0n&T#XA#2i3QPAmZAgE*JVok{qU`=_J`-*+cr8t``~kB7fsNuC7XA4;Mi zi}xf?1NcZ133@-0L<|0-Nn}RyvE*ieHzhBGzdxSb3V$~yQTFKjlUKmsLK4L{c_3+p z?@uOe09l^|u?+v=WEDUjOkN3pe<-;V{{D0l(e|MvsCRgOvH|}7Y!XrUb4j2u_xWTe z!0%0V!T0gWUiiK)iRk<<$szduLUI_spNGt&9RI;2%1{5rd|wxkRG{!$WA z<||1wz5kq?gzra_)A0S(WB|&HB@vaRB%;!*$te7NQF0dkK9odMdOUdpeE)0mt?>PI zS%B}q%Ev*OQuze<``=|mrEkbceD+QG6o8*DBU*W7@Tca(GNRbGWJIxV%jdxNf5_*< z_upg)>c;JVJCPu-U)8U>ps2a-v)cb{b*&w zu5cGx)v!+h3|6i3KeToW`9J3F00e*!FUr@!cZH1B?HMx6C;kE%t=?y3m`{9>?1sO$ z$Y@q=mOb#jRh|I&WireyJ}P_Rdz;)2-+SZ^c!CyjgEjtD84T0}UXH=HAj8@a&Xwl@{;~`!L@1IM;rkkS zAAFa~OYpr*z7f9Px(p2nrOU8p1bJD6@0*v`!T06MMezOgW!PnecP|&ij1!mb@b{C; zFw4b~Wl8uncib{iM!0qPIQYI}8CI9@MGX1KGSE#dTCNoiL79^Qe(y4@E@5!_Y~eTD z`sH(k-vRtQfZVfef$wiD!|D<(%Uj^@iRFvodw%&6ahTh?e5rUnH;UfK?OcYn#5tBr z;k#^kJN_<%@5`61@V#Xjt*$N0uzztEpnh^^Eu)oo94>j>a)o#`{9O&LtzU+70{(`* zOzd608onP`t`l#7@@?>a`f?9^uV3CP9)vP5m&8vj!(0-7vyA4Fyex@dfRdB&efqK& zzSl2L!}oE^0r775zES)He7_Za?}y^tmi=h=xnMtv({a}RP4IpCezd==-;eg?#ZHUKJ&I+K3?!*%bsccS_4e zR|Z8YqZG|Yt}WkhMTQ`(J7t;&r1*Zg1VY$O%-%(vaGi6(z0a@Vg_*e(HCo5+T~c zw|9$*x8IZC@vr_bl?haTR3hi4$)TZE?6r=(;rqPp;-)jFJ6tt4Rk^!yacVeiky{cQwhv};n_91Vn5yIViBrw4M1}7Q7 zke30RnG#Om>PI5_6q!>2u&=1$wv9yGqiJWjF01e&WJ=vM66+WnQeEWwDnV9)`%1>; zYZJ~w&{3;?FsBxTVGJdmiJFB93~d{1NMq}lgc~rFw2<5_Tq$mWK1}Ij7<5b(;e?!Y ztbJiUZed#A2Tu$C5{42~bPjVrU_eK>A2Oh$+>bCcI$5nI@2u*d3J8xO=$O^(N$1(D zsuSyGd^+er6xI7J zsPg=7{REks)y@H0Q-$5NQ4#>j=}&8i=;}2k>~cr;#L&4_A^cbtCb{42&6tv`;R%r( zl?RoPt;M1vZiF1!nBmAaI73csg}VSr0YmArr5k{JMFu60jV`ir7&?sSzq0<%03iVX zY--IX`{Z$Dz}xnN9G2sT7Kx=?WjNqqE2GmnS}1ECx|xi`@l<(w@bg#xnl=?V3be#Z zg01u&L~oP2%>s@SI5IKV$ZxYmK(C%!Xoh-Td86!72KCAVNMOuWk%L-oIH;XqR1a?m zN#r7?>qA6PcEPN}sP`o>v-g>1dM8Py*J-$n=p3?!1slm0T`C)$zfk}^++e=qPK_yu zew_iZq0=<}V3o+S@j<3R{*V()5*6zoCkh%K0C_othB2HMSpF|O#le5aKF28V2BSb? zGY&+sx%7OnNjSf#XqF6=5v%X-kQU~GVfQfkAmoolqLx4~N>`{UQJ<95Xq7GdK%x`! zcRC}{uAmz{l_Ym)q5YUc?kZ02(5pSKabd4mnVd355_ zi79kER+er+Hai^G0r#@_jfrpym@Gm-2XR?*Er9hnioMnB_4#lXOBj90H%HvZMkeS= zH6yE!ck@vavGgnO6M^K z*4k35qZ0C9S{*Qhmg4?IL?z2Og93|8M6ys|F&_$afoBbzg@+O0=t>Betp;I;UAp=q zZvkhbux9?69dDy-lZ=5zBaGI65pw)36W0r8@W?`7#ZNpC->wn}88C3=#B?8xnIaHf zDIWsW_nMp&cANP{;IP{rux~4CAdX`WqOAe1fgTd$c~cnEmGZ%O5X{6IU{A<48SFOm zlfh=KsIuFUW$IEq7RO=j5W-*ulrh9}rVyrvcrG7=b;jU$i#=S3hvGZbxeW$g8HmrW z2JzVfA)Xy)^Rrtw*!cYCp5wUZ_}>bD5|Mr7(esHf#*b46Q5&rGyyA zxpiS$W8+rNOE%VRsxi7!KBL>v)z#e^gUj8ZGcw)ih5X|Z^4xHS7uuqdKkI;3m>=*; zTV++1a-=7OI8N`5$bikSD2#emLx!Gw$$+-ARZ_wH9;c_mx;Rd~jHmz>yBs$_PKI7n zRG=&6O9gl-@+@9ULZJ1}^X~=gf6*p@t?AMCN!ny5+5{kn-!d|c<(dWwab%^T<7bNh*D%^BT4H$RO(TamuD>K(kpuDJc zkZ!cg3oc{W{YxuC*F~XbG78a%uC0Nji69`Y;AW5Vf`T`UAKkK!qOcx?mnzc=L_>L` zw@g1TJe`IGePlr@{s#vp-21qP4SoO6ye|Cb059)UX8Fv`yHN9V+#YQT)3aO+J8)*LZk zv!ARvYQE+yS<_gOp3z%6#*!v6X9+qCB^Y4Et3(Q#RijJ6Q3*N_6yL`veosbMj*%Si zoTNMu^Z)$W;AH~l)#ys(FG9SgN6y_Xnfcdb3|9lhdIKF6lIwuVk;U> zmx^k8rFpgxndif$H-U3IBb@Q2H$I!d`MMDRe8V7LH%Wv&t0BUkf)Jq&=Di2p0YKv;)u}Y^P~Aw$38kv6crrj+QRhUX3k4Xx9sgP^3fx}VCd zi0VNwBcw1`<*en9*$T5}VEw;{#(nkcri~mnNM=a}(mq^+Hvr+1+%X?eJ-v`%o`jWVW!>GDTnP#uLC)cgi?gd9*%*Y}1cFhq3YJullTG{1waU@@a@ z&nuiec*>ePU)0Us=+rQEgt0gI^!d?5k`0vpy}?Md^e87B;*VB5oU6;!ImwdPz)UL$ z@rFp^Ss@%Aww21V<*(UfTbV@VYeAwel0@Y|qDUm>4?ObAoK@13u$KGM5^6I0zGc8HuRMvA1D!>T^o0``A|%?z(``i@bQ%@%Mya#^`z*Hq z6Yhok|0g-|boe^?LmZodP#A=mXw!wvq8Xn` zT&5EFS_YBXZ=-D?M`ebx^B)q5g{k2WxbK&Gy_eQPYwK_RHOfS;XF4+E?P zoFCY>UY|37H6vLMh4LH}L$<04n;lUKw16QMajXNT2F#ohFkLAh z#QVYO8U7!Pg5&7vcAKrzj_vK1V1RLsCT)PJp0}DBV7gL%==Mu439g@o#8|f=;?r@QYEE1KROj1P zgK=9v822;`fljiJ=)&E*-mv8H1?L(esU;G!bc1=hO6>Sd{Cu^ym?d*-FEnO*}-ogQhbev7INHl^j zm@DywHuJ}RMRkR(3hxn1@wxZ~D$)@18^V~ANK30hTFMXUHk5744`vd;3!B8p9{DC zzxwx$XhKB^syd^ls7hB_9aR&2Ty^kHo{0KVP4>l{gEw7ib$uB`Mi(WT zZL==RAu>z|2FxsRhxuz?byY=~4R87hE*?t_Nb&?kghrAGR#O8)S1L3SnpI1*Y}?%G zXduRCRT`Ykph5X+XaGvt)qDE0CIa5~Py~1r!v9r~;Y2>coy~9LgyZ<#!g-L>XCr?( ze>LxeJU*?$^^mvc9m3s^ujAXo)50sjA8gU$eWqf3ikbha5kcVzibOckgubN?$0&|n zh+=>C_l=30na~TF&=Mx}A|`YTznj}6IqEyZc1Lr|@SgIjKA%Up9nj2#zMl#GFm9J_ z;j2vOcbU-dF`>7!pkgn?1P##r711`C(chx=P7-LA5fCP9v<`~$rDxdv4Docq+>PccN7GDcryxo zd3ex{CQnk)4xt%Oq20L}v^xueHgcH?n{fAB)oe%WU*P`0aeolLhR|MC`q|ABOBCDBy#?8?4K~>w_vVg+ioIFxX}h6yh?{l`Z=~P6ja+A&0Ou z7zBUuWJhvBdH-9MZ$R&FA>SX)Xo7mz6qd-)wQ5QfL5+zXfMM!BKlxIyajEaklylX< zk>?FeDACUmBC0VJY5lQ+8CFHLqf)VHWjiV@Eva@UD_FKV6b*FFmaXXJN>!F2;R?db zRG%4~O-2mMJ1$L5qT#%dJNF?xooJ`Av_o0SfR-+RU5&71Es$GvKEu-nEX*_gz9O~z zJfvS*46SsSrBVeGX+Rx*QjH6(O|WmcAu{1;`8JOqc3oiSv?s zl_FOpQ5Ht7gDf(k9rNJfgXClb$`F@y)FNi&I~9i^TXfGQBQ3}o^arzPYC zy&Q_qGaU%dfm_yuWbuT9ehcKEk3?YyLm8=IL&wpHO0rVM$YyCN(UHW_=}Swu%BLdA zP+PM%8)cAeHKs&RjnH)!pHKB^;iwak=Je6_81_ad{aGy`91cTrqV-?Iox=$)bLW5z@N7g;_L0!891*uN5_6QgMLF>4_ytWSX<8#Le@xEfRJ6M zjCBfGIwHO)WSe^EYJhChkwVsRWD~`*s32RR98Yv|EHlWeyNm^2OrsD5Izl%C{9NkD z(G@yEBafmvv$#L3|Ks@v4l;gS!apIFtE>NL|My^vd@dTq&85;UILL#QOqJEE4PwTY z)YDSeY$>&9gM$p$BRJQt;?@vJXR5)CLV(20?>bjLPkCM6hj6^e;q>d4t7WV&-(1@C zb{s)*kThO5PLQLf`Rts%I;R9kRqZGv7DXU222a2>gFF2iAXA!awFe@kkOY((R?^C-$OhU1j}C7 zvhFyi*P_ZA&~+h3Wr}IgO>A8|tT|ptw!qMEW+`!e66XZ4ms*vuV9_B0?KU%L!=NGq zR^72cw}Sj#=rTu$6v!%;d2}S!C2?{9d+81(WEvf|(hMmYgMQ%Obd;bQsVT`Qa)bzh zGz3gSz~uijW+=Gx6WFGNp3W>ns1uwnYijD}#^#n)+YAR0@&6)!4#%G(oC(tZezcNT z^@;CG^dcf4yGblI?;^zJu0B%&RJu|=0@V861_=DoFd2kkLJ;saG`WCe0&rl+_A6(ZZI)lbijmqu-fS2$50Pe*{t?*>=YUg zCWckiJ!m#mk*J%ChDTS*2ld8gNRQ(j0Euu`rH?%aoGZcirxMwLFU6w?9A^zpUo8Kp zdauS=YgW^xVBag+zk{xnAIeShAd3VKpeAsyHZy6>k98VvwEl~@gB*8|zg}n-x36mL z7obwSU#>+{uA=m^#75K}9j&#gPTNIc9Uz`)fsXL1ar9VNPB);tj^XDeHH;=PW0BkL z3ZZF@bj?d~x4cWi>dM5$2rD{xFQW@IRz2fL%Z`$`;X4Xg35J7C*CgFiO^HFlHNx{M z0#7>Bun9a>b3c%B>GFL<=$2|q3<|CZp4~F`0;>R-7#2R#fssw%sZZocR|jTC3c)1} z1h*hrUJH!Sq}-mqTfzw$3@Z~Yy_(d!h>q`KG2pCArc=)xPmJl-AbwL$7fqtA$vP7k z%?k3Cwms*Irb+{)vnKAESmm(I!aJ zrcp;5Z1Isp5Cp2JrnRJtW}T8nHnH-2f%`^*{QvQLnD>8J|3w_m|9h%XEUtqaKzt#= ztK8or+8-84<1lkxpLC^sSRu{$M<)Wv%EMP0-PbZ%Upu5sv{ovtZ!96G$m2NR6dvZ> zKj}&ZLB1yzb%E+JJL2ZOe)NdXC$Ll;F~Ew+7dh&UrtWW@RHcF-PK8)8)aRhE@2kUo zE`e2D2zPwdqtIwt4em_^!M$lF<_+OEIB1705b9L?Z;^YRhV#4w@nhU8x{sF`Ea#VM}X1;x{I@A#_k&XpG|+iB2Si4g~&m!u1;RYD&yY z9$ig`5M33sNj_+3j|35YqqWxUIzbG&&#=OofPZV$8JI9(-l!xrMD63`owcPE6_sUW zRvdGQwb{pSNS3B3kidboC{$EOEE)~^noCzJGzEHtzJ-bG9xsku@lTwR#K|ks3_#W| z9EFG)x+X{bvGI?=p&XC?ORm(Gs69%iPhQZcK z$@r+75xQ^=OxY#6|MUE-9RI4gjeiI)i}Y8#RlW|*m(5&50w;~1B9mKHXjk$K1~#oj zqwDB6I+c4&7pAWmIbOxftEfX?Zj;9_2BmEW@XlU8rN5C(*)HP{;=+8aOO?W_cPf+N42>cqam2_MwD#ilhxH4Aa}xk$Q(#!(=|RYC%^p z`PS6urfL5VI9F(7haRC2~NqU1*j>OJj8A0;OI`oZ&pFA`0O zLd*#2EuBXZ9n_U)c>Z&baNHxp@R8I0rLP>h{k4CWj00kCF1_xiPE>V{9BYW1oU&1- z8CccyjyT2{iE-%$b*hpuDB+OdCt?5?HE9lpVr?1Pk_NK#-CzWyEY)1J0aPPBCgvfH zyS{pnOzBBZtq$k3DFC;tAN|muEqn&K?gv!)VCJY(E6D-In;%lw^5-&cBAz# za(>8L&3{n%4<38JudoIo9UZpv(bCa%k$yR zgxfSd-BB$yE3t!?$1XY<v#-#AtevRM$@X;vExBW<3# zaV_%DjD2Tw?OY)|QC^LR zx1>^X@4tnAvG-eN&$-e17lh*={uinHTY(2S_UH{qf%t5kL!1Uufa5d=E<%q#YA(7t zT}gLrl4i4bB`F*aT3BiEYC5L_N^=0#9uqjvL7Rybxb_P3@thsD3cHeDcqx8ETs3x4 zl)@Ae=8RqFO8H{mBEkOR^r3hbaJLE^M*MO7FYZN7ST7z2|GdFJiK4{usd=8n_M4QU zhq-XpbftVoTQhBH1TT42XOc?!ULTw{KJlzJyA>I{7}*|YS~=;gK46$PVLGVszc5Pi z*#!0i)^-63QFGDU=}HBmPP5bH^{IL+L*Zy{20%J`0Or`)+X!=WQQaxbLH=BTV4z(f ztPP`t#96MHxtKobFjx5hWsYAK+m4RKzih)A`^wU@-`erU+Y#r?C+_kg5r*UN0&Jbu< zkv8Vq5(s89;SH2VgP|Hr*|jCR*0i5HcZ7EB+@W-72ksy4Ig9s3d$iqNX0xYCsP`E) z31pPgk|OvvX`w~|rxBvXi*&UdXkp48667Q}EEd>5H$$EVyl7UK z{0^MQ2U9#{6$$RIRTYL_flKj)q$5QK#rl^*NR0%ZBOUay!0D>=oG?X)7T6%NBac_U z&H&J6wb`l_5j0E2yaDq6iriN@{vq)c_~&)^i3j6YMvgc)59)n8adtXY898|YrKd1m zNjHVbig@5yyK(yF+E_mT$0Q29m2I%>cB93~VBT#NwsNe-S&IAPU6^?Xl(D{Mo*dLi-E*{vcQ6(yI1dRw7}OH~_@Lq!)dg&@!tYpBq4!nCDZF-2Rt zlBri42trb#0PkYjer$!!FPABWc8Vpm{ssO%PArD6W1pjz0`d98g1YFjdyb!QUU1n> zO-#B{J`=MJJku8<-bka@3DFh&*ig)rje41RKXoM{GYpD^aaY$rg@%oZJ6$LXu8>=O zUL`P&dra47zJxwV=t@~$iWPQ7B2bFJhljR^{}=fk9KS<24>*9M;B!>p|Kn52OoZbB z=ROV+NAHNaL;-ZAd?ra_J(@m)=z^E^_$@bb49a9T@YxU#dDInwy#uIz`%SF~x>7#K z_Vjl(sM@#e*qUdq9QMlU3I*lBgo-lG|20C6Z@?7FbftVyM!BuctbE8~j0A)X7N0S= zy8PBCUeOlrSq}btEtUIcH}B&U{O$aM{MYy&^Dha<3FjGR1FC`iHH5}rul-jy6QpaW zH%jhKCiE^Q^q-i}yP41rF`+oaO-3JZE}IPK9n2O!z=VE~2|XHcyNB7cdzsLWFrgo1 zLO;fY-p7P~oC&?334MSG{R9*GNhb6`CiGKG=%<;`&oH6?%!EF~gnpI@{Tvhec_#E< zn9wgUp3qAo4ohsz*IXb${-{hy8Xo$;-l($heJKsNyMF|&O5qNK8S1jP}RdQ9eQQO zYp;3qwL~wF3i6s1@=7X53J;WLJbQv8x>7-r2LYzfIUWR`GBXJP?W-UHmj8p1A6Wl8 z#0%bJ>;8zL8y}TVMN_W=qKKY{deJqY_fhfK7;&Wf$Rg!?CW98xNv1fmLCp#=4RrS7 z;JYtH^mO&~89K%B^RyS$iC|{Wdp^JiVdqx@BW1e;58w!EII;O%%zqJ&%$=Ix8Gq)pg974}S_B^Qdv(vc% zu5<2m$LW$Vk?A72_dD4wV-udurPlptq5jb1f&R$jGIq7zTv~Uq43+Wv2x`x0eWvdS zI0jQc&UcF0%QP}{6R8kP%(66AYa4+{=M;_bOuGmH`#}vkNr93fKnMjC+>UCw$S(5|SRh8vc_9`6j zT9NvUA!iY~x|%Er2S`RvJ@87ZG0Q$+!;G9WAntc%C7w@dXLHle8MaCX zu9_2@g5QWZONONe?VRzDyfh&@arl|)Cf?zD{lIY4cCF&FG7i2&= z!XYP>Y!%$0B#>Px&4S!&*69T;l@Id$LWoqCGaOz(32cKgNSO=;$JHd5-~a^9L1~Af zZ5B{nK+>a?0w7C{|Bt^D^@?nl-Zf{6w?K;B)vY=*}eDC znup;3hm`ey1IOcd&^he@Yg>5#fP4wQe3V{u6E=YLIQ0D4e3^n$*X_NCnskUjt#34kucFlA3|aJnhgFkN zx}h{KMm2IrqLBtBaaL@BOoeo29ay^go@<+8+t%l{XTa7GJXqM;kgdf}Fv~e5c5SooTFKO)MoSHtQ z&rGMb(p{!N+T8$7{oaw0pQ=LE3*^6aKNbH$%x7L`5h! zN{h0Dx~zUtZF=X42I4kFJ!@^{=V7p=lP_6TI417+FV| z(a2_+ecNreN_!Qy`-5S|ZqSvgM-A0?>b@JFPNZToY)Ifhgew;hhH+dg+OV3gln;H( zW(X<6?y!uzRwj8WtIJc=iBBf>DtFrr32dD~^?YJA(_tbXbbDsA2LN%b3Rh(Re<^n> z$2|ca0Jm}<<-S}XKLCgYsBR`K%VYjb7L@xF6Z&%|^cPI%(@f}7Oz6*;&}W#?UoxS; zVnY9i3H@It^jRkK*G%YhOz87W=x>jSIiql00yHuJ z2;&4Suc$0n`~s%qSTcbaB7ETQA@L5GIsxfQ`QhE^3}kn4%mv%3%8K%8#p`b_j>E_! zba9p_s^>GSL3buUboF`bQ5?W*oS;1i)U8&lqpA!!3}XGiz(F#!5}`F?`eT4xB4ci7 zb7{wcixH%gOrhm4lv3$LD;CY_F)eVT+aU!=j0#y~RFs0+OqLzf8ChOe2B$dLt4>d@ z6JJKs#;1j~BhAaAISj#oQ%14r=|P)BgC$Fd%9p*`K& z4IMO%5EyM>(iu_$Z>97-Xsx7e84NFWkmaSC8bt_(I*vmiLXs)T6$^u!WSd2AE{rUL zBM@>8Bw9%A;w6ZsliHB_0JNaBWM>nQX+ae&tMWVxvL;cClN-a3g&wklYYRRL;TZ;m zQww@}O+aLIrO}+E#AF-&Cd8puvPy5xQsPuEv&)4mu?$py`KLfD!flO2pYUM%- zs-3~LAcd}{g(62KNsb}KEGZX4P%Z3U3qqiUf!9?QBf*ttglZ(1_lBX&Jcg(P=lDYF zoc-Z1tedF^*(!KTtU=x}#ppiM=zof|wi+?qalKXg9mF$gSa6ao=rk<2n=CkPSg?^S zs7Zn|P6gWh(y+x=(z-KdSde!9Gc4#J9nl zF?qaX!Jw{1&;gc^E>pA~TvwPZ*gz;ZC(8{BW+)?#CPqdgDx-okWP&JT{%o8qXuM=Y zd-S8@oi#*epj=;Ys?M^m6 zAl$GZZJ%yfu#Gey4=Du^Pf(nB9Mgv4hOPFHt%eN?M#+K^!-9ik!Kh)uk=?{pGv4&) z$%4jfdyy7H|& zLe_GdQKFHhcq|!H(BG`Y%=MD!cWUw?U3Y2pn{c84Ns7>w!4p}1bc7H$vamq+>aStM zxvdEtOr9P$7UKGmq|2Kg=iCObth!R?PDRT96#Kt^j_Vh&aDP<`fT)!0m9eql=F*n^ z*rkX*RW+*6a{vOoyHLgnraKzl3YY_~R9rV#@XDZ+0}L@qu^VjBQ8GVe?NCc8Lp-yh z>{ho{5VmQFZ(1s^%2!K+VK1_YcO#4Xm=EGDAgNj;y-}zc{Ag;`6h7~KhHNDzoLWlm zmiM5UzPYq|zXgpp%>WsWsp?~3=>k(_trT57?TbK`JL*S~@IeG62J&E0!g53*1&Y@N z5jp(9sCPE4t3Ap_7O#H+?Elt@MZ_{b_T$Y%f%v|-5?~+n-!~$(y9sr_Gi*x#O;^fC z|D6#A5PIMO1qt394YyvfbamvA1<($8F=4Y^iqFS=Xm>;Ef9zU9VG>yl`jLX7uf=9q z5qBg1_e@g{EeIl7|2%&N$DbjVy%{5b$=l?u%E{JopaxC-ImdgileZ|Kw)=5JL|P!L z9b@#7tm;nN0#U`I05X%M4?Vh>w62w1o<962j)b9IwUd(LL3FIn#JpjgXe$?W8r9H{ z33VEcUCt_2eOFB=7O{t0Ha+WZk|4Wvky#Y;>KwdqsJ8a31x-{YF>MQ;Mb~JGD()0j zVC7$>tp5*k{0GHz(DXk#K4v}R_V1Fh;nwET>u%~qU8gk&S(hroz;$^clLZRMmR_`U zgXu+^t!D1aw}Dhvbl*HIU!iP7Be!Gbj8=PO#SVGFsDGnUJyL^Wc@*n!3{=S@lNX7&w9alg;DA+?i)0858D7@qc6 z04@u`KrYmvS~6qtKvM(Oe}lsRU&sj;il-hOYyVg;jw%X(?@ubwL&_AmjP#A_FvT3x zmGWVZ>w6(prgI<)p(?Xp2dd4p#HqW0FVUgi1@@wWpnBf98gx7JLl-6fn{#H50Fnc; z<&FyM6PSws^?r_jzi?dv=AC?^Jc*AiZpVSsk^ApJ$W~|73=F4TnveBhTf>0jYn#d* zN0)@th%SOck*a?C`#?4|<_E)3DU6Q6Hg|0a)xtQQ9R{I4>=h2U1EosjmY21{R%x>; zd07X80}v>r8#w4iBRTnu92s1wvS;o6;p9V5C)G2N=WW*E$HUC2)M zWLMC@kwkR<)>kI?KDw(0`TuPN=Rc$TKim^JX8;1vKk*SUoB!X%On;gM3B3ZH z`HKDT#6tT1r^+2HgOG|F`$uiwmX%sFd!~Rpb z*y(p}RY6|Ra50rQJ6pEfVGBtkD|kmpC>lvFM4{R!Ukjq7da2OT;4Fn)vYg71BgF7z?=RCy+IxE*uWd4R{x&S~XUF1htALY}ynE zwGoKoQdS(LCdF`cN)yn8iM%LVD4I&C6K-6Z@CGuvFb>jxrNiZPkByHHYk4KM94#&e zS@DDKK3P6qS=%l52xw4gKPPQP8YxDZgFv^EWz6W+&e(-$JIIVf)EHynQFQixlQPQMmr8E~)Y%C1_&lNx%=rY?)=<> zX|%c+x^G%n&V*dkGx8XC+@SgyI`3KG;65AJVnpq*0cq-*y{v z^@fa1e&+&Yi-I_Y@BH}u`i2Fo z$Ue3i7Hl93YJPheeWW9v8@4z=w%BR5V3%RR1+v9%!-5@TLCv=?0~tCxxF#CU>8L?d z&nWbCF+@|9!U3l*Bhok|)Jo}0GP-qac!&E&4&(6Z^?zSK!e_r+b#>~rK7bWo`c9_*BOq2)`qSN<}J#DoVfys z51cxZWp`tW8VAoq{iF9-Q~m3gCSpp8NxC{R4OE}4rkD6;gcy#9E9@Ny?}c$>_HJ>; zqLbjz09-^Q=9;uPQNZPiAViG`AQr|l9&|4tJ^}@(@jC+`vcX_FGeADv1yJzt_AS@* z-(F_VHnB1dl$M=KK3q}=|36nt68;?Lo<5?-q&t;*UhwouPCo{RN2vee<>bKa49+@ zaHK~PrSZB|&|$o;Aau~ho+=RDcHq;jl{o(onr_AWW=WyjEIKO#$ejszwp^4?R6BDf zbS@boaM1XwiZb%X!`8^@da6`4Bob0UVb46ZdBPPEr9M?|Li?Ov!hd&ubqXk|m4&q+ zMlK4dl}Q1kfE7?W>1t+}2zu~p_K=h_4r7o}bCj%U9M>SD<{(+qs$E?&#C|#5WwhBc z3u0TWPlhcZOZv`?z8zKcVDSU!{O5%eIr#6`=S@w4WLUl)oq(W*-H$bQbQBM?8RK<8 zOZPPu3M>%hq?cC1(oN|6Z!*f#a(&9(X%%W%(?MIuhKT)M6Zz1H9?o!|*gzz$h8@rX zqQd29Tuq@CQV1-8TQL-kc)|KL;8OgJU2wVeN$?v6U>g$DLVl}=7pn|W!g6#pverMW zetRSe_K04OS8@+}{Zg&fQC?xSVS9}#Re$BrG!IQ5mvux)EHBs*r1Ce4vznk0p7b<8ByioH;zKu4Yu5QfcN=fF^AA&1IP=FOEU z?_nro%ScK);Is)wCqbiTsdq*sAJ|u8XShLUctV1Ci1qr4I@hufUpk@?XT3KV^no)0 zR-gb$klwy_C#}leLcOZ8P{W}P#Y%UWEObuYVIm|ksvurU0P<`CF;YHl8d3721ofWk zqmL3*Bqg?IQlbzuqJezN*il3Wb>*?ne?|Vcg%h@jIE2xgngu?FJd*Qa-Y=X2eyY1KMz(gis(jw+D;q%@hRcY`c~K+pAW> z)|^C?!geelY}rzs#&6^==db2> z^FBVo-_AeCe~tgKc^@F|R>k)xtN+gc(;wj)CbW+UEoDNtGod?}&@v{}%7m6Pp%qN1 zjS00gp$;ark_oM1LaUk38Yc8gCUhqgTFZp)VnVNCLa$~*>zL4bCbWSGZDc~5n9yb> zw1o+6WkTDS&~_%Yg9+_qLc5sIZYH#c3GHP<`wNLY{<11l*lp0 z(8`%RQUqxs@c5>MyT%ks_qk9s+D_7F zYIR~Ws+H-rAWIG+vn}OUhGnGlr-^ zr(fSrW)7)bm86_%q{_t5W@IeMm8Hl{Qf`VC{Uj}_!QW{C)H;64K+qGNgP7pC>;hC{ z+t-37N_Vy1W?%nRrsxtP=>ivMZC&MXsFJOC>(+uGT0iq9MZwm;LUP8UOxgeL;`m+S zHpC0OUOz`BR^EHdWS{l7hh(YM5+e!Px z8C1xkJ!f3)ddX09z_;o+9*fHItND#S?#fjRcA`8 zJx0=pVq((_PYx8Knl!`47Lr2gJ}WeWyZ`$X`=3`i;nmmc%0JfcV-#2=1>$dw<7jmd z_&bSHRai;mXLIS@=}P&C`ZbPQwW)&8&{7z=W(4i`>fLIUgpN){ry*L0K`4i`ptoZK72Osz3OZDtTMXD7`L zy=S-DZJ>$7uQ6CYEBpT{u0t$_XvL+XOF%;?18M)CLrDL3F{u6uqy0tdXNcyI}^H_3Ejhl4l|)6Oz0>R%GOt2%Pe~x6M8)pI>vc5oY#t*G09#g4JvTC#=V zKyR0l*#IdQaCmvT02Wozg)IAE-a#5+CkPmJJAEKv472 z&Gl|;$Ld4YTuxZa5IL-+=O?d5AyepAVq^wFkml9nb>SFlREdBZ9gBhofyb${HCt_E z-JoYdBq3VC1Y7C;Z(sHO-?qB=U&b_2@!0z)j{k88%m4Y`a`4}=&l{Tp^64ltui4m% zE5<|w)5@4XgoM&`fG$`m+OwSF3JpN^=jbGgeTZWadLtH;204Hu8G=?6MB#1}J!sAg zL55;ch&~hydh&@Cw5s>z8vn;uZBvrPtVaG zhQMmb5GV*4hy&+1y z0JP7}ifh0~@15!LtBZjbpHE(>B0tYSesVR)gCMu0hFKAd3s@ZeI7F$S4DXShM=A^- zVl@koyK?_ea(q&_WFybod{K@55bW+5pB-Bf{olrw}Xg72a$y#$YnJLrm`h_qNZG#2uK(5$wk4FdE- zyxK7I%mP=^S#J>Ku6A$3&=5q&_d&Q@c;NIwl2-U96!JkRCG+=Ed2vvo&XotxLDXp> zsWX%#b$Wx5Xz5fe0^xR`8x|CNH|%vwbgK}fm|ZYGN(F*ZM1e58)@^Zmz<3Rl2}SvSEd0Nxx)7+*ZV&t5Us$s zqSf60t5$daH|F;#`ClsjugD3ac-WTzv4)Ro3d9%U9`*RcXFp}oYVHm|S1QQyr*7Cy zv&bDX%L!`I|>hToIhQupvV)pEh}&UtHNJ7|DWgh=f$g! z>REc^J13te<8(rsOB)WXN8RbN>%&G)O&B`+=O_#-AE-rMC|rTK{eemY>AI@Ho0)T z&3Q<5fcGUcX(8*#Fr%$%)|QMgqoxCwnocy?*QKATo^Dn9s`&p2|H#3A$3Dj>aI{b$ zAthbtSgzwX$djm3GnIPN25G7u#ioxyniqd}Fe(i?Ct&F+HV$-2bQT+z#@9Z*;X+1r z3NWp%S_(c@<0QN2H!SEN3kD4fwvYwG>Z(lzH6vYV4EMf@YKIc`o^d{;{C_{k@t+fS z9W8@@nEN5O$fIa(Z7v--Fpl2U)2-her^*b^aY!5L!(s^~8@e;#wsd(xF^v@A>7dti zbKu)zsvdp1Glna5oWK4tzzj*A2YR&)T?ke_(6K)qcbFjACo|d8nVAe?=p;AYopic8 zy=0joV9$g_5OI4?pZoS)aY4idMOFnAaYI4Wr{emnJh%5;o<99g)$M!x*6CZF?j)T~ zAnh2E)92o*Q+2B9t5fHkVi`NNnFKBY7dxon-~tSBe5$_~Wd6|Hozw&F!XbWen~44j zv+f|TE-*VeeB>q)!epAP5y;xb9930EV~mU2K{ys}>GHSx2d#C7`kVX1WS|ifDxKEJ z8wb4sQ4D&FfQWdO;xR8?y-x$cEk?WP5vr2;JAxS|PdBySQ>S zXV+gsFR0~9mm6XTsNDT4FF1_<&g|eT6o}~XRZMsVvI8}B1tuXNxgtY+nSy8U&n`yW zovgM)L_}Aozk7cRIGt8Ra@F^A$@;!@$%DG&WzhJdw7p`Y!?9skx4*&P-n}}#4&%Rz z-uhwHuKjTM9KL0V@2t%xcq&HcTUebBgZPyi>QL|Htme*IddYfjxS-HNW?`FP#fIpX z-U`aC2ASt-Uedzmtj*?SD5kpySh*+atbyp!-HI-6`I@I*xrt-Vv|L-LZTPiq{8^` zQTN^j_AX9a3451=toTzPKAb^K1|k)bep&e4X)6K6+F8jHAbBiJvkp}NzC+EIwYV)q z@FWPHR;{8H>hskTy_@axRGoD!5(_};=1O$Az4Rpw#(%GV;~yCE>?JJdX;$`hoi*4T zs1TO1FAvJuOTe%SD>ht~9PEU{r%;9ZykdgSXo5cnI~}fs4h|}T#QN-W19XM)-)C~S zC$m|tVHFO15FjRc~>bRbxv-HSbUO1Fd~HmAL? z_=QA=)S;1BG+3c2zG7lScG0L3gcI>;g(RP?oZc_91xh$no@I3&vXMq((XKVo2!;0U zi|^a*i#7s`fB(GdumGp)s3G76U1$i16A4#d8_L}f+(v-gNuuBq5{QJ4D5E5yi=>0I!cH;k<%NW;`YXZQ2l~Dl4@=oDPZh@Ti%{l2iDhB*3Cw@y#{QkT&0<#OX~kG)BJzZ+vQ#| z8*F@@)h&>@_-qpVaDMganFtYdl)*fQ`g=Ni65&_^;@Z$ReI2nGMCz}z0uj<&=+9V= zf{K=?olY8wYvv6+pQ$}Zt?Y|42~t@fTUmduU{tfdPGvNlp2YlhR+V1PD(^a1t@0U} zD5-KYTjc?MmG_2&A(8yQ?12n-T_!Qf)FP3KKh8B22~rwWAiK;cWI=&IB@%K>ve2ZHkXHrVr~)*MNsU&b zjl7FyP2mMEQvhKCM%49RxK|MFmEv;!`Wpbpt9KCn^axw7mq_gs>~tIK>1+-sN5YVj zC#vWnqL_)MYcOM^%b77cH1A172x&@$=)t;Swqju}qlaZ2FP2dcOwB8LFZ;#__l<~g zQdjg+S9B+}KA7oSX5UWIUl&&IK1OpWpvs1f{e`{@YS}<6o#ezC?68OdeCP96$kLh9 zfF;2bry+3w3!XEVU#tpq;W4UwxQ~rRqq+tSY0Z-OIdTu*R63lXv8Ktgia^$XS%Bo< zzZd`VoVWuNGG4xr&KO$8OAnjruHQ6g+YECt{bN3L18XqK2t}tp<$?S{O+fL#_(MVbp*Sw>B8sr~KAFY^njvqSP=9PIsovlL zBOn`3>!`9R5DyH8Bd~e3bOz!%w22>p3p6>L7-p8V@;U83)l=GZ<-2TQfGFEG!FTL5 zBE>8##k4L3y)|Y4tW62Mxz#~Lhi2c9H8Ea{~tKiJJwdm zjsI82|7Qj9vqU>KKI^6h7Md2ivLpLOnz?z5-I>SqQHe)(M_^7#z%Ei}z)ylz53}Hr zjken+^W6##bgyAI!5iqXx6?9r?XrM$W{J9XSs*_IBqF8hyF%dArtGqeMU>gFx@1s{ zv4s2|MW^*x`Ltd()-utj?d))u*X7wdvL^}iTjV~-|34;30r;`;DN_q%W{zEv1)9gE zNf}MdrekEdD4>ZSS`N@`ZuBR@L2&NVjUtOeY&ERgY^cMs9chY1V(>EfVXLo(wb|!z z5ijfc%;YheZ3%7|I7YKAd0Ei|MTw3dT9)XoP$a#}1rATadJ3J&I5oFX6 zK1%yq*pj)r=)PWl-!4fIYX6hOTLkeI=^;|tjZXzz;F$Y3O_Y8q*e2qn9!WLPk0zYOLE%;Gt5gQ@Hw!Yft_INyYo59@0uC`-P9yzqGw>F0Zkywz< z7=+jlu95X>D0nW1J)|w~P6Pwd?192SG`wgAU$>i{-cQni6jer2UPA<&a!6YCdiu4= z3^aetb-a%ZI?{6V*bOy-X4S#sFVb>wzzNnjb(@19Eo;m5KvKhK3R<~OnF%CD7oD4z z+;A%X|Fa^nWQDD2|~680ZVEw4i|(*)T|hlm{Egsbl5IoM^SWI-=gtviEk1=Dn2MaCH_u2K{{8mOM9i4ODX9l z>7CN2q(`NvrN79h$lK*Qxknz8ua;jYe?b0%{J8up`9AqO@~@5a$r*CBgu#}0rz!PQ z`P6*MU{IVlrCwu7z1Eca3R7yvlzPmRdfb${U`oBtlzP1>^#)Vwji%IBno@5vrQU2x zeU&No7E|i0O{uqc5#%-)KsGlPUGzO{s4-rM|_K z`c_lw+f1o%H>JMAl=@Cn>bp#-?>42rhvI+fPXheg_-wSmbEE~1osc<=+UUSsKu?tW z8UK#My+s)%@k0hbGXsTK&9?< z)Q?PkI2-*{;KGBVw$z2UrFyu2@g@bcH3CGXrUETP8TUp5gV{k3<~|x^`vO87;Xcst z*j%TQ3dL5wPa5Dh)HyhaH?)1TG6^OfxWtw$W(InDG#Q7BIas1!Nv5o;*hhM7c_sU( zm+d2n!%T#e6DvMmXe3c?q^OZlWbQL`3JK?0#pc&zy(`)LGi>v>l5_`wR2uxQ3mU&l zpRiy}fixP*sG+^Vnoh6EHIVug;#9|Qfk6^3@nuN*l|l@)6T(IUa4FWD;eFl1NyxGc zd3RT@U!8ggcP09j$f-{7rqgM`nOzN^H|yH@c>=%}pL{t6--1hpQ_b6HR^8kV!4MKq zcrTzEeNDue*Dm(C>Yln!!v7y2`+o^c{GuIxn0=0IIX*>@lB?QtjNT55-JSH4qW)B1 zLJ7qpqe=oF8nqC2_BhOHibeOxOyU>8RX%2HhC?Gm33N+^4u{Nc@7A!5S=;h8!Di?B zlh41c?s%Lu$R$GcT_=$i!@}wf(h-YIq~rRZ7WHq;4t~5^Twa#LAhG%g71nUHv5c#w ziuOUv^BCjZYSqbJEw!KPgSPa`s(3}`ooewIkFeKYc6004z&h0Ra2Z$rh%PV6kS9!x zy&cTYt>XyGIDi=e`&(4|-#-QEpJs!9gSOEE&*~P)%x5-hb^sc}=Vk2x#8r^MvBGu$ z{Lpgk07gS#52W8L&i+S}+M2BS`Qblqr^DrTYu<{v{6E3w?Ai9Twj$N%FEyclW_Lf7 zeZ*{mt*T0r*l0gF>9*2^zZ3i=+7hvJoMh1>fU3~pwZlU_DuKsLD!3;Oo~J;s;L?ce zTW4bg2)5v9v0rO|n{9b}(uFTTtq`{X`7 z4j3%S0OXbt@UC{Ku3ltEq=46qk~Qlru5BB=H(}HV2<^%1cW&>Qj3U+kFUABh20x1X zh?fES^;DmKd>?TN>g+IH%skivZl93JA(Do)1>jr`!#xPw(Fum>&`_TeOoQtb*naTi zB=lMP4(@F>dzlLTCp!X3;{6&P3oE1bhTF@WZq=w$@jz)6NLWeWhwH4`H-%cxY?owhuq41p zK%Cs>ATnrp4!qnfV6zQRhey-k2p3COz#UcF!lgoilw|}s#?pysI2j^e6EjA56yA`C zk=Pr^SCOzJmBe%y9PNpJ9LZggOb^p|!Q88A=o-M(sNT9~ufMb7vOSI6&4ZWiiGy$6 zTtumVnROD9W`(2GsaU+uQu{LNWtFP(p55`w_Uu+`x0_a#3cl+qmBtP8aeKXBt=w=q#F}J?2##?TG zcUwcNrP|UK17mnJ05et2l{FU@hhGQogGD`O*uJY%6*crD)uxrF{{DYcJwN55r7WMSu&v0Dog8H1gOh8 zJ(|pyIe)~XHEf$e0$v?Ame3sNEj6pr9C|&M-4q(WT|Vq8nEy`{TLk#G@mXpMT<3kc zx~gCB%0{xP2boRepaQFULo%r(lO%{m!$d>u(m&iy9mvz*0zHSHud^Dyq-%Tr>*&0| ztfWJ*nunwHR(~j*gymXAVsup}f556tmhoh4QnBO!8(r2zfoTQ2;Ku^fu|xpwFVn+j zqjVXE^ptv3&qYa#0(Y**=7?m(dA<|wbEJ!ZU?db)rfCKnLT`cDZaJ&{+7Y7k_1~o~ z;!9ZnUD+GGN?iY4>UCBo6Kvl4PgFh$+b}&Ia*+*r_Cf)ZR~zQ;$$)WH3U#7z5ET}d zvZ|pMb`>#=x3G0+EqPTl1iBbAu&hM;bhCBqX4|JJHW`maVLX-6Lgb5t~ zAn&l7nF098C^SgjOi~*9X^L3A#hXn(t&p)TTQsCRl~BDZn{@3WeB2|O%@DkIUYBn_mM zpsE5EXt#?dZCFC$1k!O;M_Xr&%k_8<8xsCMaW`#Q$y+4+zVQ_ce_I))ob`$V1@~Q|gyZsb4Xr zK59z+swwp`Q|i}Dsb4pxe#4adO;hT(OsQWmrQTyoz1Ng_pDFczQ|bez)CWzeUo@qD z$&~t#DfMAf>T|D;A2)4>Z<|uTV@iF(l=`G8^}D9jr%b8eGo^l?oc~Een;^7FG#|S* zW=s6<@rDJO;BH&B?>d^`uA_^Ylx^IzI0(&AkBq@hAh+AZkCW>m%=qvob5-+F`V1ad zL_V_ww4LA+4Hr3(Yvn8>p%^_5pN1$;FssuW8>YkL^wD@-1;u(xNE(CPsk!bxc@DeT zrGEZ+eBouP?V#!UeMA8!eM~3Xxt-WB0zn6srWo<&QLp^aS@Ao-*2!mH^WE7;GX8+u zN-`9V(>pl;oW~ML%S<>GB8MYk;wUj4PQ+-wva)yO(!II0J}2W}7XP0h{y*uha%YL7 zeuI7ev_PizR5G^Dt3FS?N-M%%3bMDS6QV01>dD2@TeS!!%umlO)Yv7sVncnF?ub>C z$-Lxc3st3D?~vmqW}tJba+i>urNF0*inCuWC)vKM-C2#T|mUW8L}xtm@c%m9RkX z%h*=^(to1~Cb8S4zTokkjo?~6xXCFF$LZh!K+kpd8~L{M1fBxE=6(1eI^Lm zy!4lrV$eCvzNWDV8wM7uWP?twqKn&e7I6?)Luih+vQj`mx^5>a!Iy}>1r3S^Bis!GXGHL2>dKKacW;#>r7r$`Wy>aC3v`VEE)fz zI4{7zXW!>~ODma=!B_d_ttQ^KZAE$8`t@{<$ltaN2CV*F$sV_UaEQH5*sIoW+l>cE z{k*J8PBT~~$rK>|FHzBL!a3Jp zLsYbDpryg4Uzt5Ip*w!~+nYe$6#PAm5%mjeoz9w_!oYuDNsx)M9sKvvbK`XD#PO+i zIoxiy8e(3mPnE;z^VX{QBK@&`xDFzsFk3n`yU3mff5IST^iW-pB@EU%YWzly9^L3{ zgugUZ$=n_WW}x);LP-ng-L zg?oeM+#U_5h{K7B+IOYT~y-BnAmGdF(U zpi$-Q`f1P;&L}Yi!kG^kDu(JwXz>5L^t!9=xvY-jf9V`SI!9iP{m*3L*dVR*7RW?H zDmvM6Y?O#H$a*!$vM1_=Lu2tnhVyjxK`yS%Wx!~SB__IHo}z05XUMv24xh_zxWa(h z?&gQW8ty!p&rH$TmE2S^ZX$4eDFTB_nSO&n$pW{NeAt0VIb{a)KeGorLDUT~ceISA zG+XjwjA<@o1LKY{I$bQ|43{y;zHzo##<^k{k8l}#*`jNUWwaH`Xy-D{uthtHWpr{G z+u1j|ie+>c%jnUUSwJ{Z@WF{C*pZ+G|K@2|JEMlf{N{_XjQr+9jQr+9jQr*sWQ*oE zA7bP;A7b=y+Zi!>xr|P>jy?k;jQ`o813I7|4Avp$8Uu4d#vv|a3sd|^L$QpF#WFS( z%h=3ioM7vyCk4pUNyMn<09cGs5Bbky9AJyqBm5U*)C2k#W7Kc9&tt4*i`H}ZEXLTu zZAbLR&SDw6xQv}_9rfHjd38jLdhVXZ7<;)zC)uL)>-F;9h#32eeWUIlmG{OGwrJfQ zYB9zG+@jGN52}nX{^zRl0RUn?#C2%j2Dps)%Cuq`M~Y<}EtXN?GKSf1 z94nSFSS(|R%h=Br9WIt}yjaEwF5?_qbfj3u$zmC!T*m!u(XnC~?#aC)uL;SQ0Vvu_R*TV@brw$C3xxI`Xk3Vl-k&79Ju-K901pMe}iF7sEKp z$B~GUk0Vp;8~HfW$1?J9r482LET$Oz;A=%L?#a5hEW<4zqRSV@brw$C8MVk0lW!A4_`JI`Xk3V&r2<#K^~zh>?#a_p){5 zV@brA#gZmC5;5{|Bx2;_$S7M!K8{3;d>n}whq=o!V&vnufHd>ufH zd>ufH{QZQJZ65x90x|OU6Nr)QCt&=OG@2$;cMDrQe@}rJr@8YdV&v~9CfK6+`w7Iz z-%lV${(fSBEtv-F~Ssmn7^MujQsrsV&v~95F>v-fl>?e_Y;Vbzn@?kVf^daK#ctT z#2ni*{QU%C8 zSQ0Vvu_R*TW62t}Xg-!ijC?GK82MNdG4iowBU8r!A4?)eK9)p`d@LDai{@iV#K^~z zh>?#a``DuSI1(}PaU^2o<4DBF$C0ya9r-vCG4gRFV&vPU5hEW*?qlo7$B~GUk0TKy z7e~VQ&(tAiK9)p`d@LDf1>$2##K^~zh>?#a5hEW<9%k#v$C8MVk0lW!A4|H}qWM@7 zG4in_V&r4VR<>w9mPCwvEQuKTI5NT(&Bu|5k&h!0BOgZ|V2kGCNW{p;k%*CxBW-Na zd>n}w`8X0W@^NGrTeKODgzn9{k0lW!A4@i~Z{%Z1#K^~zh>?#a$JwI!I1(}PaU^2o<48YS zG#^JIMm~;2jC>r482LEz2wO)!j$|2!`8X0W@^K_$L^u-0|8aNr01zgD4yK;Nd@P9= z`B)M$^06dhB1S%rM2vhKd5A5Vk0TKyA4eiaK8|#-Me}haV&vmU z#F)d8F#Z>q(qIzkVF<%~EQuKTSQ0VvvE(#cG#^VMMn0AtW8cWfl8BLyB@rVZOAfI` z^RXmiq-!7R|?zh>?#Y5hEW* zB1S%roMG$8$B~GUk0TKyA4j4nDL#%wjJ3r31jhe$^bXCe%LioB&4y@!?}v&1i7_4Q z45QyLaUcSJthKXG^2Zut<_m5F>xA4YI=U#~Nbfk2S=|A8Uw_Kh~UV z9r0DxzhCDy``!>YL-?t$~{=N-4bmaDJOh1J+XLB=k+3+JllK}Gi;NwWd$j6b0k&hz}vOQylBVqjCsP5YkGapMLMn0BAjC?F< zXY0wwl8BLyB@rVZOCm--mPDQ(d@P9=`B)M$^08!^Z5}?BM2vhai5U4<5;5|zBy#59 z<4DBF$B~GUk0TKyA4k@(?a0TGh>?#Y5hEW*B1S%rL|z|!`#)mj+y4<`zWx7|2Kztq z{NUUF5hLIJj~My(|30R70(|>FV&vQZ5hLIJj~My(f8@}?xBnwXzWpCD^6meKk&h*j zj|Lx0B1S%zM2uW4x#33wD`|j_BM-A0&c~66k&h!?>>K$w5;5j*B&`28F)sluiJ19V z5_x{`u_R*TV@brwxBo|&N(cD%f5gbQNg_tRO%i#2@NJTak#CbkjC`9U^8DaqNyNy< zl8BLyCA-)LH^Y(}ek9sT8sOu|QMMiVI1(}PaiouZBOga1Mv5b0{NHRE{)aq2_*fD# z^06dhM2vhai5U4<5;=76u_R*TV@brw$C8MVk0n`$jsPD^vW(^TCDF7W zmJsBl!N-w^k&h!0BOga1Mm~;2jC>rKVDxdAk0TKyA4eju4?d1WjC>r482LET!wLiA z|5bb}iJ19V5;5|zB=Y>=V@brww@D&KzD+X9O3t@QB1XPV5;5{^l84x$4|0QL5F;N; zB1S%zbg)J9u_R*TV@brA!;(x%FbN>94?d1WjC>r482LDInyJe$A4eiaK8{3QAAB5% z82LC7G4gRFau4C-NW{p;kvXpq82`640&v^#B|&Cek!1fFuM?#a5hEW< zB1S%zJjf~rA4?)eK9)p`d@N~Wi{|6VE{1U?#a5hEWbI}k&h!0qX9?4_?H3jZ*W* zxyp+er;24va2e5UYsq36Q^hj!vE;_JY_Q7maU{CvjgKSO<+W@iV;hYB*PCVkjj^}F z@v$Uga0T};p;4UMGnU5vC>|6O*5;5|zBx2-a$tJdFK9)p`d@LDe z-^j<3h;c;E=5I{G-xwH~(30?bJ$A3l>$w!-u9KlaTxp3YlS(v2((KdAz_}^)(ImSu z8yiwlf};VNWH)AGws{K8#^j!3L;P>B4$Yq%GcwzjXre8{jJz=$Gx~@o*`?3MOlAwi zttLa0aHV+UKvNxBBDp7+i%;tFuggSfD#DkMml!Tl=?Y9L_10#% z%R3gNx0f5fGfA?cR*Aj2T&)s&ak5EGW*En(!4Rzpb7>57&CL8382_U9R{{QQe4a}! zuplq&-BK+G+o~t8ZzY0vxTXwK-FTRch0Zt1D5N+>8$862$GFi^D|RkQfPtJD~@%wNyo1elxVQ%`jeP?Si$Xltw^rn8-?0 z)<&RpT&X4quuTxDvmT5kCM-R$3kn2FY6|qG$<=5IQdh02E7c;CY>PzetZi^7TO3Aw zDT;yK5?iU(pff+kJmw=ruj{g#0|q}$G?q8{noX^XkHry~DHH0%lARuprrDN`*I655 zsgNZcwKN8j3b{~^&cgmcBB-RSbrg6pQ&TFCoal+gBJHE~^bS7$WQB^FTB+_(;b&FV z;__?L9E%cjQ*sU47>PP-cQi6b8iTCRN+7K_%JNzQm6Ke1Ez!reL@K`}Rzf3CDd{!T z4%)~w!AbL5q(g!%d{q#>Duv~mbq;`EcXXjz#kBk1dx*#qXS;q{hmjac_`BO0S}oNU zzmCByZGpI@AsUFxCBsSlo;vFy4`9TK04T*G?Bq4ZR&}P%+7VMhWGTx8ddqAPT8L%K zK4La2n=cW1J+&nQFr^b3{{^%wgwA8zNmsnmw@{<@P3LR-2%+C@1aIRFi}np%XF3U8 zM8O+iWeT^+f{kC4>m1$_i}a2$$7N1YK&lUAx)yd5Asp2g9y^T)F`_U+G`8f#=t;!l zN+LCftJkNDDG3F34%O|=`0+Zc@mp)1$?GVJHtU?lmkiVK4?>92-vs!#@!4pB=TZwC z6EibpFng-E9EWpoLBIj(FoMTno?&fn$iNoF57k+%5cp{FAa~#n12{$d(uSM~p*;$w zim?FfrOEWl9pnz5%k3i^CWE{RAU7B@0WZ@Zw)$$H*HP=V0ejZ|Hz9}<&!w)fh^*JW zu#c$Q{%Y5aJ;HgLHn}?($E*Iv&d$bk5_T!cWM4X>v_!#-HbM^wJCtcK7_scV&14+` zyOu@CG2&MeJ4`;8wQiB3sH%>}M$*KhM-5p+V8{wbsi7-8l7f9iw$RaCmf>_t`->U? zCj)bqVZ{PA^}rm|ZiiA6tl4Fm352PU3uFT8S2C6c(JV7z*q$YnsdSjgk(>ZEP429r ziq4c?Z{4%k-`R25p2qIx!OQlhL)|KFU+ldQTtYOa=2=(O2D5cL)6cfh{l;MDafa__y&{t1Yl_ zXpqbt)z#Z>JdMm8)9h4m7>090GO47RK!t*_#GIY|{!m|bz$Nj61aGQiF-a!{<}H?^ zu$(aCP@4t(A+TO0!IT|HSps16219rf=892jUP-`NfD#QSLkdg|$@ECb5=dGSff>sr z)Gh%>V98yUcp#C2|A0lF@>znh*eLu>juJ)!$hSig`s9BEc>o~hAN*N8l07`20CQpct=nd2rh&n`vg+2zRG)6(Y$zbbH?fx`mTC8(gl z)UDiP+~-^W3~+9q7(QPjd|rTm-xPi+{6pL-UMyZN9uOnqG4V~}N5u!lr^Me$CrIZ? zc4@Elaw#R^40R|%{*srM}OU`hHXD2TZ9SG^KvX zl=@*)>TRafkC;+FYD)c>DfQ!~)Z0y|pD?A~VM_g^DfLcM>ZeSppEjj_#+3S5Q|eu& z)X$kxKW|FC+m!kRQ|djY)O$^-_nA`fH>EycN`26j`bAUfmrSV-nNlA%r9NUx{j$3L z%Yr1!qBd4H{`;J2fy|NPCumFG)kJcXTcWL~r4K)}oTX3q&(zLCx`)X@+@g!VqxePN z?Q%QFdD(pC=8~-7e(zGG-@BsHLk&jYP^c{8lkqP~&j|2u&wJKfaeW45g?&5YXINOnv9z;I+y z*8sPR+RRKg2lV`3qVYc)9zf4--D9)v|fODl=prnCL!*z^}kT>Tr))0043ojIe7m7bqHRO2DeiaX$b=@`Ou(_Wx zJ`Z-lVRL(QERrTJj0x?;d4gd&G_*G`98TdU>#USaPnz>zLAAs5xVgbHl1`>#;OsGH zi6;UhDTpkxfFDUZsVwKDxX^1$J>&%^4=>fwrC54Bs`=e)Nho0Pae5333EQdI$z$O~ z9cq{Ey#8vFF6}I;OQFXS;Ng-DbVGgH2|<0#_a%3-KcrZ|;1?dTq`~5qh|H0YI4E&a znIQp0(A{I;B?BRLNg88kNzNrx%H(qTe~Bfo3)P@Ur)KLtRt**ZKPX5Ks-u77=SB;x zw-z|&J>ID;pdW z@cfNZA7K1mh@<1NF2lpPDK@FP5d-8Se#G%aY#M4aYMBWsQ4&3GnGVMwf{~;ih>eV- z6UpUZfF+bx>iX~Wxt0zMy*4`ZONEABx(zTJ?`Zv}{(qv_FTlTz&&ssGbr<)mn#aQXQa<Of9VD}ez+pV` z@1=7@JP`(X4rwo7s0Jby;^GgRv{)jkem$%JMX83NTMcn6DP01KCKVmb%NfP1TF+9) zeEKB}#AxAihYf5u3%sVqCmle5?2g@nP{l#s8L0mMoH6>X3$| zS^3|zlYRNuy#KAt5Gv1Gq@N3C=J_BrnVyS2YD)d8DfKZ^>eo!EUpJ+G!<70>Q|h-& zsgIjdzimqWjw$sCQ|gnZ)bE;7pE9L>&y@OoLY?*5lC`=&$bOsjJ~)dyDUbR=Le1;9 zX6frDy}1`O=ll7*@(WFPn@p)~@P;?%y`c%H`MN_&JM+kJ$G)6bOH=Kpi%h9IOsP9f zsTZ5J&Ih;3)#8yO#-Oo7!`sb>7n%`YVn)1(;D1RHAqGJDKlpXy^IU6z%cL`*C^#fW!zaMfSjo$WAsU>1Z~MSWh4oDpM1v`~RbYc=WlZ_!XD; zI;l}rz6)+VK=ui>?7qFJD64!+bUK`f(SR8Ie4W+s<&y02fnYG91R>CXnB0@;1mvxX zlB9Yh;BOS-dC67)z<$n71v`VWG4RwOJY=h`%!a{A4+lkrZInbY$KrH%PtxQ?A#4~7 zU9fvt-Z>7j+@WDS#0MNj3t_>+_DlX|B8#h*Of$dsw4-e3I5X7))v7ysf<_OMUt z$@v$5G8tlO+peW@G(AV>o*$CLmefczt>ssQwT|Sg2`k)elN$;$MHGH~eJcoiRIIN5 zZx_V3SA0F(s6bu|EWCW7n_z@()t(#e2`ev(-rFz1fV0EZkI^`HW^MPwLHY&o>KraH}c*XXBqW-H}V*5 zEaM?<(SZEFf}lE97l^ur#X{X+Ef#g#=Qg%k2$D`3G8tK5PZN(t0rZuU(QcM~Wik0B z!|Wqku2i#Kr+G#@L#1DqzV50hEwZ8og+yh-`?v`zmgb%t^KHFcn;3&6R9*cBgAH+ zkkCF9j#KVfl9~}nTH+P62b8&HrSJb;?j`U4J?f(+jdzm&tAzc(__5(w0z58Rr)-k- zVUnb2BIX)$K^4vKqTP>DDA$M61_o9taDh}IrlAfRx?o`+c}oX3wVLW$QB^f;xMoX> z?OHTRNKKOwu8f6qnPa;X!9X;8M0GQP>nh;f8Wg$OqB+u7`eG1vQP)rIGO!$_`AF1O z)O^nkK4Dc=AU z=yoKldNB;Nn*s^+@~V()O*@c(6Kee&8XIzidIwQd@%qRn`WO{OZB#6QmJ)8yj% z)C5zFC9F+YaDtSA%znmcipHW9KW(5IM6_BmGwXU3Uk#3z@T(Mea;f#>D_^#l*g=!IhRw!2)mO~i-pK#d# zv->K$Bs#oao5xPQI5eH*Es8!?!IFzBm=54!PeZ&a$RXFVRz*~W#Ir)X%C#!`D!eLs z-EOzHs630)tY*O-EZQ|Mka&!oEx>JvNy`rE^xUkZ0%1s3o;x;y?O~3MRj;+RH{5Fw z6UP4?FiWSmBiW}lm0rR9p~I2wg_T|%9krF+AG*NgVpDA{i%njEFk^Cyf-xJ6V)Xoq;?BYo(W?mQN6=aaQ;#os6Um6O+cpoWGL(8P*r8MS=CD{;a8`X zT5@Po&*d1vK+!9^zf^bhg*O4+{X`RC{D0Ej)<7K_#vn_sHsQ6AIFo3AxNbqCWiK}+ zD3YEwuO~~Go64!n=-#dht`j_EnCYCg)BwBkTTEwdWi_jK(<hPKBULa?D66PlX*zYG(at+EqZ3DB*TZRX~}s7IhA=Z_c=D2tu|UZ8dF%^L8b{Y(fn_eQS9dOS3ZV z-;RXrD91FW2F8|Q8msV;cPY~t)9x_-KOO8Ys86bjtt(Y(Wn>I?l|_}Z)^Kw)wsh|a zr&kRZsnjV_392;CHgjK2jdVYWwisOaMKAVJ)hNkKRcl|hY^;bq+mXz@(p2x2-G1LJ|*vV!`(tW4Y{QIy`dOD|_-?f>KSfN(Gq)v~6HP zr7AQ}!T7%`fqgtH;IEpeQu|q150^!y=zS3@zZ-PfD|_T!f=apBW`;X1i&CjR_0sOO z4z2RED!__6To$b=co4btjiJk4275b<|Icx5E!RBGa?arO)g@@wB-_+Vk($*Mh(qr1 zNH{gOmZ4RiVzsf-j+R5Q95ghx9Me_aws2Lqgl@OP>nqAZ$IiBLut=RU23YnhiD}3m zzpj)Ei9NQ0$_3;9^LB^LN%n3oN104oE{B@Nv{EK5pVv|AEXvTHW);j35mXRi85OL_ z5aqmP!-MkXFC=(5!h>9nGT9tJZ!3BbUA%hLudPTS#1zl~H<`uETvqaGFiYj7JQ(P;ngVEd?O7@ACm*kuvEDe!xkfs)6 zY!qwXXy3<*7}<_wo?O8NR9`%u<=RX-%jqEjM211QrFQCkA+x3O&> zFIm0ThY2iC#gN=u(vg+4tGb*8-6oc?vvihWXIY+IWr|fV$S|YXD~g=G3K_%Jf*~wV zv5>@5D?4|=_`gR5|LXl|W!POhE4#ZaVeay=$_1%;N>r|O;XjzCVMuH(#nOsSU9NIj z?_HJMT9%+)4O7gKkTJSQw|Ox@Yc)(^F#hj_92Ph}G!L>D-U;~Gfs@{jWGzQDk;_x2 zSX)>2D!)ouD~dOVO|cqPo13q6{~LcI5KWTYcWNpVV+6;#Gg{QL;?8YH@*i2*rBPiL z%jJvX@>X_P%o;6V{NG1|o(cyGwXscaFHO^$m9=hi%~LHTu&perRjxI$!ag{;4Eu{_ zU^>X^72KVxA+BO(urk`kmTD8rQzRszBdbVx3+#%bEnh)fgwZAjP(@|l(pQleqRBt)-$zIUVzjW&tC~9S`_QhrcYX}%OkfDa0 zKG6wCb+gweB_q_=iEDnQaqmZgP zF%k-=;EJj=$Ytr;6sv0;%hNTu*lPsBAlI^eB2Udw3+yk8 zniVvXEn`3FT0Q&3veax6)C|V|LkC=1GOSS7l2pxjlC%!|tMjytG=!lMI?JJL?lL*= zyIqw=Xm0%Uiki~+lSnJGYk*8Zo zB-W~=t8f?2jk&sJQ=i)kjcb~O<`aQ#n7uk;FEl0WvkF87#rl+>D;LF_1WnzqIWLw>EPbpO@e;^T5 z*yOCbg=_7rRG1YmwjD{U--in0|7$CF#h<5KIqE$2b)r_YY|9&NC*`S?(Gs!tH;k}Z z%u}vDR%ZP=)D=~%a?Mw(n48Yb##Q`gG0wJfI(O7ljAn7H@-Y6tzRGvG7d}YLQ!XTS zstjj79@p}Ws9cQiS;fqB%Tq1~t6YZU6g=gk^mVurB1y}Un!@8Mli?H;b%iZkF#f;M z5lC{9H7!r42HKcy)v!JCc5`9P?JW93VWN_2T0-!s?i< z6dhZGHi|qgLh{vSwJ6U!XQ{4TF#f+89t$g@H2O!&14wSOF6bS-s&)!bg*_*VvQZpl z715B%!mo^kqA|E&ly&Y}{aEkP-@QlGtuY-lB11oM=so0Zd5 zjuq0E9lYvDGJCDyDBTQ>GHE_cd%Y8!mb0s8g4cnu#SQ)-u}4 z(xGq)jj1k8FJs#+kJTr`)$Sa9>YrOTCyaS|g@n~EdSB56 z&sV1G0HCldyjv`7r&xkc#TlIn8&aEabZQM^_dMNUg$ik@OI8fKo1rIfZNWIRJlqcB z|J(IDXa>M4tIfKLrfaCGvO2Alwo^SDT*f--t892$QP_Q))jq?`?rnj%#b}CXhz25a z$#BwA&5+lH*}j#PvT=JeOVF--=B?FGQDx4pi#OXx*;a1Nq4@&JwQAEF zjQ{T>>vNHRg^lxhDuje8*#=Ktq0%0vgyDONA`q zXwJDJ5FNFshvMwUU<6DX32p+3JdHt>(UX;|@*&iUPIC4#-Pi54m)#5tfogDI4Nw%ic*4lt-uV1>Y?W*S(nNMce#~Z%wh% z^=(Ju_LoDg%9XOo>#D3Tt8w0f@&8m+Va?jdw*7$;G^?C5*GkS>dFq8^9w>`?m1|k7 zaDt{%W-S-1TZc-}Etu9;dNC}kN+D4XmPMrsI_;QQ@nQUbFK7LWPd0IVS-q7`vgEYa zdb2~O@Pu`MRjR`!D3x)ITvytKWF9Pwc9qGi)$6XXV@}bU)x~Poa0!~VY8L(Ct@&it zgYp0UTo6fs?2S6t|+skbM2Zx)nUg81W5BD)_p! z4EI)cFH$zw);wiGVwNK@g?B}6CWZ0;pC)&IfTmVdp-ONaYiBjekeq_AQC+doHJ#`r zPoI#GTB?ebJjM3e3c6J+WAR(X>qs@NHJ)IXpi2Q(mnL)gp07*mWy_eSQ&d|szB&$}jV$^C4?icek2+0*)jlH6;ut(vl?iE3`E31LU z>=o0j@)`1+kU0&;|BuY~i!0;+mZvnRK6)mSb?H8#x=#3PWy1BK>=jior_Jv5xU<3) zHc8a5YG%lL!e2U9!uKmrzfeW=WF+fZzsluc=(EB2|FH&|D&t8FF~_c=d~^C(4Kw6F zDPF@$cmK-MFH{XZAIZAbuQFYv;H$82VJWU(W2}13=0KVu1c`bY=3+qBLkxGEm1}xC zBPfjjr+dRQ<-q-}GI>S&TouOs#l~?r*cn@wA^!ctLb7UE zPgd02RjwROpsN+d>?N4H23eJI6tm*oLlFPX&wk~%;7s1uFb-63+Y+qEQV=b$Eo??`5 zh3s5;3WMt6*5;hGwNHfc|Fcm$Ug;x-jDRRHn6L1?k4rx$@>STsu!KOzB&%k=V$^IU zY+ZRehAQG+n>tpaewE1w2NbuA>u51+XpwP_O{{heEk(PEwsXPw|9Qhy5C8f?T{Hnbu?IE>npstvTo5OOm7F+ zw(rkPZ^dib8l3X-R0_%2w;EHPt9I>7d7$fSc*=v3N;C4Qt{CLHt6a2C!2wJg*u;Tj zehZ9$N&LGY{$2VfnF}^P&z%;?T$>r)QY{Fc>Mh4lAYva#>>tVng9RqCeig0ZNQ5RZ z=>)gw$PB|Y7{m|NS*>-Z4_c0b`kHC(a3HDp=i*Ael_pu~1b=ix&P90Fh>}dwQOVVD z$Wi5{$IT`>UNxrGp9*2CuhzpA<}>pdMQZ}dqzNLXk}T2$k(F+OmTAB$i#Gr@7MIun zbo@&X3DQIIYo9xszai?9)F%#?9E8k}uu&@YFCAstr|sX70C zh9%tq33jH#sX6>;omKnBT4(Ya?zG08teg&yhi0rX8LRU*zI?$#B-vIy^qP~2B(@f(`5Wi*@$=(jeeVsz=L-_sv=8s0cDLgDGqBpg>L3DPj_qxIIg1XG_7oj5r* z6kZ>>9B#LhE-2iT2u^U$^{Cz>#zl?hQa+060AX&VcGfy72KkkWT2(ayVY;v{RTA}k z{QC!uqV3Hh?`dG>$W%2yuK~ibL~JtKM4fh~iPSt3@yQOuk&bSxree}P{(M7&v(@IT z?YiooJ*UI}FNbcPV7l2+<2TeV+LP-x+|8J?^lt7NXfc#EF=5)xjZA%dm)y;C+2nS! z3$7UVsb&z9Zf5Jyx9o0a->1s7GTp2?%^ADdLAv>5@%w^B_>Ca^MwG-Ci8W%gcvy^! z*NJZyKQ4Yr{DX9o^e>WA+9zElO-rwmJ}rG!`ib;c`BeE*xmOO#^YZKE56EATAD4fX z`=9V3;Y0GbkPSCni=;d=33ni1A*F+!@c7J@{cLiHawl9x_vUtL1(7-`wlCu z?$)+L2kqXz9>=C`LY?vT#>`gPSgq-X)wl9c!D_slBzaO&1bsdek1uQK$EB+C~aX z4I3fuXp?x1Qb+AQxhgm&Wu3%a3ZFEg+JrkzsCMB~CRB&;X-b_l$`;%&olTly zy2IEugNFy@3!#I14h~J~LX0)aW8?+X2g8H4Srr+d2@cDzpj5VlC&l-fP^04eO{g*P z1C%<_->_fP(!Tb?py0oPI*d=Z=ym8j+$*~&Z`>%vctELU{xed3!A|MnwPj>jG4Qd*X4J4H&*egbkOc}`03B(phe?z!k ze|k7Mk_Zng{`Mwoo%$NEd(7V`ov4>~%D(VH!)s$LoYGoZwpRRDowZF#HB1M>5rSW| z0-?0Q8=rT=$<=46vTb*>LvRi-W-NgvOS4Qz@I!UhmRUeP;E-+!#33CPeF;DdDDBt{ z@y#b!pHEAB_2?X0SaORuh8=kE#!s3o=1`jNhQublv=Bl+A%1BX5#lG!OS|aCTKVMa zZE8!71zEszpeZ&Pk40g}j77u92tNom-S#FuHH!KgkgfJcpQM*IixB8Aw=~2!g8nAC zgY?om!N37o8Ld!S&5bppUfM7MLgU=h5aR^;dz_Zm1g^c-I{h6`n(bs!7S0#`Re*mV z7VZt&Oc1P4kA!EYZB+x}2@iq&` z3GZm*KEpEU?v~#zHVbEtb`CrA*}dOCA-|l=!lQoUBzmw*qAyTl1CB@*ne@62Y?=^1 z0WTQuAIrU9xKF-{@+Mlmxus-uK>nCYHNG}5BmB;UIxGC%ggPhuL28H6rn?X5tGeIc zv*~ooJJZmgD{X2_zMTreE+xG$G9mW8*o4@BHhHPyZtO_7G;t$i(hVwguFI>hy%SUN zt6+_6R6I3@r?hptZ(u~Wk(EJwER&@jUO-qsaE+7 zl*($JQ}~Pt)g^q^gz6UVGNF2e&rvG7()Wu0OQ|!)^(p$;;FN%K=#J@CIh6`XP4&|ve-mP4wnQr)&2V#R# zovw|R2QA|mv%3yYD)l6g782xjI*gPG^@tA2XHkI4AnBwQ*=KbDf>A>mRo z%8UHm^n&M~Tzx?`nHXNb-Ph7&Xt1ehLoyi~3Db!HgIX8M8pe;Rn9uM6I@AqeNz}wv ztM)AWA7}urS&2ogCmf~cO4N^JC>|pYAoW&9?JkGsQoDPX_fl#zN`_)H1JUqQTIozj zQsHD=`DT46$V=&}o;il7`r#Fr@IXYr&oH2uJ zxN%xo)&PEVbsERD3+`AdN8^C5yyQmd9h1lBA-*BncKs1@R$%CGF z#Z6nQ^p3drK&r>E#fygvbi_m#!#a!~U7e1wId?hBq^5mDQ4{otVH5mzrTiOtP>xDp zl6T9S32oW4I6A5Eg%R4huU1V2AglVW-B2k zS(qO_oe-y6_8a$ydtXS1v8jl`4%}J$0z&L%cZ|`~2(fXvV_$B2*)(%1A;#HlS=T9q zI5!)q)%R+Bq39Mu+|TYuJkKY@-kF25O_~+Df3N>}gxK0XIIKV3-*Wh5LL5nrCvwE* z_9{a3CX@SekGK0yB1E63xj(m|3J#x0h`rOUq-M+X_U)fIfe<^#XS_LLM|?9O4vdaB zX~glFv}+R~rUx1)^^I4ryICg0u|~TsSNZ|Fbh=z!nNj zC;9g<5Bx%YTK<9jr2H-UG5HbsLD=2jCEqE3T>h~9KKWhpTje)`weS}CMwlV4k>}); z9FxaoMZQYDLOuYe0v&Rj+$h(}UfChn$UEij@-~=B&X7-&pC_Lni_+hvKTCg*o{@eD zCkg*4eOG#1`kM4*>5I}m(r2YRq>o7N2Rp-Cq<@uem0l?wm*%AzX-b-q6lq8rl=`Jk zsZDB-E|px;ZfU1<0azx^l3pM^PueX0Q~ay=NAVf)7vhh_?}^_Qza~B+J|NyLeppR{{QP5P0DewqI)Ue^Oot$x@TClh zbmEsY`+)O;Ogqr8&FlrrLzy=Cd1s~-e#SGG0q1uzZus`*j0;NiXPmGUd@SPto!F9r zQ{l}@22#Xqx<6A3Jf~)M1Lf9C4g5Zq0o-@u;mj`hc|+zB;AziX49`B4*$F(a%M3nHP}-Cvz5jYss7mKd;D~0pI>EvlZwc$ea$pKa+VO z(C0HRfS*S*&j1ODHO+SSmkbgM} zv=@xPpC25C_x&&cf3{x*U;G1lL+&S?aT4@}9E7gebOQ9mrv0QNu7rM&Cj9W{ulnH6 zrXFCq6?$dUEzljCZX^BhLumRF?r8$PCmMid&r9Kp-SzO>+W~xUuJyr>mw15oThbvr zpg%UB58bi38hS%|H*|({2lR#XQqmRQfIc|kbZF|6S}p`yE3qGb#R5N`{ulW1mUH1p z@@)9Ag|v4+Y3Mhe3I&Buf@&Py474iwWN18f{7WJE4*AV;7o6(-T>h$fv+!QwRyivE zP}(aT5gwDiC;vveQv7fE2|*F3MUVJp>38Ct!e8W%NDk2om|;-t5Z@{MM0%_6S-Dx< zBHu5cE!{3vk=N68z@MIQ>l0+3yL%UzqPilEmYiwGHhBplHY*(wo#^i!y_gbf#tg*1 zz)nI;_9hKxu>O5J2(hQT$w&-bM2HD{n}O(WcnKj6u~xK!3kfmpnKg1wZ70NF>tQ3& za{(b9vdS~o*dDAke;*k zAK4qotwQ52Y5+WvI*>f1+xlwvQd6O;Z`inAInE;Q9Eo&}K`fs;S8i{tg z{`aJP#zywi^?xWZXXuW>X1e}6C*4M3A6@@v_8vC&<|JMJgAu=x=%wp_+GPt*!pN2` zB|`T*#t!FHxP7b{zMvt za~!%|l!{ViutruL-N!x(x&1qoX(hruuRc~)HBE{eg3y(4G+1w?m9DBXzs{+u3ToJ_ zjD@3NVx7&IGV1diPL1|pAIN9hKE^h9dixMdn9jN|@$AY_RWl=&!v>e?`D$ymq<8SR z>8&(bE-e|9scSJ5LCiJv*;G{Us*Zd=D0i*PR_k+Uu}<^zs%fd}=wY@i#_Fu(>@1?p zf_-Q`^hdDWcru`9S-!OXfLQ@xk|3O1wQ|NamuDB;=4x}vz9!S!>>ES0MF5V7mg$MZerz$C-Cjj36(p}ME_RyH%NhKAaX73g?I;=kbXdut5+kp+mM4FHi4u(?oa9n3U@+UfL+3a>z+w8TT>@QAN_LrP7b4XEZ)C%V*>O^#TGyo=FD>My6=vpsTpSXO4 zUNcHxT$Qzq=KWW-kwP3v_L*e(2-uEo`k$$}h*D1*Mh!I?3B<{SG0?917t)ts4sGUi zX^+g+JAK;ks<2=^%)UL_6#C!E@Fun-flR9B#0I zA%?wb6B{|8b=4ZDRQ)nhkb~YngS9cC=58E{fCHvcIFJ|`AX^~B4MvmkK!W(Q=BP7j z@wOVBZz)Y53MBheiP(gq4gym9d7>o2M0hkSpsJ#TK!X?5&bRrpN|XPK!2(XK(MEM{ zAEkeh!KFtH(fA)(+$Mbb?mbA z?``O58S)SIv{{WQh5ysk^hMyT9GvGQ=`9G&lFP z^!HoHeL1@}?`-HBwCrygB;5*>*6zNR_O>oU*^%qHous{6pp2%Leyv0E`Pu%0*BPH;zRZ7C zmw}coOOt=7wY{T-K4m0`f6gBr#_ugVdK&zDsfHQ`MpJi}A9^R-3;q5E z|3H6s0MX%5xL3#plK8mg2xhj0gfw)4ka3d)l0pheMpa_$}U21 zwbW=BmhpbNXz0_r`J73Cn9f7zOPl(hDt#5K(wD(1NPwVD!BzT-rOoh5TST+#uv?9V zXBbTa4(K7LsOox^kCz5V z_IY#1;}H+t0F(+YM^p{;#|%Zy)dLbBOJi(WNx))A0tA%+EX^az!Eh>++s(AY(`xNz z>+LG6TT|=1h7Hw(rfA4@01(LzIJ)kGC9~Wgvj11@fAZaY`+vD>vGM*DXn_U0c*4ky zC!cz1T)hJ@8|M~Qk`TbFBzJdH9Nz>nylo0Z98uqlZ8w80n0}ra=)CnUwWr6Uu|)t*^S` z|MiU3Us`|fi8oAc`e*oi7hKi)UuVvI<+`JLe{);y32%9iYks`-u|FkmsejSUiKpk^ zxA%(LSA65;H@x<~uibaXSN68FoVfk3o8KUR^j#xQ{_^WDY(H(!TMpj-spI?qB)y>J z)2F=iTldcI_~VXWw@UB4;-Om>IxqN9OY(`QUwZ5hsZFPR?E|TEr?=dH%dehy+m63Y zrtf>~`=`91`&X%NZF@!V0_#-E6Fa~2{3{%xAAj}E6SlnWbFV*If9lpNPgGv}Ki7Ra z+UtMQ`))b+3mmdWejRQNop|0nrQgeUe8`~OLz!T$eY z`El{x;>8g2bG9%nHHv-0O~O;6B#nzlq`%7-$={H#l|Lx=OAkq}BN}s&+%Jx*w@daU z6cVP^4EdsVJJ|zU+gTjGcJ>T6b|!~!AA77jdj^Nk#h&zbZ{_gqXAgg4r*rrM?0K;F zg&e+y5Od1vcmap6ojpn(IE}*>VNaI>r*inb?1A&XQ#gDz?9p(`77kw%dsbcZd=B4U z_V{|_c^tlW_Cz~!GKX)3J>;IMa>|q9bUdqBNn$8U?#AcaHPi9r?olw;gd@qC*~!RB zW)I<`Zv4=R7sH_yL;@$nU`vjs=;m0RDZ$-1ngWw*E8Lu_%)}BCCtM<5A#$eGtaU0o zr^XVS3-BK38R+crClp1Ui<6sn$_IJxRaqcaQ7i#A5g32{g*p2a!YS3raA+wxIIW$h z)ztnko-K%Hi$7Esf)aloKYL-4I+6+(UH2kl;m59!v|UYIcoPOs;Us>n&dPjka0R}; zeIZKD6wVSZd=+)i#Wwkdrdk_~gfRxkU?yJZjFzIJ<+@>Au$$=71+L+W3zp|KP0jpf zChM&4ud}f4ufZ=C*S!uKGWm|IEkT3HEN--f`YSOg-tZm-+s$Q|07p8sdLmVf>aFYCk}3k&dAFM&02Hd#1_;2JMF4Ef z7G^Y$=_=3qkIHUc?)u-whP9Ql{-@YS+ntfsGbeM=h{Lem&&>m=MGX z_+{f$&K7vXi(jyL^8J0M;=?VR0sx;=8P4dhoILo{m(TpiHP^Nr_{)d9zkTGkn}WUm z@i#s-@WpSv{crzq$-Xyy|D2Pq`{nJwc;X#%?UqbWxavjm(S`az@VAdV_WDQOe#48s zfB(s6w;k^ON9f$<iETf;APS2V8glpd*$~|zH7_he)8v^|KfjlUOIX0{~i1O zCA&ZQk<1f+zT@!kZ++JXZhhuG4;+70=Z}B%wLi5y{yz`A=YeP{_&_%b z@Z?>y(vXS*}eyM8wDz}r9nw}aMyzU|v}b?5&1^Y7aI>R*cKD|+ww z?UuWq>HTl-C;#i4H+g=udFCg7ylU55zx|5Yd(ZxI{X3t$exkWPbjt@%yz+NfU3com ze|qr#oBEUAKK_Jd@5w{Id()0x7r*SusnLnRmUn;hM`v#j-?xA3ANub5@@=mY-s^h! zn@>Of&bOQyeC5+$@Sm&v+rF26{`$)gKls7R!%c zk;lIK{;&M}uU~Bc#jkEl|KQ`laL@lOKV$D&%-G zrvZ1|{;`L@+PhcJi7nW11E2p`qrI2pZN4oKJoig z|FC86qx#+P8UUclxpRT{Sg(cRlj;6Zif4fuD-EykYl$LTsWiLh-+JtpNWL z$g8ow@7|&>w33HvOz}A z0wd#1>4i8|hHBvrW2AR*j~kRBh)yCgtGRNR9D?ECFPz##-TX~9q*I|-0xl)UW=5uD z^MEB#HmA$(q8k?Od7jtqsrBaP^PvG1M{R6o63Ev_HZlbhM&osZNf>P^6UrnEWAXwr z{v}BGDf~maR*eiRCeUku4J@)50=?GN{vXsY#i$=nzf(Z{Fzb@l52yH9iu(B+KA(%` zl;^4+MAZ2l>g6`Z<78F+P#uEHRX_B;F?Em5<|_xqdogrCn&|+X9I8MEVAdsf08T)) z|QrasG5JHdy3tAUK-KdwkySuknclTc1 z-H*zDW;U|BB$T`B_kDN&JLv7PZ^w3I=gpJf!?3|^;Fp`}2}i>Cfh~9oOx6-(4@Dn% zh6|QT=jG?4kS55pf+cat>M9CYT``jalbv#DC~&5hg}^?X;n&M3f()W!m|x&IupPL3 za)B5@nlnDM&JI9?g!;Jr(8kc6&T@1^I}{s|qv@OCKClZJt^LdN4UfljVY|4Hd)J@n z8{qgUhyJrvYcQ>2^>;0l1jexXDv^`vq)96HD&gK)4)0Ivg7Q0t83s*)iy6(&%7=Vl zOa^l5Qg(2|oCrBod=l|44OUHyq(QO&ar@kR*XTQNy@xAPrQM)ADRRX%?4S#MK%g5f zOdAQkHs!!SDxb_@$az;I;nx^Pk}HGay36EU1WaUW#8JRXLopnVW^^qRD-Cr9Y_nKNZ7$hK)DF~lwu30wJJZbN8&L0Syr=$BKEvf^CQ~(p#{aM7Q>2|+hG!j> z3cUL?fljl~tV904Eb{!{7z40Fx*t3VZcRx0i0*8wda4Ok1Se`41g|{%Z zZENea-iirR=mp+9OS7FT;tQydPPYu@WA*B$?ST{54s)oe`08~Jd8YW4F)stQ@8a|VB4Awd!Q(hn9V~~&`BwmK$uOSa2i2!6FE?VI6 zs;q2f8hDcg5%8PQ%xn$vOF)9=SYj3KogS?ZGQi8WB1hXwED!V7(>cLfx-d9bL7}Hw zX{P{qVVOd#^aCv=yDm$Wo}LNfe!yVla%lpax=n*gpc@MhMFQm`2!@c%PPz^w9~3v& zZuAAI5~R?hHb@1`#RRz+;0ibau74BOCyez8cf+Swi2R{r4gUS(|3MFUHm}+^!oEVZ z3n~JI2I_)JVtwq{qH5z#op7*x0f-L*wGc$ofj||5*S8|8D>P zZvPOO=HKlf@~A&x`v-Uc#96fdSw}JO@86%lcMtrF|MAbm|FGSCB53}&`x^rfuo}#H zyRv)8lzv@0+277+eKfcG_M^AE9KIchpU7$3V6wU011xbd=j+U}!}t2LuI)ZwY6Vb0 z;#F|{6Ra?d;DN}W&?4yHpFh_haNuFJPp3EGfp4vIc<&8QWQxS?}=%xC4l@kqqMf%*XeD7#(GV1>r=5gE$`} z0Rqqg0%;BZ)H3;JWo|}v#sL~vGT>e{LOO`_hGaLal@&c)Rnf!M6+K*2(ZjVBJzQ7O z!}S$C+)&ZOjTJrIRMErD6+PTi(Zj73J={ils1f@?cx7@_wpa9UM@0{JR`hUJMGtpZ z^l(o_5BFB|a9>3a_rrxqK|~j80ts@V{VepsLO>$M^-G#mq(l`Yqnbr%<-I6(Fr^YV zD-fy>)d1bDw7Mz1O39n<0T(8)0s>G~8iKFO`a9sRhtSO&$oIYJ#j9Tf>G18-yDrvm%yDo0C4;n&CDB!7l*s zf>b_108!bEhu~aE@611-zYs8ZuE;=v@=!>rT!v7IfNoA|@mS>XU zDN?CNPV;P~TVVn{VtDs*ifYhE%R;!=5OOeSS;a%vyqbl_=u*9II?$8t#66GLb1M`Jqpyh^8`l9(d_0S~8c>c#(rWjrXEg}{Iqk*K24k&b3eFj_jat|rS2PiS}a{VYOR zo*390cmiMmW@3Sr_KC&{0L@K9d(!Z1iyF3#Z9!wPlFT~HI9RBiOoIkKJy=#CGn2py z=(W*;p;iuM_@MxF5`73^C?3#82t-Lb8uSm8NN1Fo3@YT6+5PFaWs?&ePINcxDe!=w3twPVbUz zRi?&-RAUvuK=5`%+jpKDfT)8Efc9Jt=HIK~%Er3xPpZR=IA*Z>ed%Gz9GRKOvH!R0 zK~IDDESj!&VWU4ZT@QMGKV1JTY#j#w{Y`%UOZcA&a-?=9O-~qZ1x#yYwEgOLWtztr zG)?})ydcxZt&KN5EOmlWyh*0FoPg=C)P_Nbt9(%mhE&evP?IZP6w`>W4TIoa`J!S1 z#9sQ~18LGW3__xgZ#Kjyfv96VCkL)GdEzx}Km-oOJiY_p(byq_n ztgfs<_;7qSz7ao$-^M=@wTU*YhC~>VNcnwCRQSvIp1=TvN-qx|6sTC@0B@O?u_nm| zsY!;Y#4v-@5v-M1G_zcMGH`KhRE|2E^%7fMj=BPr(1^yB02TuuVyHI!FrtPzQ*den z!|=NX)9x`y-C?K%LTE7UY(jutE7#r~;4R9(+a80|!$cT1jadTGnCMf!1foCA0n~Eu z21lpYLE(I_hJXi&k{ggU5+{wP^Xonjt0~r<)~u;wfyXZi2LQKB?hU>4N_Mcq^^VV z|3(-HG}0H_fIVZiWp!qWS$$aZS;ts!@n*O;F2?(VxZLlER$z<^iFw3H5FPk3@e*X? z6Im>OaY!%-VFs|$>44+(XZlDK_OzJhl}i;KoF1E?j0c>l#%Q*=(R|D>>hyd~jpk!I zebDpOH7D#@@xYf#l&^rc$Jr1YKC2qgiLm0+(_ugj+5^dmW-Ri-J}WS$SN2u?vx_MYY#J;@f+Mg>=*Y9pD|N%tG->!!J=dLYwUn^X#OI5B3@gMei|DOs1jOfM1LV11|u;A z$TuJn{lh9~Ei;}#cq4+%0#Ybs+ntpK-aRWAL^o*r6TxR?A=gWQCSni%fH4IUfR(c| zT>tg31sM2u40{8j0EDuJu(q(CZLBV4JV7F3*)&p z4JTtp53cct-oj8OrnaB?>HqVqq;mb$!C)qO3=bD(O;(07G36S} zOcn++i6V0J!|4>l%+$?LCQo`Omt1BhA48eE6T^b^apSpR%uEi3GWpVjsKhWcaSde( zqDOq;Gc(y3%oLK+T_5!zQNqmB*ia_gCJ44SW~LT~GKCgoq#@QB&4_{&W~PpYG70HX zb`;D^en1A`>3%UHW7U|*UvFO=Wl>5jvfeVWMvTv`ny$j3N7t|HSKrwx<;#(rmz$53 zUENT!OK>D(-Fq%4bHU0pJ1<(eH;XqukbV=ij8m99?JSE?#iC@8#Fy zeV2~q?(M@fU%&EG^M>YTer@arZt3&*-K;xj>}8(YlCk2G2hSCb>}W36+#jV@=xu8+P+Hq~4yea9K`&>SA`h#_wwtaYdrcR=7wtCyw z%rhf1Om~DHdR#mvVCRxE$9;wFYd#cHpLzY?d~ob^t7NW2XBj{G=ocpR_zoklcCHY~k%bE!S?9Hac;@{pbf3dE0 zwISW`x;aDVj<&3}aLDdED@Jm{JlmAc$FH|=7N;a9R0Lk1rBEpq>aS}pxK zn-&j!(f^Fw-Q2#T&UptvU8>Bj9dNW!on{Sh@_x09AAhvEyn0FJb)sQ@?b}~=p~B|SuPujGPZWDB z9RHI0)vCDhzK6;qPsV<=urC_9x620Qf|Q@f%<1lP=FmIn%f9n3ebp_4u#YlKM%eEn>NL$w&Fs>}Jfq zdja8pv#Aqo zU`2gO_a)Zz3R(mS<_@kC_ZSlRGX#2!nw6w9B~zXz$;?NggtV0~!wBgrq5C%H&|f&? zclb6lvS$7ReVc*Z127Nad>8*8r6MI?-?4kp7iB;|2C4(-UA*%6zlrow%&NR^GdLi+ zS)oa!YZya^3xJ6wGGGlRCsDUVK&c{CWTTknp|ktLq>0)zdm$l)_c>!w}J2aopCV|mm!lL zmjPI%f{%$M3JoFxuB58Jg^^G$eQ@#^gY%yml;sWcx5%8$`TdbJw^kFIZuMEqN zFsnnKvGOa1F)X2ASd>}Ww&cM4T?WO6J}3@)&3Xl;-nRtQG6NAUhnjsz21mjB3u{W9|B;j`H!{#{efeEws!qi zWRMSL3?@8j5XQK?D9Q;`8UANg^9BRP zQB?DP!v43M%?Fom^C8onIkozKaGb6{|F1dTl+*uT0QSEC_y@1?ciC!w-2RVT8@_vt zAya+l(^i>fg~H2_v3jZD5wh^C-49Ni$N*RlraVcFlGPG_kXK6rG14E} zjhtstsvjbkF&vjEfxd7+T*iO&`TuteipIcS>#FgGgfd2gWibW?2xa~~|C2s+mD^~3 z*wB1;qXD8B%K0C|+zOedpg+(79^mvT>;*1;E}gKT7dXVZ2a$#U3j3 zHdd|$hMQ2~3vx-9CrwUB1~Tx6R1u#q6K7H^N)_FiDai?_Pj3mGU=3;Ovz5q+*Ugm?6hVc$ zjG@aH1*Z!2cC1-J=m2lTImd{&iRa+RadJSyGGh}JgbQIiyO2y9b)J&$=8QG~X85~s z&SJXxt;T;DxCBdc-K-epk?%wiH$gaOO&5vM9Vwlj5|6sbumF~PO{DjxObqv?y?40_ zz%Y~XU3A0D2%@1MwsOPFNco=`<{Wx0-T&|~R~R;l=fL5*kiN6DFFrZUj5YvA=s!2i zjPl5LGCa)PB7CUBobtV4CXo@S!`vPYa|QB$WL=>b7#ln5LrabCDai~^KV){nFRy~} z@TOS{<^O}_|7Z-ptAim&>PHAFfgKEGK>ncopE7X_ea!!m{2#Hf6|PidGh`J1Ba}X; z3M#QunNA_(_N@6JGnjJHV2rX-|AhRXaZHmi9W@gS$^S{~0VC@S;eQ}`9A*8x$w>Ym zZ}tzA|HF^g_r)?O)el-$yz*k1Sh{&x?mu1rk9HD`t(!5ni?j@^l&y?@>oTb8gc?Q4 zw~oa1H!EapK6(HD<^RopkNh8w(f8&5C{<+^j=m_PWAuHyOn{vD&zArH6$eH2ulyfk z=)s|>+(z?5^d z?HA(Hc+G~x@@!sYpM1rsl8}&2H(SECtU@zsSZCarR5QGeW3C*|&IZuiYfeMMcIEM> zi|UO^{4I2NwZ=JSPMup#818q#yV%m_N`d)S(6P9W{+P6&6_FL!MX1sy7P3LSq z9#_id^j5F*w|8!}ZWRW=TX;YVjTlStl zH#zf&TYHnS^CspjvzS=2XMyGA2Ny5$>V6qFWx4YvyLmfa+&tm5^g-6i+G)?cpWI`~ zn)I9RvSQu0D{h6ej&)m6nzC6H(#U((wRbmpbCnUjI;c~l7QQQqYS&{<6IEj0>hT*a zIy`z2==XH=mjt&&al=i%(>FE19=u ziMQRRFLeBgGo{tqm7ku44jYm&BJjeN=WMTUHXAKMp2=r!YC10Q;mY~kdinK|TO<$q zX<+}+E^?ST;>APuH18#q^n)t1->wE7halwO%{F`sPTFYtU-aSI=0pZP_unHwuZmDuw-}Pp zw5{xhf2(EOpx$Se<-hk(HmcF9xb*D!Y5C^2dIl{oXq)Kn{Hnp!`gLX7?C-}fZ7|?# z{+5PK(%*k-@M-F)k8h{7``FF-kj2FGHiF{!zig`YHS^>1;IcM;%bRQpxcvs=u%E||4C<8S@m4>A@KIQX*HJ2A9$pbx?$AtsU+OX8 z*T;l#)3;mT#fLm%r}VEs%;(tASyQ)`#=bIb_PEPc;ne(Xhc6Yx&cLRhFMc!LY3{Y% z-i<#M&(EH=chV9I#o7HX?3RIMx1TG@9xNQOF5hKe^Oqa#q6S@cExCLBpm__Y^ZOrHYumSW=FWt;2BNvgSAEzuG-g9ctrG`E%rm!8 z#r}5s!850K!}svl&AB$L?$SN!y>_M_J>jzF!L#fMq1<`LN8g+Bu3oOBZC3TD=@YrtY{boKov}O5qn+j*l<(RVuk!zwepREowdR zN|)Dh^x&U*b);;$=l01>665D=A9-uip&NHLN>+-tpS`#+^~Ph@5SMq0EjBnDsdG%%-KLJ)X0>^FXlm-SW6G4x zdt!!P+!?%Qf|qhwmrsX}HRqnWn$hu-dcpKMcl=&_Xuz$%vPs=E7n3rMW6ji2$%R$s z9|025yNpS##TT=DbN+*n=Jw)h}>;hfFWcI5Z$@#`CYlLcPQ_gHpyXgIU&@%^$Z_e`&;GkjkSS^1^j zgRrvv)3cu4>~MU;y6rRXm%O#SVV1nf-}7ke^${Z*c?IUo<2Aut`b_<8)PsUwoE$s@ zGGsX>xB1ual&LQ@PqR5w?`c`m#$Bb=9=84F;?Q(vRo6`yr4+On_D=KAiZxL>X3*ZA#>{KHn( zvUWGsHD+`KecjP%+}^w?rN>KW-Won*c@wj78Koab+fMI)>wT+X zYX|abt*|(>^_=S6`xQY;JOjTh?c4mtlhx0jwi(2$f)CoMJaW|e@b(qUruxqGdb*1J zZtqj)rdzh(zBaCPX6q#mJ5Jo$)aE<}{r@{a|Nm{+DeNBh8>=p>9kzhw$`Z0<00(3o zYZ1T%Kf`*!`T|hFJK*lP2$usykP>_;z6(ExKgPci4T+9~J3w`j6a9&a#8P4xah`aB zu8)Z)SVO`V{A1ZUJ5t=ZIU!XMF<6!z@5}S`i;m(rheyjT5m6XT@pE>Oc$gCqfW<+= zbCK%OMrC3j{&canxScpIU0-Oh4|We$WtTy#N`k#d4e%CH()xyBM_>ck9@N%4J2=9m z>`c2DovU`ZC=we1lX5s|Z2j9taOxnUr(LYhyAT z8ta_f041gOb{2LJB@GJY=sVUc(;gA&NPyhND1R2Us+4GK0!+&D@bu9)AUeGdNzBgD zbyj$ga}`9)N{I2&_Mj-lF%lB_!tiKac2THv5UhzGL9I!ifXzTjC6Ot*q~USd1t=-4 zS445xC`fg7l!xoORhXBH{Q?Vc<|VQ94d||rBceDgQa^>n87)wL+9VE&#qJ|&iWg-L zr8{67VF4~Y+PJ3Vf&IUJe8<|OY|K7PSa9V~FTsqU@lUX6d(sZokG^-h%VS*eYmwk| z*GDxD`QC}Cwbd%&=tRc>r``|ndetuYn`PpwFF&8|H}I)+_LJmKw~MabGg)`&_!za% zYM$a*6WMv5>*krKi|0JtJ>uxRZ(%z}*V}t1Xu{nI+p|Y~bJ)_%`RKgoF0r4tOvvD? z+HK6KaxJW@*=SYPc$cCUv0vTZ+_@RHv?so;$NOfX-4K%8}x7KSRHea^p z(PeCX=O!zbwA^@XQ_${L(i5iKnyuYl``sDim-5T0qgP&h{q=4=Ymc;oW#g?bw7WX2 zTEHjk#aSVCH-74$5F)VH_dMlL?G1}F`$m<;jO!WnVs@$H3DX5LxRky8W;=hz2>zXk3j@xbztilOA->0o z3U_>+U=md_&B~m2k0{(9dwArkjssJVn9lJ!B94hP*V99^BWj*VA?jYY6MScrqlLHl_`pVeif<>?1lkPH?Z%>*VZrf>vtP`_;u~ z2HWlzUcNeERN=ET(=IH2g`KtTeesBOWTQ_t=WVY4vwywZvdGvojYg_2_gnjQK?|Q! zS@E>2z5Tz)=8crw9ktsr#A4ckUOj@viKe!uOWpI5?tZzS7t!Nq>8$)(wcVUsOq%f} zC#u_3LL_iiHo1EGYpQp{IMcxRYSWww@Z90GYM*M;$bZtu=CvEh&s-4v%pcX#PW2A& z(YpB~=}4Y^A?NATpd%x`=4N|bYqdz0va{ZTSn|;1SiMsAO1&~`#+iS?`Y8I3fBbe%{51>C3CV@=ixvj-NWO7o z@DvGvVR(4+D#HI{6=SSo*5OL2|Nlvel+ezhHHMAlK|2Sra~x&=j!dLPWo15s8t+*> zhuV=)#@wAvV16(9$2*gTemq1~t2U^tqb*k46DF<`97)^4b4f%hZPZ*Qgda0CsdA_1 zvHY%eMK@*`**H2ON5H2Oas^EMIzeo9oQG~V6*`NM){J4H!DLU6q*!mEp~a|1WP=tO ze4c}g3vl*9rf-H-1w3aCkL_e!bjHqQxt*}3ymMq&7uN- zi5_<-nTOh9Nd3p>u^l)ZE*GiU_3+;Z75v_ZX7@{g{C_*_IELN9KC$Ys+OPzyFjgYs z{1>veu}-t@v&!(gcstw`JB|x+8QupUk1xV^;Aij$_-CRX(T;Ehk%44HUt$7?4YZ3m z2QWcPbfpNvh1NqwTz`1&CX%n_la@l@_P z#TgrqlE!CHMmvZjMoATMl=DlT7q$c?Rf_WTM}lC59U^8>ndc}+1tQYNHHQs}zCLjZ zeWSWXh9Y99i>JQKAeASq-Zz;#1A?-#JCN!}J3d7Sv4OAvKNlh8w99rjMM>kRCoNGf zHWpI->D?NFy+KrZWo+ywqGnT%px$}dDOgzmeH921I%+@yEh!f}0}BX{)253r_LMyT zFTt=~*m>*;_Knq$WzFgWFhJ$3eyjOlE%M1J)!sheh zBSLg%qHs<;_A8|RsbWF@vFSsY+XEKL_Msi0^2j4RTj=GXUoLV7Z`l8g(|)uR5mheT z^e43_z8NHP=u0rNB_h%nOSTFToq{M8CHs59HgQt@C|{fGSZolaa$Tq+?c+i&Hf|7Y z1+dZglAFe(9%{t`Sd}B~^~G!nBGM-?7e_?8P#|55h|&HO@Z(^A^jUIDi_O(B0pXtE z*gljrCxokCYoY*K*nCI&hgFF!gj6T`Csc(^f(1C`M&;?oU*z8%h=#zV@Sq*vU9dwi zDNi047NI@A3waK8A(3%@cd>{0`Seey6Sf&8rJw2(vk@^`NI8Cor`JS8`k_7{5f#b^ z4nZzI#z{Yq-0z$NWAk<063$j2ahEe)){=~^gT*@2HmqPzQrhDj5k~n2hx@dH&0?JN zQ_+{5vElKt6^Q@GW?fH>FdvP8@Uj;L3imrT;}$7t^^5=6DBp z4SM(n-J@#vI&{!? zXVk5*c4k&)+oQUlvX6bQjGpqb`}?>+Uihd@BZmV%tkrtEr#*dIu};;Tm%sSQtx+M# z!g+T0kMo>AHkgt8R*mgCwYsp!jfZp8&pc{df5;9<>iog6urQ(~KYPcpV`FV5g$#Y< zGIfLc4L=OvuC^R-(Ua*g#NE%vqf;w6pu_l&5y+ke5r4T1>=roUgd z^z)^|i>i2UZXo$N{zaocIrGc9e_Yk~py^r@k@b-M#Ia~A)3!B+%Nl$g=2GqT&kn^Z z|Ev{-4@(xWdGRHBT5+ap7k-0*wLD%=J-ce}!q&}eo2riF&zmsu=FSrhUpY3MGPnJz z$D3^|XI2dj|9VYyYtj}k?xP2f95;k@-ap4KWsTd4Dr4^5KgcWG;OZ{-do@~E`)%DA z;_Tu9{jZD95c@ja%$l-uZkc^SoA50M#94a=_;sGQB=d2?>-;{SQX=jSlbY?mGHFyO zko~oUYpuH3?+rf#4>MtMg;*EACN^CuS7c|oS=q8}9NC?DE;bIG(FrJ9NlOP13&cuM zk|bT~1u%+1EGaRx_c9pnRS8gv6(ErYkr|a13oAz1-CUXW@&Xx$X80b7jzpkBUu_<0 zmHBSA$Qvim&6drk#iL2pg$nbayDvlwn1=3xu;56F5q6pkRhcA}%aTAKTNh{eZn^S= z>?Aif58cE`;!~($a@iaXX?MnP;A{SHbkh9c|438MwWQ@SC;^R5aIf5*)V zbWN&KX4RR@(KTL~s>~!4m}x@pmis>$gBO&m$&)2703nFjy4&phR4Ez1jI26dg7CH@ z0!Z%yvUbpET#hyih%V!-{hNGQj)V4I(-pe=EO`P9c%-QtMLPr4iM0eUg`}AvUQrb4 zGg}VZk?-it<>|}UCh#Y_n3_V@S=r?LGB80Ho)K|qv|>}Wp;$Ft3*H!XfToGrnG!`- zx+D{Nv*@Y0ni00srTZgYpCriw(D}*~P0%FR-+jrhmu1QmG_S};?vjw;r5|0ki)PxC z|AP`^83uGhqo$eA0c0dRxM+1ie0@p&e-VZ~!p;Kj-<;J6ME;0iCA0doN?1!+yIAL0 zPjC#|fj7cC;U0J-o`Mg+7U2`|W%zFVJktM}6P*YTB9cfU1^}G!<-{K10?_;Y*dNIJ zFe5%;Hz3s~Lh7Oo`{x{8?6Wx?1bnAVeiTeWXp4vpxnLtm^!B4^)r9B-=^aDU;N@Wh zVN1Ow{*+S@*8w|)sPy*Bv0boOAG*3R-XA9Qq4SzCq`J|Eeg^0AP*S>PQi=^jr6zGG zx~fPXQkMDB&ou=(Fr_d3WS!HVBzi^aPE}%2WOG=PZv>a3Y!szqYfw^2K2_Q1RSgm8 zgA$VpiHw7}s23upa;c)F)K;)2zw8)C{b9wg06Pt-jB|M?whd9~C-4GnCM-69uGwW* zM?`)e<*mf2O%Rc;fK2NS3k~oL&eR)5?^{W#Pu%;|;jOESxnU%vD15W&VSXWqY@S1pApzV*s z)A3>WTznfi>pvr^5-ka55C0W8G3Nr*n@9I9uOGoiov3Y7RDa5H1?ns zcmO)95v5_8_t3s5<2{IL#vWWZ_TYxG2RDs9xMl3YZDSAa7<+Kn*n@k<9^5zf;DNCR zkBmKdYV5%?V-H>!d+^fOgIC5Ld@%Olqp=5HjXn5g;cQz1KijHQ)3T6NE&j< zF=nEPnUVEj(i@%s@xvJS*BvDKaX&+<4K;)6FAv*}Md47R#Z*}#)`S`%*$c3@60rU= znS3{0jm0cSU`2m!B`{vHB~*bK(vt9;*<29(jZ5M|=~YqS6LpJJ;+apghHt@SLTE}n zWXGh5N6JiSARZ<7@{y3wT5$0KP+g{`x+%f``ReFQQi#s4%#ha}Lxsl-w~3Bt`hKIS z@X!GaMW3M#xAvu(qb(>mz)V@5!H|H%I}J6_blrr=zgE}%hq{Hyw;1dep(46`hYH1x zDL{^jVO8=W|IcQ{VXQb-0c$*KDQgpJKhXN!U_D`d#H---@s_v^?t=RQyT9Ie8eV{_ zf$iTMd?nxr4gvoEHvR%=0nLa;L~Fuv_h$+MZVlB`E90T#f?h|jM zCcef#Pd)}8fR-8J0!)$&rdCLa|MZ9dkJa0lU?Vn2^G!xEa}^21TO;(}jL^$!5>4?C z5YQI{bs|crHxsEhlc+b7sW(%oH&dxM)2KJosW&sIH>K2@nbezE)SKDVn>o~*xzw9^ z)SLO#n+4RHh18ox)SJcBnEy%wbYw+)SLCx zn+?>Pjntb>)SJ!Jn=RCvt<;-s)SK}# zBnQAP%X8$}`5=gNHj3%yFO?)nGsVKVXnitSI3pX|!HHH(I{6&#l*O$2)oq)?+ORP- zyT=DQ>!ZJ>@_d ztd5DW!pvy^iUZVEtj?~Zt1XoYLQz3&bO?@UW=?~(vECMAb#>!bJ=D`*p&$Ol2qWXh zNEXSsj}Ta8g4qOO$e>=-hKd327B##fN@-ojgu28N{8~i2!%XDB@I!fX3Nu4l~F zFqJ--NES(CNa0}`u`(R=&QQ4iacmEU?ZL-^KmUF|gM){%YJpEcQ`-hbjUeC0v{?%S zGIy|NGL5cMC2`D@0xLkY5@<3QJuC3}PEO7M*^zlxpz*w3N%GVLQljk2tXfd6Vy0Q0 zmX-T{PLT_nYP{}H`JBvXy*lpSust1@?d-tjxoF_psfb5fWG=&&dZ4rTpu3P5+rYU( zZ;`g=2G;w~WyzqFp*T67=0niAk=Zwz;bHcnDqEh)Oe(N~)bEcdd^ZljJ||&pjIir} zZbZu&N>;MFNfg~mcei$wkjwFRyL-~(wwk0S}UDC%uJ^cGvjDAVrDws zm6jX%W9l@GMvjX#a+Oy-8qtL5R6tby5UKNHJ8IQP8a){3J)PPOD8h7Vv;T}D?0=^< zOauIrR*l*XFf|^ja+v}K0s^`}Fm`FuYzdOl=rpYi%^ZnY_ItNd5H|(RSg|q;>Y~Yw zFW-&BL8w8>9BC#{qDei{)1?xi)Qgu!$+MI68)X2G302a0+|t58Z$ z7A5NSYjNEeS=fww2Oz2q@Cs0*t3Y%t(+yxfD689oAFwRObPZvLP~GYhDb*ou5VI~u zdKu$B5x@(c1GRwa0JvrbsIE)dh+a!9> zyh@#_0f5W8(m($$eh`V&N_PmN85|m64RnlrIj;ZT#SgZVN-_Ng z5o~|V;{O9ozawb{`Zs3!Edjr)IU^(6%}Uw-k@CMO7&fJ{_}`WOto*5j!&O82Krv8j zTdX7ziuIVRG==My`N$~RigXS$sbMbLfOG^PJcC$>(oETzCkc-2lWiqYWUE!FT;snWqr6tD0$mYZ>JwqjzHX zsREuJ;(zyr1E7O>XL9R$zMg3o%}i;4kN*KM@66M}9~m5;3)_WXffF$p93J0=!!`!< z&S--%xod9JcBk2O;f&+EvDB)gt=0?&v(a%4j4UMD=q8sZf8yHC^ z3F1*Oe1y*RjimFS+rDz^8A%sF_g#yqYb0H2K=(NPVbw=c$4ENo>}0CZXkcw4>Fgpr zDR4?WpIT(P-VFYIS4fRdO~|cJ^0xR>uNcZ~bbD3b25iG}HwuQ>Bn;BSoNw#bl75fTmn9 zZKOO)9w$!)`bRQ{!P7L@60>N`G{t+zmp;v71j;}k>b%cJ-$htuXeKZckS0{5^91cR z=H$ag3X4297xe$~lIL>PXG~^75F+MNJ;X_`5se1bKu?fQsI4V?h}wJlo^}xBdS|-% zLcT&HD_}dImTHs~j>t8MDFW50NpxBgosxti2>lDX4e1??^4(mtx)6=PR(GV*bcF8y zzv?tpZeC{Oc~R%3Gn$uwty6+_Y8jq%RI1)sAPY^pz6p^3XAvziq9toG{1*KC^ZR=M z-}(D}o0Zkaz8eX4$dElwv+Y8If?!Lh%ab%w!+i!hv z<5l*BrPn`v?d5LYyW>`qtvg%aa%wwTTI^=$HQS@f&-nF{hb_;+Kbzyn_pFd#{^{JY z#)C$69X#Sx`%}*zE%ZJ2pd()E%)+FF-%Tv2M)J^K$u$rSob(1C*zbv0-K5Nz0jE65`mOVSw{nPdV zr6uE<%wN)xKek<9gG;}p&7XSaTuH$E9662+$axu+HPNxw+!12CkluD~^ADeKEbcg; zC&#%zg~YT8!3MN=D*URpu<-Y6(kOj{=bL^0OMHRj`WatCF6hP`*B4AyiA#Roc3{aI zFS(?s--4j6&8vom2Zpl_&+*tQUCipZ;4G`8Zer4WuW7BSo@&*`+p~Aqv6Eas?zky< z^11)}n6k?$&op;K?*aMto!nTqt=hMry2GjC)H|FZlLo(;8)iSi zn{VFDyKY&wo!4Zr-tmEG{bL{ukf3t9KMW%zq$*79IasRG1J?G+2oKBm<#|L%9O7ey zTsw^h&qd4&){|wyiXd7tJ~JD6RQv(zKL^gASO4)TKCZTUr&e;m0&(m#GJWmF?W86A zS2rwX4+|o@#ZbCDDUIB){&&UzI@fTXgAE5*KyY2q|IpF468u zoP*&UaQg2B?0<*jv+<4iF<{yMk*H0yAp}Gikx29*#uJN)oe_(uwEik>ME;Ell*oO$<`27^GG;NJS3L<;tyYFl}vv)cOXg4GdD77^F5eNNr}2 ziky_owHE>MmZMr4ECD$mmrHA9Fl}pt)HVjGZ4FY}8Kky{%%>HrBL;l_Q&R_u?0U4=_$xu(}YogeUOyNhJmXkN;)F9#%)_^G|do zA`l9I76lyQf0Ous0ip!xe*8Cfiih3D1YN{#L8``^N`qpa%g3%mY9QTHJPRvD{?|E4 zR0OMZca)R{ffeG5&4WpU=&;y1wGojHS105kBF+CjnXiPzV9#_4R&`hiLSPH_reWA6 zU`tWb5Gn>=a&#VSRB(td)vhK68-AW_6kjL~Bq z1SBC8MiI>#E+QZnrSz0hKpf*$;qaYAA$)%=+C_3M+XdT?lG0y40IfB#KNT)9H?|*4 z8R6nd!BEUe><@RQABq3NX7eBbkQ4v!0|3NBmLip4MVWdo0oP7Uf&Wp2Ay!n&chTT~ zT=?)f^8ZGT2h903haZ6cqp^`R>-v`x056dswAoMM7cDRd=sN>tXvBWH=%+T z1Uh~s%s12I^huCsOX3jx73^3tDy|uFj&M~aNo0EeOGN>it7hdQ!y8Vfj|`6vv}D2A z%5tNe^#5&?<$tqLX84}+jf}G4WcpD?_6ype%E7>Z1y~5cc@F(@$&~|bP`JpzgsT5< z2K4`k|F>ibF%a(uqJEDD_&+;XXIT#c;%`H|Bi;oUsKLGEGz<{rRCmpA+$NM!W ztPwRCSP9VZzaiVv5MDx}Xy~(1(TK~VBbo+a*J09bbo5rXIU>@*o6Fw%Y z;XvZY33F)x(%g86bsg@V9<68UdD*b5!|9l8vDhO-?H2E(SKmi?Vh>>b09A#0B*{Xn zEu4PRRQ81VMRZIy1y))eZ;my9Mo>0YRQ{>?CZbS**3u{!4IuW2rc$TA1!iO73JDI3$fI0==Xn;Buv>~uuxzhmE zu^e?e(FYq+j#>(+igMJMfErznItNh8zr|c4%s|VQfwblOVYxx-3c#$FZyEH+DPO{B zVy6M>29UOV%QhM;0e$Ps&F7(2Kw?)M*#BZC4T0_7Oc2*^K7ImV`)tHl1Bu^ufctj^ z{}sQFKgD0;9|79G2~izb2R0&_0dK($gdO1sGz47I-oxIQV00#8$lqQTSAMZlxVWKB^F zD;29>WCyM_i|oO*YLNpg6I`8HV*x!3Tx%8yz_n_TD{D8#D{=!@haz`ywJz!cu5F6C zf@_N+4{)_8@&woVMPA@qv&b7*@C!3a%DKGH|V5BnQ`;MJeD~wMc=-fopF(8(hbl_XXmves z4OiC(lsD>z;M!Yl4n}Q(x)HceQ8xxP?ok7KID)PI3Dmny-2_}Ws+)pqmbw{8KS|vj zq}i%&0sbDR2FTg?KD8zIyF}dzTxY7SK+5rI5H8AOw7Lx_$4T85{B5Cb2cAw>w+Gj$ z>JEUiUfmJoovF44ltt=JfHFyK1FlQe;DcMmN)5iSCd1YC;O_})2XLLNW`k>pngi%t z)m(6Gpmqe;VQMEppQz@6>wGmI)VN6P4Cp0l7w~tK8YuNmx~V&ZzwOkn;O{YNH;}Ta z+8tai)m^}Knz}2g{|93FCm8$)l>Z@a!MIe7ZJQ2U5d-b;J?Wrv!3Y3P$^#IlPMScT z!qi0IKk22+lrqwSmXv4#gL^u`7Du&nt$mo9pC9fPD9FQs?KM=jc0u7-ASxo+k`clOyK6=<(e@7jlg&`S_ zlM~O;!NrNqM=o}TMLb*x_sw_amp8W8N_Z#`9ECm(!f5@JOBV~QWYTPp9EluYxTH$K z1kAD$Agnd$EUS_DP=r^c)xQy0j9I2MTa}66?PCGBj$9#w?qR8^09^nSW(7sLW)$%e zty|3ui-4Q~khz1iBS#|wGSUMakciFaIcuuOmZqhrLNOSC?Dmu^5+EX#TVVvZn<5CB z+IY$3!NS~VDD@BgvpA4bngGjSybSmzw1w**C)!{@zYo4yf0-Xv6|_>Z#)B>4 zN{z}&Ptnh{z(BO%(S`X$V1>xkZ-ODG2%fVez8|tPsP4q|NF8zW4Zk*h~<# zZ&(g&CUZ&9fk~OTmdwOJn8~Sh>1d0y7|YcPjx|3gCy9!xdkE00q!g`@LpU z)?OeG2#68EhDKk!KN||9B1ZydP(}e$_=d*ci20Kl2m``KI6%ON%Ngl2Dxn8tE@T0l zE&w70xmrCS&7Pl&JOP<{Kwz{_s?!Yqq20&-g7*Bj9KK!=Xsd%g_)94Q%T16d=OAim zHYxdm_$zt+_f7b(1ss9v-@(C!&voH&NI*NoiY-G-oue)z;)o1wQ>0n~ekJtCC2p?e zuu2pj(bQN6$~O549IBmypXMV@^Uh79+cL;2+R6<#pUq~2S0~Y)3~o%?H)NB08u^CQ zU4+qwo_u3It zb0sI9c3!z=)4GI6h!>VVml!V?6Hr`G9S+OTZTDqE~E0%bhEOedJ({K5y-_kugKim89x21Vf z*&3hL$?VOmuif0ezo0&A_wqfytOxDnkyvO@eo*Vxs<&pYoS?vMXHNFB>ajdc@VV2W z>g`@W@akqCS~@ZBEw{zxI0~n zlg>vLx3Dy4e_NdM^u_3f9cl~P9%DOBe17w&q^fCr>7bpa*2ytdF8_4D%Y@7`^)?1L zjBYi3h1=3Q9`fPg_0R8ZasT|6NSS@a8HZjQ=6KZ`JN>DYKXBeapWyhc_anZ_;6sM54^m)G-dp>B{N$G;xpFPnUCjr zY~M6GxUDdK#G0Iu$@MzDl+8W#Hf`p#4e@t2ElT4*JGQ*lnfEL2$Co*;U-p*MEa%Ycgko^d$x_Hg$rVZ=cIGlX)>AscgV#`?*`@Q@*;oi5a&4<_7biQQD z+n82;-&|zhIvLcoro4SGhj+rb+%C=dCxXL2KM^cEdi$7{O>~QfcQtQ1DdpvL>9$jK z=9X-!rTF#Btw*;_Z&i(rIkI}`yUBrpHJTp3^nRi9q@(Fy3adXMo;{T3uKqR8dQ|hh z(nHgFy*O}heOkAXFeiD`wa<8NeE4 zq+miFYE7h^(I%6d9uh}-Hk!r$MKDT9UYN~827u60Qzw)X0Z>c{kTYqVu2V$0URfb= zA`Nn-URI?ARR!k&m_I-Pz|$qE9@#)J6{pIUMoLmunj)Czic0zklU?lQ>cr1eYNag6 zxrreH#zaUZxv>ACE79*pV}fxPHI|Cn^BBWTOXtRgI{ZgRBb3ANR*8~$si!1WvsC_4 zUP1sU6!(A7C~-(>_SX*UaqPr4Nm?AR_S=`zWnb=H3m$ar>5=wx zlZSMYN4HpW#CuTd@JAgQulr@U_buO6+>Z{QhCTYSYs>umpGEuQl0T`(eOl$}B%kL} zaQxY&(f(fQ&7)e}zxeuEv_(@zFV7oauzJon<{hj$3BNSwQ%P{pN|V0*Ct1Gr+VOL| z&8{2521Vxm`j&cjEIGfV%5E>W`oAPweyiueHs9Qr^XkKw;n}?tTAs1?9kB9~^-!lZ zI~sRCBY5@ohSc=PM5m1s@y6H32S_un7*=({&)X9mm4kyGHhr+@cxXq?%J@d5ZRWq|=OU>2 zYtqWvosUFDTw~eRSC}eBvnJbiNt7j=$Q-KNuZ;N6eMmodn{n%HZbk<-ssDOBzlr_8 z7B6bvxU$H7V|DAAX_q^%dSx}gUc#$dRr+QQ+wg9t>AHoFg9OeG#+e+cR{x`7L(hHd zy+-exxb;NZ;mE4}@2`_g9R27?Siy|49V?dJidj)LcE{TU=Yt_XH=VZj{+%((!p8F& ze@y9o{gh?PDF;p7zFP6Q!2y#^;?6aywrrm$Z4vg19XHvj^wEpHFCML|KVouwf9u=L zH?C>$W|vpraLam@lZ9U%?0m9ycWkGtt2THZ&uw~p$@aY;HVsSj_+{eu;N>p$Z(bPT zBD{aKqt6&~i&N~^nbktl-mZ948a|Ob#IE0xDOZO)p8mX*AAjRWtKKyi6$aPmbKSdb zPkL=p_tU3d`{OK6%4AlH_7-0Ie5Kl^s7+Nv@6C{&ol$+YbxCm2Tp#bxt=*S5ow@l` z>VAt_3B7N`++SIu@cp{cW*mEk-JSbQ%R-yY+!Xb^iS315GYZN^)NE&YLlE&{#p}NH z7G_Uhzx*`UtGD$RtXG@$^Ws#c>Q48|s`466sI$uF_>L22yB~QYuIu_dWDp3%aXk6@Cx?kG<;vXd?Ogn?zA8U`4TuAQnK9 zkOG2A@4aJz5CVi2NJ1AZpx7&ly?5*lE2x|zcE#R1_O7T1e6zDDyGu~?ygR=8A6Fog z*_qj?Z{GX8->dfzrN3dX$xMf4h8>`BWR5hNEeYe>$#4K(K_1W+M^?a+GI^3ddtj0B zJOpAPz%lbtnsz&33~JX8^G*zv@PX7T5KAUy=rosC+ekBjk&1H?ck+kza)}&Jrld>( zq|g3lI>67A05}`!NO^-h7CEyESOm5|~XtHfwVl0THiW;b0grK*0F~sjZ0P8)l zZ3x6D`H*8=UHgh^5`7kQkpj&4bm-_VvylMVQ97B`Q!x%wP4ZCwb>siPH!!GEj7Tiw z(}H7*nY!@|TdGITgee<7Hg-#vo45cTM;EpKQz#Pyth5ldO zRSbwJQtJOpVlRf2#NGk=e*`(<-1d{=�ivAX+e@`>MjDBB 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

|>Y{`W7}HE%CX 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

Z;8-RsDT$pqUQWW5A5zL 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

K>t}E>p!YCbaQSmT{_+}`)8x4WTNigY|;x&Lv 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¬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�A$>wB&b*RVr|H-3`08H(=>7EY z;opV4_xYCXu?eXkeBG<)@&vH?mfc2MxSYULGE|4)9O~O zT79}S?X6`)ZoatjUBj36uhf6u);eI)nC*klcf(=+b5*&+A3Rd zy8nZPm0Mn2Gx}8fwet6_)>xaQKiw(n>zoU-qW{<%^Okk>wB6gvCr`4DefYXzL%+}7 zK6IkPj7~RS^AB{iXfb=?bl(~_*wduj@c_YZ7$?7{Qz?{0o--*a1j8GEYv zU%BBE$0v0<%(|X+my$LIoZP!^wRADZ@7A?_Ga@PW4`-i;(|Kg7sfuY^y^)rDPOg2lQn(Dg1WD=9y%FyzVFzD zwXx3^t$b&LvApWrUu&NH(`V@AAM*ctcWSGZ3!jQmUGhJ3a@=59WyFzP%jcO-Sl3*f z`P-9<0X?lJ4u5@ZW%GyngjX`K*X%=AJd3_~39= zzrtou{B}c8{-=1rpl2N6r?0EW?koFtUrJu-c+{FULkh<2|0AZXtNNP-T|a#^H@ta_ zN~x5T{P@CaV}@PXmC&xMHhSqx3v1TSYTDZT%Oi_k?tG|upyPo}b%8qbFFrZpa~=(J zJonDRuO2}8-dk97VA)F}*5v$j?T1~@UOu&A@3ycv50#xtzmcEv^{)MgcjbKcVKkIG z*Ewtc2t&*3Rq2wzRg)sN?mD$-?9-u}XC*Iaz9O~n)w73I$G#F$b6|M$F*nW+IluY7 zL7Aa`_jT9)TT^S19Qpp7 zkec^Z*@sS-*Yv%q-+1Ln&CZsE>l`OjtX*%k|Kd|gy?2WnP1oiXZ;`NNcE?=-Pp*qHVD>$U3#i%j3zjt2HY=f7CoO9X%J{Rs8Iv%1=W z4ByGi0i>Ra=B_7i0D=0nc=o(ak?YA~TB#8;m-RR2b!j10{gXlLlhRU zg{4h7R+E7)5M!j&U_}FynQk0#;|gZtt2g+NgrmhDp}DWxvZQ?uTA7@)ovuI>LJ`96CrFwF1)n??xOj{ zi0mhphj7RSz_bsrxdIrUEQxW`^mlNHCF;S;R7h^aW<*MOqaeHhzoRMT`NSo}^WyQn zkh~<&zENVvzc5xZO-O@uYfWOaF5 zEu~8-qK6W-M6^Sj+lIN6B=7ia16=j1aFK<{5~u)hE}98W1>n}+x>4KO_Qv;ruP(^{ zugl+E()Y^UxgHQJsLk=wrxLJcfWJ&Lg*V3-OG%l7ax5a}CRWLDzIX3YBs6!ZT|^Op z$z$Asiir&fTu)$h2KB&IApa3Ron;l*>yO&g2M07=i1)M?hBcxI(=C#hpcx z$?(^`A7>9tHr9qP>hW2bB&JqSJ+3Uyu5jWN!cxdWta_Y`Hh|z_|52|W6-N1huCDjw z!rEZA!*h#xB6^TUu)_;v${0?E>&ZfFhdaYi{P&C!R6Mj)et0Bh09^gR==3{Y{~~a7 zj)VvR?}=xM3Y|*;{YEJDhdLt%Bg|wer9YiIxNZQoGyBgOiuNoLdnm9olit|0iP`BM zOV;5W%sb;AOZr87lEQDxJ=SjLI>Z-N`Ude6WQ^d~uzPFv% z@V)ZO8*!T<*#UUB8Ns>1xy^8$b}b&`z>qc^?J!<97g~IaoC&ZEM%O=37t9l%qWJGt z-MF>H5ueBkSdkD`H^%(&Wz6t<@&7`w%Y<82U|>|Gp~O`Hr(@&uQ8;AwBaK2Wy{cAtsEi8U?WFk-U(?keLe!A-D9H(z@VIZksCo9oPnTTYJ0@1S| z_@Bd(%G4N^-UwTO3o00Jq)p1E^s61fj$st3s23c=sFYl83^^q^hGZphZb87fd*cS% zfD_KGQeivK-yF9V`2H`({(t}4;=lLcdp&SFJwU4)MXIN=(A_haFE6VER0&$+fJ8lygi9;iTaf4xs#2(9Iu)})c|L*Atu`F>#k z8-mY&krqfmZgmJIhSiQ`4C7PV5|`X^8dyc%vWRSz#Amp;34z&2&%?$=08s?t$OpH@ z{>{4lqX;0Z`ynOiJNW=;Q6Dgz^u6F8rT-@TSW8T01(vGs1_FRURX+v9QBhI2AA zXaO#R$p6RQd%!iZbpOK?K~bwk5lpB9{N}x$pCNxHkgB%^cHaW zVt;|7fX4&aZ%}DufC#`Ch=5jB1>U{UREJ)^9$qk9oG27dsVH#pwfBb-XG72keo0YL z+*E4=1q~chp~Q#4$p-S#@fX;$LB?(94gS!MDnFGX41xfV70|E>KM3p&1zT{4jOGL( zDHW7F08N=_&usx5p8PHZaREpKpb8f25yJtQ1K`s`etj4l06=mH9JyjRS)`-l;i0|5 z#YRCR^cVOF5P~Wr4W1TCfG1?<%_m79=J@%zyMQ##NT4lgj?s}~^NccrJ69z31M>%x zK--8S0E2)Zl=V;k*dX&t%SHzf7C;6{5$Gd~0*Ca5)KeF}NX+JoeK}CJRu0x-$*(AY zp$nAC9pX9oK}Zr#lq?Feg(4AHfJ$trsj^Y}gB-d53(W>#N~DRnBCMe=Cou`|4b14E z2G)61{K_Da+1A@0;u^>*CXR^l76^D~YGvhJb!YRVlYmPpi87>zLvx78{}vz#7>xd> z#65%!AW^_N1@ZL}Ajm1(ewv9y`tL08b;X5MMA1;`KgyO6c0x;!Ayi8zHyIRVW2!X4x198U49R5x6aFRX!2eH$e=>PuiS&=^Tf zzO29zRUt5>8I8iE(iy*l{0W+Z*l!x#L50Yl$dx)HG}MhRjok9^0&<;!r9TI3#UO>Y z1b9|I1FZXBM=yms!Z4@)-WaG9GYSF#r*I6QQ?TDQ387oy>JTm+lY0M<_Mw_HoNN+p1IG9y&@9~?!f|ur>0?ML@KxxJAoC~> ze;fADw2+2TTHvR(&MWuN=ONs6-bxu&=dSC`_&@3t);SG5X3?Sx^#-P)gNFGU9 z=L9F>EVslt!OB5f^;Jc^=VhAER zkk}j|>G58az_7&o=j*&krAE6?wU1VRRAm9(!dz6e(0jw|b7z7K8 z_!MF!Gx|3aWEuHQ5u-yJHsOg#x8yw#JyyW;;Jpuit}=QkT7U2%L19XXu8t z44po7YP!whQ&~}Vtth((#93q5ctvg2(T+j*YZikrNxk@#uds z=)s{$?QJH`7H?%YGJYI1O+J6l`+9N*cB>ufw=1n|TeoqH;l`Z4?V0OgYGwSO^uv+t zZSUhJIP5ICQ>m(Er@OH3;H%Z|>KD^Ks`aWKU2>t)!0OxhQJy)=8m>*^&C-b%x$Jf-|3+f-9)llg3-Wt5dol2j#aOwTDXMJ`hJhDw}dJ*vE z$@(+7x?@M_&T(<>apWn_dQOgBfm4hfwY+lJmsW-yM=!e8rf0#MqB+F?=AfVEz?bK= zba^p$L(f%yihN*a6p?d#?@2RWjQY@1m4Q3$mJYr}tIeLkZM?F(Nau8Z(UBAF_ie~i zdm!vFJGLIAW1HygFv5AyxK%9e15Zb9jbd$ll;&$ZutWbq@u-3Mw=3rCWh@&pJi@{5 zh>P()M%c6kPQ&XzxQ@KZH}e=;_TcF(W8+4x0&Bx~|J6C)^WBq=B zYj9=G`8__Pvb|4s2-JRRnbm&ip&EnHV{|TktjzKa$O?SM*N@Te8?SEtPsRNs8QZKL z^H+|{td7lfsvUCfMMLJ-4{4Ueb$W&D$kzmO_u{V3>8tbVZmC~+n8;k!(V)B2#EFNy zWv?qPK0WMQbMv(F+dGSzzQn94(bjo#ch0@Cl-wIP{8u0BMf*p4()jNEj|xR12UpkG z%pC0to%)xDQuJ2r>}kn3Qr{KKnL$i_hhZggRojDY3o4q6CX`P8(d@+(-2XQ7=<02~ zzxTG)8a0Nx$XT!Y(Zh_iH>Nk#jy|PyWr#sV+ls)QXsedEjfpb~f&C;TsMe1sUntMIx=V z-++}ni*x{xL~korxwkuIujLN)hg|)WUC*cW8GZKY0Z;e2IXw%4GJDO|0)$IaVP)5T zdRu#+rnhu}k$2NI1pECWjX!Dk!9h&|dTt_)tJPLS_<(pNMb~A+qI&mf+`bh=kIV{?gRTuE! zzSWK|$-^c&#!P7HqvBKgV#UbfRJGpOtCGI#8aBP3oty2U7iEV!C3Qgbf6DRy5j)?G z5yLg&?|1KzoDhis9El*jTahU_EYu`9L1c>F5lm9!dHr(RpoYTNEM>#-40J*dHT+U)Xw=^irsIOAHF+oyvNX8;c-q&4lh|y(Xb=*uy^;-*SZg!+Sw&V zttMvk%ndy|OlffJGVyKcG5(;!9qYwS>&_%tEk8E#?WCgneZ4MkTpe9_@#?qh_dX@FM}BD(Qzy7PF1>g8)4Sq~zLO(& zbbed+sxuHyHi^}%!o+FeQd5)Y)s|K3-rj6_ zTzoOS*!1eQ*z{7$nj35r5}Ob2&z*zhoC0q4X)TcO4Zud6w<#v1zi2@gAU zySP@vTkmRWaOAAcJB+eC%vaiLuXngvwIri1dp5@}pf`2*@B{Y!U(ROx=@^N3af$}E z9UG**B6gXR`|x(AKBo_b-}XO63*Y6p_kM8a#J$7Zx8&94blF%hxX3-d+^=HF<4(Gs zS7`_GQeSm7o<4Z~=q)GrX-^$svvMt`O;4@5uEutoe2k!9^k&^28j|H~Fe}g@Mz6Ee z0t<(W^XLmIhVHdzL+_4nbZ)!xpUw_jK^ONK56~Oouw=+v=-o9gUDpNpJgI*CPPhBT z4J8({XZa<}vc1*q)Q0K}J{2o%-Ma4I-h*+Iksto3>x8vANoRI$-hcB&7hL;AYw+z| zb8ziX;@WqXw&yH5?Kyv->*4J&`x~xZ_)HnAJ$Uf=p$B*8Y~TBR-H#p@&)rU}o>q`Q z%5%}?{Rh6%uH6dxl00AaW}4{ji}$BKdkI3DTJIP&ZsqEuqn_7I_)@#E)adQmHs5xQ zolw7LG2Lr-)x!?^xc?*{IM7fZx^d6u-NMBKSz(E$HZS_s%{WK%oM&Nqc2jm!T+Z2L zv+O&6KAi7!&2H8gU9)n^i37uXMOl}GI=7n3aNG&heMd)@D~;kb!c zU5~qEq!$*9^f+7U*e|3L@c(Z>)<0cE1^nNlFZHV`+0IilCA$Bfp&o?rmIwu&Kcs3L z((e&K_s#~@tM%w5!NFjV31Q-z?`7qRwn08 z1_7NiEzB6;HVj?x<==LZ*mXJ;x$WZbV+aC<8xxO!5daJd5Wp(fcR;EJ9`vI=VoLzw zB|zT^>qy9=4toDj29>Q15$H_m{U6|eu$lB+ z`21JxqN3U*HpPV7a(fNKF4Efq&FBSMN`Q}Lm{-Qq!}4U|Tc)-Aoh*hwu4Sl0%UcQ( zLqa9JO||7KZ83^UOo+FxiRwR`b{VFtyU=YUrPvPFi4K>TGvyy%u=bk4K&_7W!wWNS$=z@frl24ebvfHhCdM6M z$E-$&UGx(G3=j>+cb*+K+!wAAY33V6B-Ed=b{n~G?bP}Or#X{t9Iouj*57nJWt;t= zB*%`Y7xh;un*O29#rP@X>T~)=<*%{4`M9im^2=qGCIfCi+MYXZS$$YY4DHU=!?Aih z4^KU!maF1**!AuBEo(TgPW{fWEU(zr{IT$rN$Yne2~9)PTVHSAxW4lK%oE;cyjyjc zRa4&giTmECE8ptrvFtCn4mOJGb7xBG6P>dkuWmARpOs=V`rYdJPY*8YyJzG3k!{Xg zis(JlrYd&Az6ZJOvaSuXuWzfmaZZ~|(~rJya)Y*I&g)$>m3L6}BwvNyJAdtuuZ`X+ zL2Gn``paTUf(l>e&^MJ$ex~E_E^I8D^3TT~x*9Zx-Pzkfb^8I5pgWD=f5kDxNo!(Ukl;=;FLvdh2`mR6Q>=HzJ=@yU$P3mnFq!8byt>!^$Qa&Br&CM% zwLN@$@Q?k{o-blk#ed>1p;r^lQnF7;*?K8AOVzL)V`ziaFC z-l^>O`tO<^_G2w^>72CLV8XTIfqjmTsL~MV#Rp_gzo}<$V(8aV!;hgsb@J)AQq|UC z+Tk3JPFMUw>@LilIQy1s`byTG^5oYeHivE1>R2>od|lTr!`irKx*c5R?-Nqq|I+eH zeN&e2Z96UX!ulshy*=(Mn&_}OU{SPLb-p!cTDM@P`FZEG(s2$tnZAdNgM4DMGR>^t-V%+y$sAL- zE3*F?gIR6!y3BS6IN$q>!I!yuYU*YVyEZ*lcU{socZ6G3jIlkt=uTwWheH1~b+#&X zi`1O9j~KGE?eQDT%C2rU?WS%RlUj7=RpL_nT^&7&V*)j%-)Wmn-?H|Gu-mpkn-7;p zN4zXz-)5dV8o)2NI#jH}}dGRySxmOgv%B<8WLj_UiZ%y%{4 zR`gvuWrna;)NR91=C&nUR8AdRel&IP?QJi5ZM_^LeAIPtJ#^=nbuIahQ{{PSO}L+oW0}V0kULZORBNBu|uZNyiYETrwq~ zF8d4cy_KaN_b?HRGPIUwh9?403}7Er%*yje*ZspsFdN$#8f-HFAkDp?B9hEfGte>S|W?VocxSG4XnBZshi{+t)f-KOa}hbJ(Ky_>%bKSg*K%2VqHB z^KKlD7W=`EX- zq518pc*CmZj#m%5MRA-G7soh88kw9mUtYmyA9!Dre)Y;Zk0Mq#xBm6a{2vDVzTaqO z`N9gTW$Zfk&B=@{mP13Xc34oNSJ|q5z3aP1!_Q6j>mPcA1Sa2GI`!G*oN*ov$J2#Q z`*%O4m0X%Q#y`#ZC?{{a%lhH-?2Z~#s4m|Yvrw~Mv;LTl_?*v6&wImE`j|CM9d*lZ z(C%*PV)gg5DeYONUFv7-xV7$O@Tfbp8QdxSDa|#bgGP00>$7}w?w;6b4HbdCudhC) zvus88g6{mJc1`Q81K&91pSoN2I?}D=TGN|z+a`3paE~VJad)=Pi_hHD-l->jeuOj_ zj9Oo{t`T(G+~-S1mw`}G@`?~Y6>oA#qpx3sfS;I7WARv-SSSI6Yjwbr`&8Uz>9OprN&S?ljuX-Msz#N50hbyt`?$+KS|Lg9dCp<25pN#XCRY?emB3 z_I7SFTX)W#;8zQ}maV=&xyNy>dEYlx&+E5uV!ZmM>P4$}*Y0m~bj#Pz ztVxNpLsNTYcK;O5Ja+8Z=P#eXKHpKh^rU;)LeoKG+im|8_lX_%vRh+f&7#!{Y8`LY zJ$v@aVdy04{bSH5-md0JiGGvrUb=geS-0`Y0pk~wgFZHjzMbrPtlxA~ zy>E_#w>(ZU+f<#So5fVytaETEGpb!;S>TOnUH5H$#_=BU>Fvu+Nuu(aL!PRcI?R{q zH{uQqWyM5}=SR(7(PlxX<W=NgUK>ynjx-Z_)8R`0gNT~!R${pfO5yAR0H^%`ak~!2cDF83{O}h zLG*v|#4%C+T>V9+V~h2Fl7>jeOPu=akMw_JcEhQ^ApIYS0Mvg40T4elsRHzm^nYZH zJB|RdiXu$^M`OW?B8mPF=>E(g{U5mhTXG1|Pq<({drky5p3U=%V1sxkc+$|YBT^s{ z7JY{n`vD0UDiZMR*gT#sCx)HO6$r6#Cqm>P=|QBxDKuKl5vbQf{lsa?JAqaP5)Klf zgfuWv7F}g8WT!x{wBhpwSU?$v?+cvTK>QSsg9JWG9tc#^h|G=>0Y@k-!V4F(7sM6E z*ocA0aCnlKNMO>KZ6YI~phrItRK*d2C`$(re<$X0L?9ND6CsFC5QsRD_Kx<}25PXC zME(pwB!|Oe9pV$9>rp%aiV`Dk&p(eegy#$EDqdP7@CB*cPC{-ZS_I7x9Mp(p>_vO{ zlPEfSPIyu@X7f4;;-0 z#kxXakkS}P{D^Rjf`rG~A)r>!Ef7RA%0$PKC=Ji8fR_{x(%nU|!7VtD%}XM(6q+0k zi**Dn8^smI1JqM@5Cq9{iG-NZUVvVf3FCy$e*~fk|U<5eM!e@M~Kx0YDmXIt?VVHB*8ciFS<%`#utegh&9SKCoLL0r;c@ zrAh-Ooc-b57YofGJr5a^G6?`4^e@~0f<=)b(mr@lapymqNYycae*8%$7^NbCjF>iB zz8f~i888mZZ=V0=M0y8X(#JUC`NkP{{6XhGi2%X>3If34fj9#E%K0xt0N3OgJO?J= z2q1g@Bg@2$ZcZcl|DLbb2)pP2F#k3Bz~Z|Q>n^VkOGN1Py!w36y@@vX#)5b$~Xd|p4@hzBZU6p?6VmO zLO}utASrM}aEJr?vw@|wTpx2t0er$w!^N7hlf*F~*^Gg!E!Et@5p(7MfqJ3{As5!e zS{qo!h~s&{>Zq@86~kdi!asqZq4Pg*Mx)N9aoL;7vbz@)HEm?4KAt&q^_pouFYZ)o zRu=u(G~i>09*sep?tJf1TN+fd%DHFAZfs;5 z1#aEB_C|#N_OM0ijJT+OV-x0SDq{Wp^xU$IQyiVp$QFPJLfSi{- z3fLD06lY~Ecj;xsiDm5VFj9Ajk&zL|Dk>7Er>DO;eAsGYV{L&|ny^pKJLVum!yAsQ z%@e}I4fZqF4IeQgKE1nzPVquxvZ#lVYMjR%L(b=@|djFU{j?(x<&X zjTcx&ue!hY`ucJ4o7UCphbQ!2EzY0VZ__)$bx&I4Mm@;b#qQ}eE;|dYYyoxnh>>dqfh!&&{zJr$m+Tx(--Rf zc8}k@d9%l>FVUNuwW^sj_wT7RyW4i`MV^>FSnhet$S|(b1^2 zh4+>=jr3mOaU;}Y*w`J1^8-f-k6k$=p0&&4d*Kml!!f)8m~j+-}7a%*1m=cf1QwPCuA z^K_oyV4r>(#a6DS#Zl(oJ60_WWQNo|eS3A)muclo?>tjoSTtUHAJ?X)lmDni``0~r zx$ga**k1AdnDOmi(N?~^JypFmzxeg&!zN#!mSj!Rdd6P;GUQw7n%67dWiI-D{=3TZ zRFjH{oBTE|oges8ElcOM-SSX7_Kd3+r-W=Tjyq4&8ehRq`*5sAc>L&+3zmZ_U+z7e zG|@8Z%J8opj`!oN%m_Z;=)2ti^2-IUk9-~f#)9tpZ2tUq&L^(ijj;P#>0lbRJ=bgN zhynE@``3zS`=%T^J#bQSm6Klo?>G8tEQ^jg+q!R&k>;^bZ9kvp4tqIiA#;SRx6rb5 zaqzU?ZW}i3_4f1UPIW+abkXTmRtcnC5(Xb3stq3wv#U-JD3$&iJX zB#+e)Luvrtub9h*#T?Qd!XW}*Cywk3L)Z`HIZOcTc@`b$5Y6H4%hMua*z*`^WVMI^ zgg)>bT5yLTdzHdN!|o7r$E55IVS?y45&a*jH+FwR{FTh{?=lChqs588zoP&9DWl-T zUr7EZ5y1XmL4ZGz|78e3azT#55kQXo53@J}U}3_6qZ0ie$ojwxk_Epd-B>mh0r{O@My`K&yf^sme17lKPSW zFVvRnQh<^eB-k*gG8O0Gfb_@~R2I{MrpTHSM5MB?1^b|YNO%fiACx;L+&-uvqab-Q zEd^W_Zg#z#F`qq|icCL3MRUa!9sE2c{N!t!D)HsRiSt%6k0*^M(K3ceuq1`=lW7@F zY)O%HKz98jQ_G+-neeY^FJ6{)>kb>M2= z+H&=uw7$g#v8!kAylnes)bnre-|gnCxTjHUP_5@ZMQn2jJehyJxaLrOX%M&}aWl5K*%&^IHSam{*z~UU_xw7(sAcB1KG!I= zPEWA%tO{-@Z}75xmc>+=m7Sc`0pH;|tizo+X^N{ zyKC_U;<37qZ+V}c5;l+jNZW4y9GzjsR_l5R;%=vRZMS^pt%7?OXzjW?FTLq_`H}v5 z?~jY7P1-)|vz1QW^Fx9KV>idn%C_L0>~pQ{Ywh-rMY%jac}qOHtk3px#ItH^I zTkd;NQ6lmS`WT$lR z-7##W*5>$Juk!GSfL&?5%M8voEHzAiUVS_1;I>w!27SU-H%yu1Wb}Ap*Xkv=7a7q5 zJ1sqzcv1I2=H$1J8og_~e-}4@sHr}{Ie+wZy?1t7@$r2~{`YCqR+vp*T0-anARahnrxrY`Z-wH{?} zt_qgC&iu&EthxBQeBg&I!e@CIBOcm02h5N0<<~rxJT~}rd#8_$11IHAzSr^CjDU@; z1-cXOmgG%zg(~g7+xO&~I_T{a9y0Ytd^-~SYV_ffm0{O53UWKF-()bd{5E?8@4}L5 z^s!9ap|LYA>V{k->u?tnaKqZjoNSTrY zF`Pl_7cN^whJD0oTtK`FN63u;`YlKc2P+JT=%J`a1T$Vh$`dprHl(3J&3FVePk;+n z8Pe8yML8rx06#c*MWC&T!>bRK884-pG`s`8w?v}`IxUFc!g{cus?AV+R8W*XM-(R( zB!Jw*U<8mKC2SwVwB;z3NdqTYx==kDZ2WX&$8&jUKt&6vEr^CsclpnNRbU7Op;rU( zLF5$$D^~#)C2FPx8YX3iJ@mcK{vPi3f(XR+3RJjgvT*sYb>KtFMh`BZ8=n*p4^@fk z(l03i5~ff?BL~u~N%YAChWm*H(O;Nm#|EgFgf=9aB5Fb}J_ofldUJ$QE4-3Eq6Rf? zIy~=w95F~61OI5P+@OqTvtdq`j0ta14d8P@6qvFeL!g`h{}!iDM%Cp?7Y#aTieHT4 z??#XemUdwMANimDAK(`MPyZhcb_{G^g~$PK7ckTRPyhd4tN%yh|Awom4p$wnlJ^%i z05g(vY@u7Yt_m|J7UVHeQBhxtL{qCcLDB0y)aY2g;nkTTRUzzlaO_e(G& z6(mZowDQ!pbPCnXj7hOzz>WoS)wZAtl}cfmQxJC+S+%VX@wOlHr|Js1vw%}q{6Wf4 zu4ja~R6pf!Vn{3qu#pf;_WnpDZm0S~(u%*ML3< z_B(*h%%RadK{%V|4cr@n{Tw;_5ZNpO@gl^MAmE%2qk%Cvh#M)6u|{Ap<#He769tg= z07M%stBZ)IDf)^ekm@KNNY;^%=NL+2H0<3a5~Z(!FVWdUV1SI(AlTm#?*w8lDJOcz zIAP^!M8BW_tX#%p34MONJ2gS-V&tBT)%O!c;9QT~8CbbT5Cd245rmZ|1$g1hW3h4u z(S?bQmD}4KsI{hRuhS!Q-SLrgv$WA zR)&H<8MqMrhyvh=0fut{aJd9|3vj{?GCgvZ66|qiMxoLW48|C4BuJJZJ8{@858@O! zI1Ko$!aN7Z$N`puAtV|O%yp2Ijg02mb1SZoD42}%|DA6lJx&TDqK_Zzo#RT># zRvC^6%F0Z;A1F(o$WWxQ3Coy zVf#g; zY+sQB|4#*8hv=?@h3!e8fc{Uz)yNdLboX$?Efhg8tW*C_Mgk^4S!96s6`eL5AcBlw zP~mUz@@qjfri>6mW+`}lknPi(i=ec^zIDHvLl`7-M&Y}mN1@<9GPLz!B;#p|*eg=4 zjR71r#Bl$vpg@c-k#JH45we-c`iJs={AB$TvVS;pK>iO~$#&4$evAmW9fGZ^4X?E{u53HP=R}L%pNHntv zH;01>X%;?-=~#Ib+rk9{U9s@BjmOH#F4b7MLkuBb5d$kHgPvjKWY-L=Jd!8|Nz!1% z(+D6i65ttcfY>5|CraT;;EB@1-Cd;@KRXKc{D_3~1Zla85GyC^8?f>?a8Z}m4|2!K z-4jwh<4geEBF1>wM#o7YbeM_pi~vb_yaZ^8>pmuc$;0Su9*^BSGlc>Hc7p-ltN7mx zEb+g=vLxP0?h%`UOUdY-fc4BL*<2)@Ikz~h+(IP6IsdyzIM0F39V5nEIDA4ySpB2` z+xSH6I^n~x$I2tbu|lb!YVU%TlcWE!a+#xet*8Bpf5-QGq@`8R%|s9N;XOIq3XXQyrtCI!0|Xd{2N-r6@?S0Dyx7uGX@i z#qzJb81|5CtPOS{!rw~39SFRb4yn`n8>?hbfYfPZj3g;S78!_%ToZBflGca$3<@{q z2q1`kOvNR${;*4=Jm(v+0D-Qs@^Me$OIIf?-ZIqL0*~e7fr!{dgu`hb*=?Y~*%%L< zJ`Nh34=vpR_2oToim-%b#|J1)UDmY-j{G zUeBgr+JPle!e|!e@GVvj`K3rz=aTXvq^fCt+1dy}O{a%L1m!T&Jf+hTm)LTGLfL2_ z9Tg`5T>L>)LmK%}mIg9a{)DVD(d{iDE0aNCQCMIx!leIIvNGUHkW#XqU!wz&73lwb z$5_hTvO+xwY92Fe-yxsavMtTx35p>9mbVNbs)`iD&&l*wCLRvm^~CHLs#J+JMlyX=0Y`vvgkgkQ`3Ib2gfcrZ5<@H|_JP&{Lz0Yi2op~W z!4Z}_CY&Kj7DpH{aUuTyp2Xh#1^kb{z#L@v_h0e<|0$!u(w7wfmqdWSfd3&S03sg! zLUatqJ7faUmaX^0MDx}cj=m4x{N>ju@_jA@R<|KNos6zGR&RPt`Y5|z2| z;g_h~F)3Z5h{XuHi&(?sKo%>9*ss3=)=i|vFYqebkV(d>`IW``QD&&!T>_Rujv1?9cQRXwEx}xf}K(&$^{>aP=WS8SwN^JAUCr#hynw4MH;{f`bjRR9+$)&I-2t^C{Ve?Qm% z!}vd_!xRQN-tvP(+7Lb>@Mw^0@*!) z*oUYV2nI45R(RB?>@IgqN(6)7#Q!7ufBBn8)_`>e*(L8UguhCti1hZCIl}_Y7%FwV z{}qvfY}2qLIod_zPED^fu6KS1Qh~&B z_VY*idwSy@<=|yVD)7Nf9$IWEt_^}qo#8|5hCA$%i(=mxSW53iut)--91x&P9f+-w z%o|YRR=H{j`3Yp9X|iC_=~OdB;fp~jKyaz(%z)Iwq&F!{S)`=}T)eQPMG7`73EY7E z-XoiqBz3m} z1HblrC{!dAf2J%j48ktN0bD}Cc`(SOG(HXvKT?jfMF`Rpi6PflV2Y9ACMt=^f@4yo zFN3lW_~A@^N?L*yOhD*rNkmluXG{c##}T^lqXZIwOJjKBMq}V?d4h;I5O@Wm-XN?~ z)DaYmpg|+Dyukp2gzN+eHA)ekT|hDeHec)uuo{IT)D0PlNJ8Tvy;7j`V@d??SS@NL0}!&uTWeqe<7PMf=A`S0R~{;jSN#a0SdSC6j)opIs*BjfatiP$%4E( zVe~1~vs!8B9Mr(p5A}bx13B1`SE4VGAMxoD58SOY|(@vlK3YGBv+3D(XD7P_AB%tKj28r z;R6(HWh@W5MFTzu^h1%v2(={~0Gpm~U|A#=G(x=SB^POsz=JP>9~Y4(b=HS@8jcKQG#qdN!gPb+=mMZ|pfl+p$st|xK&*l@K>mvn zLpljb4eS8;4_@?TFY{!#BTM^40;6#a{fPy!$VCP~8{#1YiOL@IH{6V&?(F^o7*i2p zb7WosRRmy91%CJ(n}h@o8!%i1d}c|{4e2rjNj#~@v_o+$i0=o&zkm&J{3(}fQ;;d4 znn;3CV9rGu7wxka`MZQ=Q4WYfD-kvvH?=e5dE-x ziXqo9Ep~J?;9)p+)(tqDSThg=EO>8|^(P7!M&ta6q7Xn=EfA)MpFQiAwv*m6;oVqj z86#2C3Aq{LBs%a$gsAY=gd}%DLhR%ZRuSptToxeB_Wx`4aQ^=ddmwxekNR0A)Uqm-i37Fo(Ml2Zw5q+!X|-YpL9t-1A*T%q-;U`{GcCUauKfi zGG4~e+c8Co+%X|neE2wqY!rx8Vf8o2%oGQT#z=hLDXuV?W$*tqj8qKb{XeWSloob% z>nVuk4TvY_A8g;|=2Qw3$uxqA6o>{(ahjjlFxc-&xo6v*bINfQ=m09qF z|Jv!!wJ@hkhsh0sCrWmkm+>-|-%h&klTLRMHQoM=r#oSC@GG(f>;aUre?j;o^x!wx z1IZ?d@wSYv6J`$uQeLc!0OtT6i`;cuzz6c%N%^2<=f4^3B!)Zx)3V_6AHZ^416#mo z6_qvs3#g4f_~?U=7<^K|Ck=dZz-P9KO6zIh(?dl?^E>!_10SF})a(xYhcAH7WbijV z@G%4*I{2`_X9W28fzMd*84o@)z-Jx!YzCj*;IjvO4ua2N@ci~57(ferK7-E&@H@y$ zq&5hAD#1HInljBz;O7dUs~ir#H-g_mTg{c=y`T?jHDC-Yz;~!`Tfz6vpd8$5G%~;^ z8GJm!-$CEhzJl-Wpw36|y#RbxfilpSRztu?4bqq+d4pi!0WTq>us{%>;DrWVrW|-Y zU?<@Ms5D^!u$c%__^|Q^0qYkbjy+JZ40@U537Aq4fZ_wq6>?RD<6|ZI6d;2Gq%^6r z5T6MEdbR{r$_auj5CUW>@GqjZWdrq{BUcOuuZm!$9};zr4GdGifHNynHbx$Xd&AaY zGzTag!8J@4{t5|_`+tU;U||lDUelQj#HBH!MGy&8n=!Z;9}5Zw88C)eZibadM-%Ex zAPnM&5Oc#Z^W!O@FlD~Vn5bxV@e z7x<-0%iZ}{xqng|L+aJckU%HcQ6n9s9wAg4tUg#5t-*TG2!A{%f1wR6-E;EV$fJ)k zcO-SWe@1=M4#DGud3WD67&Nc0AFVoDaOW$tb5@YSs2>+jn3vq0F-*{q>iR=%pE!oG zb@cbR;2&yDo1dOD=wR=;g<9vn>_yf%lR(G7VRoCCee#CxttdWsYJ*za!yB)j+VEw_ z!ut(V^_+T$nceOsAK2emH>+^;?UelUo7t1IwDbop;}}uqZ7b2$=`+N~(=MY~ySBCO zPP0>~FY~u8(LDB}Zx;7k$Ka3UL%v_RUZwG|a%RSqM2c{NWyc8vrX|hEx}~C%8{e_Qv?x773 z?Mn)YZ%A+3yw{bc*}wd_UB!@Pw`REtV!q!n6CI8p61(l`mlZBY3K?bzjC%gCL$7-z z3?Gp;{L2E9_XD?h^xbl_XXktVvwXhqIe4@nNJcMRya&IPSJl zLI2q{RcqoZ&Fc%UI@eGbJ$aRx<9I`D#YxE%SLUq^J(;*yfA|dj;d|4)o$q~b-OZ)= z)u(mW3NCxB2&=5VBrspMc_VXk!Ggu7m#(P#k!H5)^0f|M9-Xw`;8$?o{?Ly19LKXY zSC8q5bq;Bc2O})1v8-~p%bTOwOT4s?Ug(B(Lk^F%8krTZU1l`u*!bHc=0ne4y6IX} zDvy_N>Qsb-$HvHkM&ZmIKR`*=xv|&F^~9yR_mahefy~$SmL87n?r*F85gvW6TeQi; znY@FOm_`eP*H5k)-RTN&NsC|6bYVkJ%c7Eh$_oM}*3RWVoqMUKXD7y<3(5C=KU9@O zr5HcD+FEFAdhm0K_{8B* zID}?~qyjoA<_O5yHt_mapRb}mpGT1j4>5ep7!N`eksH>Q9KmcUAXMZ^Lb7ElhEwoQ zf5oA z#91J59?b$a-$-wW!0hIl6c1eH;ENrE^M#sZ5lA?w0B8TEvS^5XPxc3#6%*tFSvit0tR z9V(-K`ShRWl&u48#iqvQ(*vQc*v;GmAC(;;4W!OB$yS4^4lq_(JO-*tvPaqDEK$)8 z(LPjjhLcUAZNM0x1RA-jjB*Rs)uIPJ2xz#AzGz8r=ta(PVsB}rxCvLui^x@#z9^Cl z=h#E8@g&J2F@W$+K79B~B@2Sa_2&v#`H}^=CeuKSI1P;vpF!)`9?(|;r_h0nk{}Iu z|NkGJ0+$G#67k#gRG2TG2CUO&7G_ks1)_Yw>n9P5fw?EgLUNJzw*f2ZrxpWqW~xA{ z{Eb4OA~1`A+%e%KHJrsjMmsFmW-LejkJ?>XW=Q%UjPJtE2|uFxKaGf!W?CnLPY>!k zvS`SpDR)lqn-$%q-^lTaGy1jXy@;H3z1?-g#t4_FTEpE#FLz0IJek`$vQ|Tsxi-zf zY*zO!y`K!Gt~lh{xzy8o#TVDk1y1TKSl8{(6jCD1wi+gO%DbI6Y?jr4s}5Sb-|k+1 zY23hF(S|8L(|Ct@SE7zCI2>&_8T#eb@#I48<5wq(>sGJOA3K<79^}%qeS6E(9}ff{ z>gMP?aq4H)h+!4CEt;(!exUWqoHL`mV$!PUGg}wtuB+X$=fV9xg((jvj9=L&w68d- z{rAr`Wt&rXo<5em%JltB{}3Bt(Q#&V*VKL+GTkQj9&C62W`gLxLrw#0MCz5a%(;Cw zI(PP}q-DG24xfCu`}gzS;(XoEnURY=8s=_)o!;@mDTg-C+nXjwFP-W>!;L=R_<45? zP2nszXQxLA6Bp(+*t0V{tIC_Zg&gl6S+eQwY4!U%<}S3kyJxP6=VVrW*x9)q&+6WO z>q+Gt%h~fVrP)|?>_S7s;seRkpYDCMe%%u{dhQ0%qm`B#y$7=%jZOExTmItpgzS%Z z&7Y5%p4#JeGjrBV^;^3d9@BfiZXe!U$TE3%`d-Ckn|J9quB{4it5|VMwNksW;C7v5 z{mj~CzD3WOqYj+9uw#Zw?}Yb9`^{aa-RxA-Hpnj^Ke((kV8xXS?QT>bkC-NOvmau- zV0WkUy@!o2^f|ef%b7EUQfeO^KRRj{CB5gT*QbTowXHsE)SPhbrrsx=ZR!yZYvLZx zT>GXCXO!l!+l}Mw55BhXe7IKSY9HID{p<8s>u2xk^N(ZBr;ayXM)ZEger$H7>HG2i zM{CPy?N){AuX1iR`@+KD(LEApJ=5exdrs{#UqA6fLBg%5XWf_w&%|nPj>!uyYjU1n z(bM>eH`n9qfPQ(sdgzzBcjo?xicw$3wdpmcX~nadyx^L!%|+v<5CuOjfO zd+Tl!x-NL#Chb54#n4o1ceftPJ<3$i-#Qh`EB|n(^Q+wBLy|_HN|+MeBmctuPKE5= z$7t)%_03z>r&X;=b_L~V-s1~A4b9!$Xt(`+10&q_Us-bg^6aV8s96`Z-;PhnPMo!5 zdCAq(pieh)Y%|mfT2ao1o0yo(3^;))u=*v#s$p$k`?LCJ0pJ;26TH=7Q6RnM2)`~ z0i8MG<4z6^2!K$4$;~Yd;NsuGaRDjRm>`21&`m2lb?~rOJOSWD!U89GrgvJLOhccDt1qU;J(VjYhX%Ac5m}Sp>7uD3Ka1P%KmA-h&rYr6erh zUI_^R$oe-^QEgB$lU?|7OL8eXken%z<2#V0b0tfiT=lo*YhR*kpo_AVsf1FfyP|wH zCVDI4=Kzrhc|>_o{HOAu+%b{lK^dn=2N@D7aN`4(p3oT`w4td3K1SIcq4m-s*3}1x zJQ%|yIt-DWjL5Z-R}^nBmnZ$B$92g`0Grq9hPHiYQZm{XS)(BwExA zU~ZW+EkITQn3-hf8(f<4@;~sC|14_6k(LI!m+XdA7upR==k~L%~ zxfKACBvv3o1BeAu1+ujj*@$3Ciq-ZqOeJR4K&}kr}sUovW3=t=xCHX(d znl^&jMK0-Zi}?@CE@a39LsEe>S&AG508A?ukX}jgzZV^Df*c?Rb-vItGN zAXXlk2}K|PS-(Pp{?U_1cXE+vMf{Q#p?@%l)db{bSSkRp$g{Y;p;JoNxL2Mtd`hK9 z$Kwz<=^?P&jX&4hS-Jz#JkaDo^ERV% zV0;3+V9)?WWs$+CTN@Ar(aO-l2%Qgp5g?7I5Tq+n8oP=M8HFQV{o=R@u-xOqcTD2( z(y)-9NaU}?lqm9}P-Ox=1wzFA%|HX51o^Gdj!3u!&fbEshe0bP?IhlNGQNXK$1TYg z>f#+E;B)Xkhe{(+9fXlC0Jt_)nxho9L;N&5DHcPDPLe%HkOML_!8R=U^pK`TwGj#h zDNw8@>{n=@ri2)hQQ7b#g#vEmPsSF+7V?4B4hx$`_A`(^7Un${zC4df`JIKh7!@?- zM|A+7Ys)Z`x|Nnjc+bEDXraz&p<+)>U*L2y8!!q3(m&}5M3-9l zI!RJr{rsIge_#=si|GG~RaA;gCqGa-wB%+6cX1Yd-94Lx(uOVCPajU6oU!J*)d|&6 z-pm2pUAJX`Ksxm+en)8?BC!xMP;k`mXh&&*B=4#zJ&}jY^6WwFm)iziVje-Q{r!UX zI;_`Kzh*PkQN2e;@6m0jJ*O?|)X`zl{S#C6j#imJMa|d0U^ac$j8!hK8h(e}f9(z9 zgHpg7N;fX`w$1Bkqi;WtlD~aQXZ?=OLwxP~c#mF0a~`6~Z99XZ<=blL$x6FvU6%{$ zHEHi{x-M#C@9nK-U);|5&1;8ojkmts@AvWUzJ~iLt1~BkExqWncOSKjs zxLS2bFzo9=)8pqRaQ)BG+qYk{Tcc-FTz%od1>QS9#g#X#Sn1I3wNdBoZI&N>Ty!z+7dUU=e{qND3aX--mYckalBfKI1s84*6aDqd|X8d<%v#;W5Tde}}vp_lU8%>+J=7mpxv7b->Z!8Z+m@&JVua3cS5* z=3GJc4fVSR%5Rl;eSEHa!E_0?=H?UY_pK-QjWaknc>QqCypp-A<9BRaPHVNgboTUH zTU^{m3pVw$y|_~Kfb-6)rny!9Z#zHLSoytf#g6a`sTXI>owre^`$O94mo_J+evD(T zHoOsDFkJK7fcyMU;=<U+(p6~H}Qml6Kb{nl~9bZGLSEm;d zTb5mU(x+*~J+-gqbF(^pJh0(;fwjfLf10mkpYv4LeZ0DUNB(R_&$4sEAbm z#cvxMye0t`VrjvGwOIhGW0CxPrA6d zz32AGoZ@e97N)%He5L16i*Lj34S6x_`rbSA)swpk(^}t(F#OzlX5Ez+T^8RwXY%pc zguEXE%=T>P)pLBugV#<>_V^k;tSHV&|Cx5HCtcpY=vUrc{qg3-z6)L#UmjL&cY$3w z)1>i%;f=UkPE}<_z1B3ia)aA}SlAoTdSpW|RIs9gqyr}z8QHMbM}e{$R`<1BIWDu2 zk!{P8>zF(P5UP1T`S9AnXfJTA0S40_yDgekiM_Qx+aQ9)s(Ob~^*Gjc!(G}=wWT1-T7`EXg>P&G+#jA_j9t7353wS)d^Y0!Qv4IVLHc-DJo&i}l2n7N_t3&rb4bGW zlmw4(5z?j5sDB4&DYBWEAqNE%6DBc6l1xK!ekpb$k8nRRLK^7_h=cin+M|XniC7WH z<|U!h-tPgV{s$pO6{%%$+p^?GDIrnKCpPsYtt<^w(!vyk1!1%S2DcjY2Y9UTGa#&B zTi_;!avBfV*kQ#Ds;B)=!ixf&oWFzQq15? z?wD*RAK>ZN@V^9}AvhNn6K8lkYys|*{B}~@Cs+Y5MfW68lh;zWVCa$|*R4d+%|Xe1 z4E3g#@pweMXt2pa-C*$kwUVm^jy?FqsLlAl$R5A#h(j?$fL!HM3Lu)H}F5>7@|li{wGLu3n?o|ils>6*iuP~C4!%^y!{3PwfqVGCrElhXChgP z@VgD-1!+sf|D?1)|DzQDQ|_2>`X5|0@82MEkcFG4l*~a$o;Nv)1B_G*!3-Y$`Tol-Mq z=+oy}pC|WU5D>L$`Go}mAtwe^y~`ie=$hf0t&uablWWMTvaMg=7DTBHYqQ2z)Bl}U z?0L2IpUmdlEZ)|)DSY*bsWY11mGm~P+`eV~%KH76cb~lzqLQDrH7oPGQ*>3@ znZ9gft1QWyu5mbX;QjlpsusQM;=k`Dvx#t+BpM8GyW2Ym5AE&sw=AtXs1=j14~ZRQ{O+=T)?&O_hr<&?d`9+S9UYE60Ni_Snjy8ylC90H>%4A zFJ7U!|Bx4B9&5q3ypDx;Y|`BeG*2AL*s!j`KH}z74QlY(sowEc;o~=~)>8F-kh6dI z+g*q$mc#%k)*vT``RHngjY>Q#^zQNUleJx-wFs_irU@CUu?w47BZHuZ{R zcVdRtwi?cCcadYdLH(9WTt^F=hl}>Mo)I%+`H%~ASFM$?qlPqUE?aZU!kup6sm(dD zwdhr+xG~j$R{6#5g-eF=cDg)CJLFR>G}RY;JYSO@JE(*2fxW@{qf1`To7Q99$tT+E zKw;LR(so;is#ok$n>>5#0qd)j$bJ37IDf<~1Tpw?!5kuGP`LN9B?LDKav_Q48MTuihj(^oKwV`PwqxkvY2UEm0hxEtK z<6keXIaFH^J0xN>#rY$9|7Nqs^CKtUS^n+qCzmwK607Sz>qi=0nf$^hZG<-W(fPx- zsvioYN{pUO4P5u=`Us1WJDT-!IKpi|if4>tb-ee@L|^^tWec_9cGonXPZO^gbnlkK zG4n-K{T{_-j+)bsENYx_Lfm|1@-|nyj*GMOo9nnag=hB7%Zl07e>+8oed=Z`ed65K z+??u-{kQfx^nLK_7seWE2j0~v?q99vj_+}OX%ndOdTYI$>S+C~14bbB|1m16WB#+p-+$ZN=k%VX4jtzpN^&_I z6UB97*+gOnY2Ub1B|(zpsxqQHY#NX!w3CimbK*PyKlZ)?psi&48>vz1TUSb{V+jyK z3Z=Mvuo?{!2!sS8XrbGddQ071s5^CUsk?05-Ke{NGjk>P-h@DR``@?Ud&_oPGMRg4 z=8PTropZ38P|Md7cLBP64PLWIDAH?Y0b#aXoPc870bMURUyMQ9v1>M)cFo3m{MBn# z@0b|ZtXm#~ggH7SEXPT*64DTjYI0P zg|f;&NkKie@+$^b1Na#(}6wR7wPfF}(jyI1P#2Kw9jSBNdN)`Zr zT>z2=5wnp!0a@|Dj05`z%4nwxRjn9Cm-!Hk5BsV4qO+daP$DZ&2S zCO2>@a8UMHE{Dw_TokF=5|SIy!y#%PL~9%8V(40POe+dc3N2gV=Bks`*x4tFlE2q8 z+lrYmR-YazK0b?*RMv+XWsk-NmDBwSa)=WnbaE~)Dg>hsib^oJ<)q}z1W9MyEE(^^ z#zbLoRpJbd804`F)ubiDte|XLtY?tIZh0;W8bxq30PYt@GQH)<0D2ikCI$4fs^w4@ zMAHDP3YjX*v?^=tvpxYvS07&CLP6_7YX@NvAPwXE|6v3`e1_iLQV9b5$^74FmE;p! z0_6Vfi{<~WnmKi0t*C@C+@knx9{GzmkNsG%O>j8n;kO-%M|Cr@p^ zuy0`Asot7fU9Udvx@=~hiy!PCJboF`Z+`m6)8>=1A2&KTt6)@wc={xf%ZP&Nhs&n( zokMm`G9RZHHm$$2pq7KxB%4m&;?McM2PeO=Tzh1prIpu^2`lQP+#No5-MO6b&i?PM z>rZY|cx0~Xlge_yoQ8!1Z#-T7p=YlNpSK_G+28uf%hLOg1wY=tyz{p7h)33${?%ls`9T4C6MZcye?FPF)oW1p{=5(vk4+4U0 zR^8ecxgxi0>Kz;D^UM2eY-}~%2DDahUB6U0s5q0ma>v%fZe1GgeG}YsSEoik51LP? zzdfk1XNMEQ_;J=3JG!4a-?>rC5Z{T?`ii9b3CGeGtK7EK_KUpnV&C56*U`L^mx@O9 z9+`I7<=2thGKg4Xga$bMK zkZ|nXi?!W*+HPtd=<2*PX|_QO z&JW0)VSOl(->m5V@!&0UOWyZzaIo2T_F~P6tT}Cu344ySKBnG0lNV_o_hWK*$EMdZ z4^GT^n5$e<{rvWm*G0Uk+p_1M+|QZ%Z@+%~D4AW`R?WHn ztY2f-Id=VquL!U^x4o}(p!uUYZx{8>pTJ(S_eAFGSwlMv4se-vcex~X>)gc6^-`Pz zFK=A8eciatiNi14DDh}uKCsiqdc##QCJSFhjtyyHpIXp*S6zz!Zip1u-#uTM^REAN z`$hvk>_2!;e0_Yba^#h+!_R3{)og;I^IEj#nZLPrq4}+&4~}p`SLJq2vU}05#-6uL z7auxx@W?>*+%@W}w{J%ZuG~J~pPld8<7q^EThZN*!S5Efp0e<3{h*j--|r3Uuy(nq zbnI1+^b59+4}XF42~M+u;R4IkX2n~1iwGg%68>G^M>n| zC$leaewu!&i+tc6^u@DoCN`sFtF1Zta+YZQ$W|UJ2bniq534zAs+82U3BR;_!4c8T zvo&h2cxmFZ629vhZ(qsCUh}e7jI?}uaPbc%yUpl2+h&D7?y#%Nl6l{gv*yj7sq9+v z@|geUHJ{#g54W|>bvd;{U39AMSlh4xU(+wS=MBgy$W>>YKGFO0*So{o=RIpS;8EkG z&xuED_PU(8z5R+h+j-he^&e{%9*Nsl@VM`(8eU&aj@S*Xe}9_!`isAPsrA@yZq#jd zVBdG0Uw>F;w|nVB^9QV$aU=Vk>$ae^WM9^ZFU#kauD6Jek8=bCFyxW8Y}sl~Ir>pp81Z}$8>V z%$huDaO~}r;t9bc91o5(Jv1%+)1bLmn{~-fby06sI1LNEzNUZd+0N+$3t#r%HC3gG ze>&$wsH*>-h@wlcdB-9=Z_I9VPU=#$WSC9K!5w_J^}?Z@&McKW4qW%@$F4oCmki&c z9veB>W$e~kzm4wye)-tz`LPr0Kd~IhRhduBv1%spU*2-gfu-xMJ(l&mx99N2gNL1L zHdT9LmL6Z-c1W~yW8(hYesw()qOHVy=2I6cFVxPTR}IH zPyNPw_Vm*-o~FD)A#N9?&kjG;d-mW_AgbEW8Fi8bC<2Z7O9urDe&Klq0?eF_?YqXuP`U~%gy<=vpi)U;HS#Dl^xaZYlJItC-KBFv}aa=R` z^74Ke+eXY7D1Q8&=Q=d5%g9n|jZbK%%YxiZPq(fP>wkOe4*PgsAYjm40izy2MQB@` z0WUX@EY+Un-~eK!VNt>fKo}!Jk=}!td=QyB^oex(<$s;r@SYj8-0(uHp7a8dKKWn8 zp*|F6FkYRW?gP=5#c^VR*i1$XFZ_ zi0ALoJ0{9K!j%76H2I%P@69ie|9`>az+8gCBVp+iNY{{* zEUtEjg42nXA~Ky++VUVW+z(f!3e=hu1IU%9!L8!CrAn(E>ME3Pr&Sl$Lh=r_LWnnIe^czjI!kM znYGbsH8X>FZu~(rv%H9yaO$Hm5QqT{l>u^z0)GH|MC+R`(z(Kl6gllcDMW3L;yS zNgRLWO{Xug$W%6&pHAdcg<6rM#f!-#R*V8|u0;bg?%-5Da?8OP#Q(Sh_q?MM;BlQD zetzB@T!=>z_pg6d;a(92oo1L9JMT|=8R}Y3e%YI~%p1e;CdR2}3>p8J&v*J;p=QJk z;Ev_xY)oR7utNshU5tH6alBy+i1XK>W<)RlkCzzcc%w7pIz9|hE-Wf@Vb9B^7qED- zQXz#c2?^y1V*;oJ9*F0z#A^G$Yg@Y>D{=q=v(I*9VPToH{Qoh9Q@>-19+>{$-N^Xo zbN~GUcH#f~R}ATHg+=JH8-Y;1b>jmhR8hpK@)5eUnG(eyxUXE8!+aIbQm4va$x7pZ z@e<{*B2*@wDW>ssdM1euiPy60-U$R-Tk(_G5k3@WaVaUvRQ#kQC){7lss~Fe-=uuwjcEWL-=2$|6_qTS zQ9CIBsGrgQF_k*J1p3BEq7Xo@{?Fb8D7is)Q@A;_2f*=+2RQzxj|}K;R6tP9@lZu7 z5;ZEd9E6$jkPBovDyb>}q&*IhgV^Z^bcQQ=F!-$g=;TqF^mHY7K>(1wK?Vg@h(@NC zf?tJFb&jt>2?9|7*B2lb8A>SNlbtRAm!JqJ1VAG~@HV_*7~c}+Z8jJmpArc^3JCpT z1!N4-7%CtC9bpwgk16dc6*6C$A`7P#7Eax8VgnBus1LdBF%QQq`n;|9nk_Fcix{L66~!I!Z^hVSf~CZ~S}P^DO8H58fioafkSR?<;Rg+FXuN}zXW{Jf71X1} z(2;~ckH!qx09A+>L1%s8wTq9AK`OW%D9uGGuss2ll#yZyI8y)sS47S++AheWK>ypK zRbni!JlH8U2JeM$jM7MS_Eh<`8Al?eqPJKmO%uq#-VuUyHww8Mfa{k# zaDvoC6hi9s8l&kIDTEprX&h}rY;|K_=pQ=>QibxV31lH+5XDf4hjP+gRFPuj{UJu+ z{n%u&9L@oaRLnU-Ct_MUNSz{AV;kMgU}h40Y)A!7&K-3Pg(*c~Y7O2@;06P%f}i=wlR(rT;%ti)V_+_|n6DJbRcNmQ>Le>f zcr+?q>O`l56d|yksEA-gOC4?u?WIR2HiLeM98mr@G45kzd<^{c@6WHLz}Vz=n0BDu zoQ5co8B@z!dF=q@Fk-$0_-lgn%hlQxvMsUJ@C3?rnF+`$-`cOvpiE@YVq>lfS_zGo zKg*!bfjJl+N5-b+hfXY47T0-FCl4ELB-4@qb)Hm-qgC3lrE#P|x#B0Im42DzlMzyn z3gmx~Z~9MUP{zs3vPaUmKKY-ZcR!dxnUMco%di6JNL-a7gB@Fg29O7S8TZz)+C zCt~D3Ygc!1_Ku<;F38PPp1u87+0|3Hv>A#a`mdtTG^yN+rO&j8sP7U*TQMO-Ig7T! zq+|jC7d~x81{3xF>+JmaI^hS!duZ2HDw#GB_!3qf79AH6mqbuNo*P8L0>zQYf;f7) z7O4?Tt50H7Qt9QSN?1TEk01Cg>0oKr443%7YZNaz~+FSS(*@ETvrGOVoF8$)uF0lk19=sEE+6*bEX&7R!yKlq<p9q`#>Y3Dy_ljs*}xGsp-uH{zP*+#8lIp)2PAyF>QEkVYQkz4mJSy zWi-wrnBH7y0F1XFKstb6sYDDzRxl?~Nq;1q`3f?Ja}*%v@Zk}KRmtWWX1P%3+b6mb z%`qN{Wqgh%oAU)?RqAm2!YU*&Eu!en_52B9GDm5ZNFR0t7+n=Mm^)LlIjf{7Q5Fd< ziBiXEn`0sdnV3^vaENb`_EAMot4t~FUC~K^m}9{F4JS3rp&;9?HW`S1eGXNLLRAhK z%8!eBtyQK&9Yt7Tidd))1*#FGLNcH{_(f|UqZu9O1(jX(wzdCFjcJV_%+61%aGl%=bx zK^pXMpdZyF3Zb^qHz-7qo{sW&7*dczs(AZCi$E06cI%RbqS3>Y^R?S>ypZhbpYc@TojkEXjR~=;firo$o>~ z4E+?&l#@JT3cY?LQJ+sQk1D5}MDa4}M-%mP>F*(Via2^X$wN8N%Sj&AiOxHd zyiX{-oTQJ4UQUk7n_f=#hfgn$C;Lw?CwXV3wjAvLk(jy1z!pR*S*q=Y(6-XnbATLV zS(mFL#hCzTOVM@WAMp&w&@70I9heb^s)H{_TT19kqXh6#L&{IaQ9(RNv9Q1zMCT1@ zt$_gv<#JYo)HA~5aD_rfSEZKIRq|{wnD-l;B2bfytUW?SIvxz30CwO93e+0IeNO0% z@iPRTmf+X|$ArPm-PHXG4+sw#P+CK+J;SP}f6g#O3YvA$7C=vf2hkhDB;HB@pdgTSO*t}}3a!(fh7&@+bHN~Zh&vnzqy((y8at()O~o00m) zM8R$SV*mfZm}qK((>O$CUHlJ8DraX%WG(*ZbiY8%|KG^ymjJvSL9lf&D`+Om0TAl- z{Q%f-{85aUdaM@EF9m)1t9=MESR!idK2Jo$N}#$c_D<0MW-YLU;X{xt4wjL!8b2cO z@Ae@;GXTOh!sV=YH)&@Hn_%j8A{ZAliVp!}K*IlTmY5pCbzF6P2r8cOCry`<9MNUw~_k5eb`YI#S50UV}4h+O$AQ-=cbsPJ(0-h##S`$Kx$Bm6YB+Y1OiskMS(!;w<3Q0sy~2 zu(Zen_YpH*E?vckf1+smuwc7vER=Jlf)1CLRnEr6fe3300tK_lpx;;v&Sc^^BG~WW zn(mVfa?2z!&(qc;N;?%Qj4}LA#t$Y!LOVHF18%&Vi=#Ej$%)oA@W~d?i zQvGKJ4#%RemJMOVw&MYy&LBA= zsgW~5t3to79@QsMsZ;?VB0&tdR&bnXBkuLZp`0MZPF6o6`{36wc> zq^O%^02b^{{~$^I6LnV@(%&)K1ZqqC6@EZLm?g-8fMtZFN9Gpba3GZ+^s*YNG!6G3 z3^ha`&jBcF@OL0Ej3B#)w^$}klLFnCn`MH5*@^{-b_sHpKzc7UdB6miDTbX179@&g z6|D_iNgCj7NbN;Ylkt&W63cE%RY)=@1bc$N+9w;hl0ZN-vii{agX>xd-D`wKDophd ziTq>=a4{)!_+n)mMJpO0aQ=vT2r z%qv+eOf^IZ%F9W8A(F1LiKdZOulu&sou$DoUz-Z4>BJ)ezF!EiX5kMjPk9?yJV6MWvnfhzEhAIMM=W#Y;>ppof_>)76yd_^L6N47xG3 z!MYg);Qk{BfMtNy65ua6>oLRHQwX4|tV4?(IqT7tbpmJSInY^;gYoE*siXzbQ{`<> zx-v(mk`_~6dO3-FO%>AW+jhtJe>)>1JK+2TY!#U42Rnsuf51tfYGE(o*DE;j+7Pb`gI z>Pj$QI#Kdq>x7+6@0cjemk_4>5{n3|As)HSl^Ls%mmN)=CnQu~4R#7Khd-pP; zKk~xkGjp^nLlRBu_^4PZo!2X9>qVNtV=f>bW5g;Rmrl|wvLRiT0?v<%gA3q}8T$Xb z?S1;)?N}AN|34ZTeKfH%xuvuE|7#0|RvFP6ZfllJtC148t(i2X;7HLPlZ%+sfEPP5 zpX6B*-&pc=-%{?TW^7%?TQl+d5-CQBHC?f`44{{Sv`aE6n6gszMj+H9fuR-T$Z_C0 zp%cd7A%G>BHUs!brN}K$sSr~)4zRFO$N}^eDz#DojxrjRw8e-}(35}_lt-)f9$P7|oWMGGessd(sP`&mr}LSl@mI>Hzd z45kIH29F(Ezu*Kd$|uE#qKRet`7*2OfCMUgw4~76{PXA=r*gRhm=@Tj2BvUfkkD6$ga8AMHJk}TB_v9r z7S0Wi-mP!nmwPRkNhu9`wIen!4+MeL6FR1nFE46V3yw=xHf?8NEG}>v;kraQVpFVFnd=I zIY7X=Spwy^Op#TxH9<)iY`CL%@OtUXHoz=VHt(~k(N9V#uJ^lXe$C~ z6HKsf#CmNFauvM9;*gjENANK)`pAu&*9HVb&{nsG_jz ze=ROuo_a6Ntje9Dtzs#vU7*zpBM7;xc7ib0HcU6K?JH~HLV8VTK>~{ z!ga6uihZ-0?!>?F?#I)u|IFp@;PelM@qlFnRj_nET<2;xqafi_0P28NF7DkKAVQy@ z2bszZ2W>8C; zEJ_O9d@IG2Ww8<|al$w*JeIvnDjD!A(#wbB!Srgafl8arc6M-KVVO_#@o5C2n(GXE zt*geeRg1Y=uMf}UO7LJhA+O+@3FDStVo}ssOk@$u7W{`uhbg!uSu|n?VZsKE%OI6o zS_>eJ`udgX3@XuxK)U`Ipq&$*n?wt!K~g#X9=c&u7g1E<8bnJ@izs3cAow3i0L1kF zSptNFXHfQ&3zlCQBY+ahV zDEd)mkm&Ses=UajXb70HC9|r2je$OWG{c4%OK&rtarEl(LZ9azbL~AcKU_IwT~|!vCP6pF%hNrz%C!&lph5PbvEK-hPg3 z5Z!1z;Qx2DT)syZ-K_n>t^H$~w2JDXb_n&HzA1ijzWD^tk*>A6j2Q2;TH+}R3##pT z@B2{yAzijcl_&=Hui0z*LhGX|51yaZZSv!7iOcy9UdQ)Q2OFO`QO`_#MKb-pjdvGc z->+xt#otTm;QfBX9~R!v6~q6?Y+YyGmF{1!^&1??E|V-f=p8lZX?&h0a#HM=6`kxa z^sqZNHte@W`?iH0e|S%hgLI zh529KKODQeutDiq%aOBt2RCWcF`@C`A>Uu5b7!7XczIp)-N05)1kr)|9DRJW#j5Vj z>+EV-=h@IX+dKx3Sk<^!t*SAe$5(f?xIemHmrfDqrww2Iqf!6#s*9CvzIrziOd8-g z_r#0k*=A!uO)Tm)IPC7z$H84c&wQNC+5Jaozt2m|j5570xvy^(+o#zM$FHAHtv|iJ z@75bTQ;Re6-#pvC*?m@jS?k|Un7@<1y85aHP1-m2 zoqlG?j&ap@ciO60^vZkl!WrvJ8vBfVQ>FNLgDEx^Eqguuyl4Ks8FpLy?n`_*{M|wG z_<}0K*zrqQ+oG4W7~W!6#Mbt94@T`9)0Ndoynk8jQGTQRyzNS#%agmd9naghu4NI67v1dYz97C~?1*J= zpIvS7?nLdJuR-qnMg0rJuAfEyzwMqm%zet5&8@%3?d;3_I`B-?A@@0gWgMsP#$K)M z@2#ueV?h$H?0fBQ@kd*|_01nPYF?ky@`Q~u2QO=LvrQ}MywC4G7OZbJKe5F5_~U^d zv)`pJ-8nu!QCM`*-p#?I_0=7BU%i=9eV#>+y^9lbFK6b?DbD@WDJrqovMSQ|~hdmp{sXWaV_pRV6|QF3bP%~MUzg`GQc zU9wivA=Ae=^}TPrY=Jyv#nh~IM_zr*F-kSMkXide|DboTGm2`<7B(=7GhO?>x9N?m zZw`DNUvJ5Z8=V76jF!yV(dlq--Hb2aU)dikQ;Qql^FNVeRqvw3>a?9_O0QT?-P);S zS!T+|ddDoLxXlE&e$`P*1&qbTPysMv>G%Vc6Nn*`O#153LjN$HrFbLYA=RngM8GhC zBR^<97v;YLYAoo%OoWOw2(?2VDKKGzhe{}xi(rT?s_Tc2_YNguRwWWvKvxmW*#{qw zu5$^)PXvriB2}h=z+b^22$C#71hwwo3bmneMv9@Sj_!-XSNJMm>JU^a!rfIE-a0yi zqvr0YAawvpuL6h$;D(5Z3K(-N5tx4sbyH6WH=@W#lq8PDRFVcF`rtIIAk&PYE--wG z_{kuQ6fN35V+Eo3MKF+%uT+hqo>A8Zqb}Y%3R4DWpRXViKN#v4tVz%U#f5@V?YuDE zv6~ZsJY&Km?tep>w+M|w4deL-fOK!b5l#x7stj!_d?8S>glN}&#gu1^4T{=ebdcXg z)(?w3Acp>|+azSRXnvOogtI5d(VFYf4g777xq4O#iH{OMfZ$eGuq3$c-7Jx13opS~ zOxP%zTU|&9FF7HM0_@>q;P-N{&t^b-k0h~dH)L4VRbcDj>vT*`Iw@hHFi&XyF~OHW z2p}Gs>CFt(wHS~RwWXrPV%yMK`UZv<@1;7^4=!@>#$HElZEz`8}vQ|RJnNMR98vPyUO!{+M2H$lUaJ^E8Pq{0dwBJD|abm4v@7I3*XDxNT$KXG~2Z+P9 z2H5~@Sk^YIN{0Z@Idqvz39}k9ucDv5%W~W>V?5OXh`ES)3i|!RM@?tDvHrn^Lkt8} z&K(Hte~S{9WzBf8?!a_Os~(pIZo6JM^wS)f&yPblSH8>1?DtT#x!CHB$JB#oetgY| zf6{8_vZ;%>X#u2`eM zP~)p}6BSRN9!^inoxfpH!Mn3fZnpUR&EnbTn^El#v~hapK4LCsZL^QB#+rUE{<_HV z`xy7u3+MgVQM5FEVFTqduk&Xi>b^;^ke9XLAo@>qYT7&yAc%N{`*DK1y+5 z%b~q>>nHYXJF;`w<^C;tw>n$J@v-mSXxZn_UJJi(J%9aO;n0uL!5>?VoOsE4(2?Kg zMfGFX2v=wd+6CS{l`N=#gvFPQ=bDn%1u5W6jhKH)@?(HY)m&*{J(9 zOUecnCHp+NJ*i`n=#TN~J!Sru#a)Z@mY%(TOa0-&lOe-rRC&fZF|E@g4#)S86=$8M zG?_G^O7}h!*Y@1^YMt#N{pu&om9`R z(Zlt`h3+OrE0ueBZqbLfaoYMNJzeB6eUVL}>6wlledq7J!mb)$wK%+I%^wZNvv-%x zwSK>5Sld2>3Z50W-umurUBTzoUrO`0c?RrXdF6uo*^R}=zBlRhu;h7Z1NHSABWr~o zl$*V%QkVr7#;`7ZY-f!>>~q7J`))+_zNb1y$WHikdJWn2s@8#$>FJFxa3lX{|7e(V zvy4ZJt&6umyvkpxatgofFz>P3)UtE$YAH`fo{Vp;8FuH^9G_9IL$+DW>1&tF6*pMt zlfA)b{Oa_b)wkxIoawMB^yXO?-u8_}x0P=%hpzs%_eGro=T~_pUm|ABv)Vk%^=aOb zm&Zp5>-tq=FC6yB(ej&y{kDPoOy^$gL)F5>MImKI*`n#ay*>p@7pqc3`6e1$3Un7;YQU`fGDU~0wF?Bs|<@dNQQ!q02w`8 zp;7~ErwFA;1KB$h6{4IlNdS(N!KJPWT8FO+)xeB@0dmk~5aC>*L?RC^RbV{_o_M&> zP8$hDTm#}4A;e-C^=K8D0ws{CA*5Oq7Z!=f;s_BaQ$b|p7>F!KxF5jY>k5TmkC6<`7ZJ{esvR@bZ9k<{guf2X4^nEdP+Sarjg%9Xnjn?8I8l>?8-ww7!2X|) z&l+u!Pza4ho2ROm^eNBu2SUb!fz#sk7}Rkfy5P}>z#L$j00=C{cRkv;hG;}N$$aD> zQ6;w3gJH-TfsEJ>w9wy_q2hrKe5EomrujqaOX9fFq{(8y28Jm#E!no&NE@LM#|Z)Y zUP*j`6bj|+SfGl6w5*IyiJ|L)REYw@bGSfmIC2h{r2vpPIXhf{5>vwFpav`rkPRFt zvK-oG(w}TMAPt0G14nf#Jg!w?avW`l)ED@`^grmSQNbrvks29#o*FRTke&|hA#^Gt ze;a*g2xW@M)rd_HTpIecos3AC?^v*$2?TzduX$u|0SfJR2v`&X5u!q^0fh{zo*zt8xega~!{})syX>d4D z7!x^z@na62;@ksHIDi!EB4c$i269vTK&|aJ69CvWVV{uG$)2kvbC0zi5 z`iCmqxD_YDg}n!tlNcd^^&TIL`GXI5M&Xc47{s48+4^@5x#cRmWk8%Se6itIFA>E` z-{xU<5S$4h0XKf0g79l%fY;X~qFqRwbovi4Jgc^r>w^0Ttx5yDALAq=K0-1@0e!Dn z4qPh@JdP3q;r&Lx@DXAkvl9i7MgAjsrEec za{6Tfx8;e~8;=XP<}HT?-w?gdJiTl@kcaR7XmYOwr`Pvd!&B#NxXV{hZf%i^c0X8$u;-_v~ zS$1dt^-f0zMn(+k6zDtc`mAmx*W!l@CO=86)hRf9y^kMnmBsLnNB_7Lzx|Hy^vNZe z#WfFanvfw0T^Ra69KNwm*(ImXN1v<@S!O%%kM_co^2Li<@#;^w`MG{@*U87n&3JIU z;7!dFb7gzZ)RRP&WLr3a&?->hZ#WxlOz! zevd8>$hzzI^6Z?&Z>+70Pd|J!3P{%r6w`eUry3OzP{q$17dG~~>9#2=lDeRTI?uE_T zZa)eNZK4B~t+apWXZLma-5-x8wOsc5jH7a?Zx{Ps;=pA7yfG$e>=|WFPiH+ek@P;@ zb!|pjpLLhrr;hZIlnp=T!s`C@z{<&+drs<=_VnAmW5-k6H};ind~4kL-1U=lEZ3X1 zI_)>SZ0^|YUAxb^_Wr<<#I4P`-F>s;+rDcB&l0*-{qpV6j!OmOH#R9*PmPvqBlo-{7Kf2JfFzAh>0K`*Dd z51+-JdiLQ}+s>bFyuAN@MU&a*PbwpFHP3flF0*cCW1bc!%bfBy=J?Rf*G5|mSGKO* z;8xT6HF7`nDrxmD-z2@+*$~?w#tGFweyFn6$Vdq8|AmO5r5!Jfqr(H2$t2Pwcy9k( z3{A$n;0rL;4RK)xj(+mMf@~YAYzZwyNn&LumVIZ$edG6(KK2k{10Z+E$atf#u9lV5 zC)qFtfOiX~W&zh|IdZc0RzT4=B|e!@E(ABHLisaS1AgNMcw?Lg+wlF2_bglheM5n~ zqea|WVv$wTU|z9gft-a8$IfN_7TZC_FU9LXDA@=(j*AOqPa#(4>G@ zl?$Z+=j|g10BP1Vz;79$g7FFUtPD$vDpkajGCGL_B5kl+xIU25KwDG^K1lyaD`ks^ ztDhnkKv7W3;2YGT;cG{whIki%bfFVgKf@M{3#S2c2|$$K3IiJZ;|cbEF*(O*yBOG! zKz2gB6s!mK7meG{*;iiP;JvV>KJl#l+6)nkQW4jTfh~&hOVIGFl?50Rjqe3=Xj=J_ zXh5V80`&lhE&+Y~u+?oyxC0K?D2Js$79v(Fr9%2-QMw7F1rSdG06UTe>EhUAu^g^B z8mSmv#ZE+^bdrPK5F&=5P{{b$&`Ko-NYx?XpesX-0Qocsn`2kEzz-~}*Q9Y{OGq7oNS4iFt|VyK&>j*>C00AvM7iM*}B?i7Nz z;T3?$1_BR*Ol*dh8yr8bsne1HjR9!EA#$q{qre5V97?-Ng$!OuIIX~91N(nEn2mpP z(UpIcKy|{ux#<3eTy!L7_-`KCzby|f!v7eLHUfYCSAR@?L~U}w0C$lIqEA8V zkX;}>*LZfDQ8jQHX<~C68}|Lu(I?npQ&Wjd7_cdPf~bx6<;W@PM9o;DPZQjP&O$R` zNMQ$0@y#sZO(*dZnU}cP<&xU&H$J%S{;=l5HPs8Z57$oR&jEi_r`~LhsFwR-mcXxS z+Lpw+ndx|ERFH*G*+r!1QnH~3$Za#zRdynfJ3}f{*=DE7wEcS4r&rY7gAEGYckdJL zyvHBX=Y-GZ+8qw8H*Z^=v*dTHnQzwLKVZ_8HRV|2nb*I5c4%^B#(3k)_7mD~9c8>N z#KC63xpU{Pua~?Z@HN-@alYBZQJv=0`TS_|^%q0VJ$#V92Va`I@W|u09iETbci^4tjP`dD2F&gd!8Y^z*4MH5+a`(u-yeiryE}30 zZsCta^|cov!?NagncROStKf|Lr2(tn9%=YBQ`vg#z9W0TciQ!QOzES{S+zc2I`sAI zF7Mkn-B;~;o97sKaPY3DS1xyLQ~g`}Z+?M4Rt2^yvpZNCfB423o7SxkWM!2;ek5vr z^HlD|W)8E)EPQ$(enY>b)-&_lnlDKhp0R(y<0gVx!mW>|mU2(MJ}~vrqdlIPS4?i0 zt*>{o`HSlH%_E9m-54iq8!O!rb}#<$spc=5R&BiL+`&g@H#IC+8Nl*g?>w?(=!N7h zE6rFJSnKRhG>>0+W_^eKafgbATwm#u>bMzxu`WsAo_eGC3Aa(+)X7swSCC**7EjEJ<==q0g`ruvnpHKVIpeW9|>$UlI z3r_vE3B>-Wf#QFF`~QDE@)z#kTTa9eATr?yS1Jk=h3Nnf;$M-dU!vgeK)e2b6!=Rg zL49auIyFIk7zX3}4+Z{WasRmxJ|`#SeU8Qczyp6Vsfmb()xb?cNYg|Q{G}TQ3?Ytz zMhKXd`2k*0AXt<@22FkCp9ht2JB?3Cq}*3Q$w+CW^*N`xokBMRAV^ip-Crjcl$uX% zC^*QQ$Zh$xu-l@>TPh5j@KoPdc& zfn=2oLYG_8Rzx`&Nb%Vsrh=Yg7KLUqGt1-CTgokOI?SkXMa!8GVoJ*CJvJ$3n1hCy z-qg1A1xECk5%C~DA1prP<)O!WAH9L==;XwPdq|tT?|6DazvJnJzl3A2x|q<;>Q^Fa zVFgf`M*<4|&13a%9xJ4Q{Ac8`!j*tfj~gZda0XoE##q`EMI;rHMpX_QvV##yWv#UU zoW`L5X_+K|h)#nFJ>6%aiVtL0)5z4o5F!=AP{RfkqWJU3-T`hi6-IVL3P^^ngMghu zdq*UZ*+&HQf3re6EW6SnVB%X%hlcYy{&;fioO;V>ew&A*UX`UEZgeK%+1dT}Zw~p| zwHOuBaL3&5ymPlRGd|=bTFT#R;_J2C@#W?FM|Z#M&j0cL-rK8V+uUd~yHy?YHs4-v zesS?mn#B)!T%SkBHg4#lOfOaNRHav6-AU>9z1xhN$%_`8+0(hhsAC0)JXcuH@v<&RHC_snlG z_;vUGoaTFidc2)hbTa8&uNAdcR2{RYZ_7pP7Tq*n(R)rt5O?wKYkNPa-#F{;%r9YY zwx_B}>hpXqubJw6AW`(W_>Giz?&ZsZ?lb$=x>3OW{yE*BJ7ARAryk>6JB%KXwtD@| zr@M>3h~H=0ohb2|p1C^sMU$*IH#;1N^l5XNRr9Fofb_?c^L>L?y8Q0@I9(H)wK?#D<>>CiTXybM$E0g$ z{qg*#r3IyNynTz0SMi-Bw4I}R+n zEk{HRuX|vKLRfvqBj+-8C#%3Q!(VwvF1BhE_*yy4KT76y^|y~l`zMaLG*WirQ|N-j z%YE72s!uCDd5eF4BylYql(w_qm&Oe{hxqNQn_FzW{_y$WwPn&}9&4}N?brFuoU*E| zI}1P6SmGbKxJvPwqQFa+hIa~Wc(7*FQO%GXkB|N{#@1Neaz4N2E6**GeV_ZqA+rbVT5>hd&Y3r=H*!_#0pC}{AN00u*lXtP4RK@a zPwp*#o!C_{I5Bn6rFn0bO%E_1bbsjht(|&!TSr`S3F@L`ja=Ewyk4S5qSNXH`~3%B zy;f_YQdHfy(aHPGN~hfF5*D}mu z`us6|13I|Gx)z_md{okZQjJxIFI?MS@8HMf2M)GBa=)bUOkvI8@;di=tlQLkiJKRl-9q|lF@1*wvQVPVH=OTLyVmNu+XwczQ%=&BR$L@Zxndgd8_XqB8j#f>i? z?JtgyIIVsF_Ww9|c2#`xz~d@fE(JCe>X8M6jY>H46VOn)Ap}3tf&uVb12Cs&Ujf_`QMc7nk2|Aalq2jaT{L>nRrv^`bz9-rUSDj5MeWAVl_0;i&Yib*wl)2z5syk)G*dPFnWr^x|6F1zs(Wmr#cK)`fS9iDh{_K`j zVCfbISJy3p2To-?+>tdiZdG!&!|*SiXMJ8b^`Nw4%H^1ADVOFBI(2ib$%nUPZ#uqx zc>nyWw0HaDiyj6JcvsbZL*2UXH>TD!9x&T!V$2^duF374W4eueaYb7Cpz!DdxBX|v zx=ZTjkL)Gw)lzV`-P{(O=4XB!f7B*-b?IP1w=%ywMbG`8avx`w_I)ZFckRQfne~5r zfA!UgmzJ$-3hFelD;rlfq^e)5DLa?m+H&{gjxF5hF5P#ydQ`9D8(;I@{k4NfOj$bk za=Q_p)jBs)Ro`Cceao`aC0)l3FCFMr&nm+Ib@sk-!+3>zJI?AmQPXxMlKpydfFu5 z-jek#YjqoKJz;I|9arO8oBS8BN4(imdd+9T;Zx7*_g*v6vCL8CnQ#28o0;fS)q2$i zPg-n0dbg(VMDgG+{xkZ#f8!doHq>rx(VA24JKVP=@7li0)qPjWC5K_V3bNhn*S+v{ z*f-_;FTY7$j%+ob!+$X7n|)fZ*LS&(&9d6}2FR8f^@FOWm3aQ8#nel6C9pxk&4l?A z`ev?3?4>$m!|Hxuov=-;$AtA66Wn_B>U`j^Z=TUQi*850f1W+6N;~6&d;IQxww2eCUS==a$_$Z?~VkKYqoJ%=eqmjk0)O z`u$$nTTX|BVc(wK_FAMFeB>Lq<$;Y> zfm`F->I37tes{9`>h0dmt^Ub?D*bn*_+6hjAoI$Y=NtRh>;C@E&?4gnA$fHMw;6DJ zz}xm^2X_uFYB{ao^~U)#I|KdyAaMV$FnBTjdynb!();%w12Ib}v8Mmy_t?KV>@i#q zO1DnC^(jmewrT;ZiE+Y^RRjBf4PfCDFun)WMZ6YB6pz6_amO7)oGUyaa7Iam65m1A zF$!SI1Zs^E_;V@&pI&*)67I4QD+ve}h{BXoEqiVVEd^PhC^2A}NT{#@&Kw^a?6+=z z4ULrWUBU5G#7D$4W3T~3Pg9!J0&}VC9A%;iHV$tI8cD#nv4Edfis-;YyaomvB>lK4 z5L0x1!?Yg?HPDUGAEvYz;ElJbz`Wxu!SqQp#Z2&i z;wqj(d>Lz?PxrqvEObjX1RI4bK}UuK`+u$0h@nV=ZlW;#np+-797TNKSyDb&@+coH z@__Zi4A?@T35{Z>!taA}aHHrPf}xd=m9QQ?AWEtvDqe$-s`?co2Hn8nW08t1 z=+O;DXQ-5w0@XxXI_fCiAKE#tf2V_8i+=%C-|(P_0-+EEAnA``xt($vA+OA+RB1YL zU<;7@g8h^ep#kax5Ry)WeM=nYh7LF#r$lh{t6{b<%c|64ME^L*lbCVCK{aM3CiQc(qCixnv4Kvx1U4Jh=3R8yFpn}yjK2}>nB z2gpnz22GscL39A<0|#Jd0}R@SbWKP<<>o4+W7?EFDcy9NZWTo@$Ut^-hGjXPZo*DO z6H$&`*$@PPjlds%#@MN}2Vzo(nt4O0MXM}Mel1Jq?XIikg(<$8W50YHTd1+5Eh0szKz zsQ-}!_?ZL%Is{-~L{caOFa#msz*V9bLVz+0=}kva$-T?$krfj!qHphp(y=-Le`4>l z>^U%XGhAH;S1#Kby$;`!vB#3_)Ghrc~epdpnw0f?46xJ za&tZVKf?>~|LgRH|Hk=4oNe(G2vd`Q6#{rXA+v_wXtiV@TLREzSO|bJJEnEOxfdiX z02e3Vk*C*3iz2N;0RmV>6Shyww zju8C@epf;M+5!spZoMtlARddIP^Ic^*&Wn_Kdx+S#L@ zZWa>2me@!HX;N7Z&jJh!2zPj)3Q(74!D6RpTjb+!v9ZBk?P`Zw#=C*KWk>eQ6}E)+ z)eisF)ei4JHn#kHY^~&}LE`Bwg%aQiiQg=F#BVsAGVzE@7|&n^=m>+=gPrjSTfVs#wmNeL#{EKQItO#@WLBc8w_{H<=TnrP27 zl)B1GDh-a{c%xknYb3&LN4920CTs>@5jmW}uPky)!e^Y=9JI(t$)4y;JV*hIabvi& z4)*^ls+R3v*{Slf)4zICt)ADAVt^q76CdIEio07GdYYoC&~gy}>P?Xx4N3Ud^rm3{ z?+leW_>f8LqF8B`*okNvomhTpwpLxpx-%{>TJXsm1 zmdh*&#tNwBlH_~^R32g~LRE$S>Pl&%2E@vQVdzu}p%kqi(t#cG8^NOByN1%Uh--MM z9M52-6Or3oCgzFh1rl^s;TIXje=-5|&eE2Ct@$7ZKo6S=#5gXC%XW0a@`xo%MIb2# zU`LWbEE7DKS^&5OiR(QH3>|3-fqD_01t{hM4CDZBb{2;XvKDeUj*iY8B;4jIK(Y%s zW;c6VT-5{EPhH;wsulyjMcXSVV*(8h7L^ogYy&lE=_;g|L!5~U>_n}jK5k}#g-D;P z8zB_w%aDI36@-RIe?V^}TcgGmgFFrji3THAxS^XoQvo!JRNBB+DYJ~>lc7-#DHK(1 z$lXPXg+>GVzp5JuG8P5xxpHg(go%r< z%<+QZ_-N1I6HjyuH%@{?4Va7!^3!Xt0t0=-+8|(Xz(5#^hn!^q)ELcCO$Owcs5*WO z25G3rq*d&v;8F~&WY$e3`Tn1_5%iISxGay+6NncA2n z2PWj84^YC1WT}kJQE9MkPg3rQj8q$4N2MX&_S(8BK}N5!(kPE0_&R~TuxF&dkMRIh z8pe*}`eOG}N*2e@ScFQ&(JF1-IJq$_Ow)Kod$^#g_TiCynaMI(nra^unjj)dJzY#@ z!%~KtMQ+jxmD2U=SteGnG{q^%gXD0cQi6=xu#}mF!=yK=OCNBq(KS>W5l0dKT#}5| zp;EdHB-^9~FxNCn$qrA_4lOha4K2klI6;k5fg70;Xd;I%NKK;VyonZ@2+?a9nOjT* zSY2utxGqLJP^l(5ft-mbk&AJASem9*B#Fq@WT_@9SSs`JiqMiFJ=2(nO6hYMX7mb` z3OH0mr=v+IdasLTGKuqy$dnp?Mx}lZ*fN4E2iP}_+o95+WR@4vhp5;zqj?QYZH&`` zS?Su+;LL1THCfctM?3FfQ6`Jf3slramg;Sct&z-N2kn6QV&gWbZe|FjG%C;-tp>SA zT$XmgYPqo=tecXS7De)Tkls6?^avNV$y8+jb70xCIF1|+%L!AI zS6aoZeGqX~KVd^iO{D>PgL&Q=wDlmh3J;_0RVg9IVqYMgv}>sD>qrLypj)JR1U|5B8O?Au!R*xUL7Y|1sW&jmeM=!4^4(nqm6? zG&)7H|GN5{Nr6AHI1qLJr2fCcg+ft(L;XL406G6i0{o2rUxxsm^qnH45I|S3qAYQ^ zIY|4Tz&WM?_}?4i|4odZ8JQ@+U;phtBQpzQYuYqH`O7Bd+S6~rOiWU9lxma2MI@+V zxj~^q;*yDsl`tRTS@H#{R3MN@6(Xt7l80Vm0ag4^3?or3h|5N^F8Rp-7f6~T5=CfV zBa*7pWr7?JmHudV7`F?bC4-l1pM}M`xe@5!11BKBl>+`7GN*v822uMkjT0RSgbS&B z1>%OWRzXHU4w7KA-JBheoC{tg2vLA^)6Yg#EsP;%U~Co z(PsbxsQ~8>O(!7Xi|XGA0V@&Yt#d~6knIM{fKZb`Oo(X~kpDN+LOn7>BV6vI=o5g} zh>Fpwl)(UpB_V-TO~YIndOTV#2aC@D?kXX#lEM%^3W|?bf-f?80ht+@ybK7;e!#OV z8N~ba06v63@g@LCE|LrhlTl!IX~SXimC_^-vDmQas!L?}5?=u5fqq;RY9nzH#Kr|d zS}@@r{|6>4QDLcbC?$}3hI)#n;VEZS6g2=q8iDgo2;^HRg`;!Apidy|JPsU(r$-b9 z0;J8fek?I9ux{l8=xZfuY8V-SsyM7_>ih2TvI&#!zqaMLw05uCulAlRtM|@O zEbH6-Xi<|@d=r@1d``$7(`SP+zbwC=xckbNnO9GlUtG7T+2YO(t1rI%MHCS*{Z045 zJ4OW-9Xe$iY&vaI>a@mjT}IYCcV$@>qjS8f=K_<)K2CgaHgsc?{?o^v8`Uc(`&+}` zNNn%W(@lHoZ9RG`_0-wHiD35dU(Mpo}?dv!3;L8r@`` zTZzkj?`i9!pYG^+QnKE3zU>Tj68r(54!^$yQXu*jOsHl1m^yj$W;FPHI~oJU;# zJhA>D-oXt$;xS%O@13+~l2OoxGoAb8-EW%FDmG>E*!9iEZj2pz_)7NlG0h*WJ<+?h zSaa;%@(AhNL-m7uw#eEkGWIB5k-T`q1G5_U>J}Xm6^Hcc9@BKd!Q!27EP7h|w(MHy z$?`OM8f{dPJ;6V9O3QVP>Whap5Li#y;;~|Mddzccj@6d8tQKweJ+)u3$$7Xc!eY3( z@aS*eV%d%!+`^gbsuunhST?Pv|CWL-s<@On77Oa0x9w}UNzuNeb@7xQt>;fESX!{V zLtJ=^>lecJ_30|8apIJnqw3s|H-i?*n-96y)$-)Ndh0l4qq7&WAEk7(uF`(lfrUl$ zE(>A@-7`BRS9lYsH>+{?zXl@r{!9?dq;7@Y`~xwRu+Yv%6+B4qff);9wG`k0x$r9#n zAAG7++x~?u>&>cm{%q}OqVti9`fhw{cBtC7L-)tYr8$|ukDBn%^zod5_n+;s+kSje z_{wKZ_bz&WW08^2@ly5@`L;ocsg|J|ujNDsoZq%ww6Ir~`#F}vUOc1d6UIxs^VejD zWZi5YI%#%uuV$}?2fxdhk=L*D38!oGtv+l&>E-_{(*H!tOsl>neGc{ zr?&7rx_oLFmy;amZnT!2n*OYHvs8_19K`?Z&L!YB;2^~$VC>c)HIaB)tVlvLDbiz+ zp@3j?unat=MXWn!Vbrad?T|<@wXw3wtr#y?IFdS+2u)_W6`4lNaw~eVL{uxNN4XWn zo|zO?P_$RM6@9~8JpUhiUjf)e^8FtvQruliDNZFWv``n?QWuI;(>ArFN$Ru|FYa=< zAAWE^kscgyxVyW%9$Mr-J1g1UY(gpAU4P#{{qnswFSD~dZ)fJcdGdLB)24TZ(S}Jh zqifovU=^)lMfhuYBFXv0uyLr3XC@7Q5)JBZJ0@Ac&gEcPFdtz%hX02x=|Y%Ukb6*hCJ9K zIP*7WuVZmU9B=j&PBbTrGnO03S;T(D*}}QQImRhvujAI?w&&V&%(;o|ecS@>6z&S{ z4o++CS?;6%b%Q|#93NuF%?+VAEW~E3AT?1-HaokU%|@s>=_wI}MLp10Xyy#t%E+c< zw_GzDNTmWE+nKe8R81P$vk+#M3j2-mjtmuvJ)F{=L&5`M)7S}+8k#A3^VVHUn-Qz6xt+&12i z{SNUKx;*BnwQY>0DX4~0}gjte230{s)sWl^x{8EqJxkt`;LA1L-S+K?WPWV9hY5XpE$T4a*ZhV)P+ zqYde?OGX>ggPDvrq(?UyZAcGyGTM+H|75fwJs`?xLwbai(T4PpDWeVPF;qqy(u1dr zHl#;W8Er@puL2>eyzfM93!9C)CIW&NVjU~Um#$Vc&} zi4Y$mpa?a^A<<1(*;O92q#^_mbAh7@@nn2dfj0bugvo$B1UYudD-M1_>^DRp6~O3pDH z!jzTHD7yG+`j*NkIt*Si z>c&>Lf#Ntv`^_`!jPAdx#+dE7O-hr!`vt_C_0C<`q{MP?&5fH2njGBhqNu_C@V?Uh zRjsym8rge8{JMCbC%@l3bHR%B>(&1DRIq_YR-Ivr?zpOg@+nk6$<7+E2M%Z@06MVMOw$3e%RP$JWc8F!G6WXBx zl~>*vdNys)^^BFzZ+lrEkhYKb(urmEtiuErYIxyPy&GAx`?bphKfnI`G3~>n+qPdm zHE9*UcTm&DVa;oAx*Nr=&26=2SkD0q??rK+tKYtOo|}8~z~rUEy}T12{MgyqJ$RK8 z8*jK2_N3ACwJT0`TJ`DT^4G~5oz%(w8#>w*UeER4-8Ofe9m_1Mm04bLf4{jKdWlDk zbkDBF+F5FDR*ZjCm z;-=p&S{Furm^v=_)g_N#U7K9XOv>+*YH@jHLM4kDj+B&J~gSWqWPp#-jS9-0^S~+@XAMNVGp=|SWyV`a? zp`F3@-2BQ@+Kn@(WLm(?4&s)x`sePrcFIS%D~MI^s-KXJ8sFM^&IrC=TRiv4>D=O( z9XC5{s@AcIzuLO%&H4@_niLN1Hh%CY>*d27{*+s_y0BLsI#5l20J}98Tc^CO(`aGA zKy~t(h7;^8ZbW47?vPtcz%tKjY2Krt@8#GkyLVqYzW6Hn#)$k^5olr1zMP>&D#9YA zCtN^~8L(YLDzcjxvq;0>OBIO8gW;N}ff(9~F$%P|mLBtpD5Y_N9tXN(oZ=YOM+ z0>Fbj1M~7frz@X|_a={2ta4k{iz_TIR zm523-(b^z@O_+?$Ajofga*gD~O9PUn+<;_(xZ26K z$585t&KFnz;;3}OekG&QiBpLymF|!CL!%Q0vm|Q!K)ii0l^$e-9&Ch8SnlX2RlI#T zwf``jE}+uGak_|lEOf`|y{L48KXFt#F)mjsJrwUJm`d-B({rix7@V%5(i3oc8kJ5Q zE5oUD!YmfxLpkme2DoG@JreIfnnLeQyhlEjPVg;~N++H_i*iu!O+0^)g#0{;IQ+ip zRC*FlS7ecm4hkJk5A~wZ2@9mRib5CS?NvewU4+xMR61_*tOn%24cm#uc4GHoC$sa| zQ`qy_tJs^_yV)1m_t|ebl{u|AT{u#X8z&G%0H<(rIO92UI4e23fNk(H=K<#pmkaEJ zExDb!60R#ZfZLy&%+2Nw0};XVxU0BZxO=&`(ElQuMn(Ur!)?d*2g$u!q&6+s;k(ia z)^vhRQ?7(^3D{v#n2(ONxoyEWUO!A`HRBK_Tt z^mlvG-yIOc4%VLNq-`y38-xhrk~9GNxEb|hbLz(yjky9+W1^F$HMlJS9XY{fXls!_ z)FywZL;g^g{GlHCLw)jx1`WBkMAyU(Yu4np2GmeA=eHH*Q|m@tp8h-Hl9n~N%{f@K z0jH4sVG#MlVDg6{Qy`{4^pFVl@&qM_ z1EOHmQNnsinmLTN1Cgm995NOO+;aEE{P^TxG(;koxLNg0UoiWM_{fLij+d5hyxE&w zm0M>`6%YG`_d0S5)#nDS6(+a{ZqLy)srS@gx$n-sfIm*%PU^vNy#8ru-TD2Fr$uh; zQ@G>7t@@T@`gGfvwz~a?)I*Cne^lV zbKYP0JM7b-PhZ~l>Gy2o{6_-qfznxi+PA_8uZy*7tZ9GhP?t|{4yTTqyKPgTeV+En ztGlnS+}6nd+|s)1-O(?6l84orKCk8mk?$DEnXTKJMn1|a`8==7fWLeF(e3m54hxSK zY(2Z`R>^_QyPvk7e(qvmmFg?4txMm&c-%PdXIe_#Co-{qpc3?Q}`) zr%&fL9ugK)wP%*?@buFYX8R|XOsd~IbyX*x?O5}NJ&q<;-#dEx-jb?gv({~VQEy}Y zl?yvv+BW0vn9ip!uDLCY|6}4F`b`4`sBE`F-=le6by~HGgr>Y!ZeAUXL33uzKw}V9xG}Wzy8U| zB~On2*(#^FRQZQP{Dgu>!xxWr>){qL!NpwFv)BAh{SJ&=Ejc@5_K`Ow&B96*yB$im z4fxX`EvjJud>>J8tx-!-_Xmt^GsRzpybskXo@{&#zphW-9&7z#%WOryqV@KY98Tu* zJ~A^&qe{Fqr&_-0K^o3BFy@qP>-OGGDURH*Au_W?)U_Ok$KL)y(RRBRmrPn8d0tX{ zw$+a9ulL+el^mL}#br-UkN8o?FZ+n)r7zy;3V6JrUoOvHS}OTcyP>$@-^x>LRxFEE zB?~$2GXmt0ZFpq}IQ)r4`4fv$8%adaelr&3Pdt@>A5R65|I|!OjmhGUC{zTZrU0v< zN`t_Y0=Qg7T7oXoK?r@7k@3ou6p~8h2p}Bd5FBqb4xBI=GaE^DNIE*v1{z3tsEHLo z9ARc}bHeUzP8hy1FnZL13yq@Xvznu}XtWlrF)Oyd<}Z@!BEYS__@$T;7_(vv?1<4o zJ2qhg&s1ua2x_%Eem!t#C^SI13xl?QYd%jX5lRJOp_JYLa}ASQQ~;yDp>yS{nQK-N z)Ks=O=v?{M45?UjGSYL91kmN90{CITd9DD^&I*vy1k&nZh7jO13W)9nu0~De*$zA@ z1S+gpwtdnx0xy}i{E$;Vw)kM60EFSDhcB26+ePeujX_#Dr#Z(C*!UtiD$Y>OFPt@; zzc}YPPq-XzV{TWj6E_rC^$NLDLF6CI>^Jot_aXj2WN#_M4_m-;&WP0c1)@B@;1;iI~hpOkpCXG7;05h$<$c znu(asMAR@5wM;}E6ETB{n8`%UVj^ZU5p$S`xlF`7CgK1lVm=eGfQbl0Bk5C-RR||D z%~`=3gse}1Aq^qG1pNP$7G2A+l%rH;ZEP1yU#c#Q@Q()*WZl)s}+;662cQvu?b=z9P)US5rx@&fBL)r z1w#aB{0)~493El6R4jxuLl4;m^+d**Vh)U7WG}FTy@3)DPt_SzaPWHkZ|W-zi>v?) zkf1RgBmjheJO5wE7s(*78j=4WbvRcJ^8fn-`~Se7{(mCiC+w&HAI9^C|MdSuKJ@-y z_x~gEAIsrL3zr)p&5J`~V)cm(UP>zuDj|sonkGXh;vkF&e4?F9CY8YbjuE0?AQK2g z0H{&OLxTiU5e#Njwm_uuyf`ZMQ$iU;bi6a41Pm3*hZ4fv9EI0OrS^wH<&i{munLGE zavnJby<7%Z{!ITca(;)i6^-DE$6`1IYn=2!hD?0VrKO5k(Fkbn7RcN9dRL)2GSX8e z2(4%YJQAb~U8#9I(rob5SkVYNVm3qrAizu*_JFZ3j~7b07T|2_(djJZ`oCd77oucD zYr!EMnF#>v+cKAWbsDOpf~pUUP)`67oB_%SLGuKnSR@o9`gI#Yf=ZvHXE|A7#u z$QqQ&5T5Df{~_lAQMcN>s>WT$YQW*MIG&utoKc+loMLV*&P@)V^9~~W`2%df0o+L( z9j8CU_XB5pWXJgV^ZzdgkaMIYNJdI3^b?tRB2;l~h%W)X7s!w09D{AsQ^g@R09`RQ z+}AlF)!j_ZKy@=y!5%2*(};N1JO*k4Yd!-tk+lF)l|B7KaJ;4fv7MO)p-KV*!->?h zAU_$mH>8G;@&AHGJs}K92X)|N+_I+>BX*z zwj~E8#`v*oA@dl4!o=bA5b^64r-aa>s9h8Z+t+W;3p>qG*X7{$jkJpG4J(Lhs;ZS|y$-XwA#HCkrbUsp~CXUB#ZtNWi!3&V0=lvga{-;kj+WDWxfaD6n0J;I>yUzb~ zq)ZY64COzuPy|wn{0C?MN_zaik1VjmeEk1Srj5!S+K&LECBUHMWlbekttXd0+=pJ$+Cz_l~q;XcPd{sI^ zF;ox#%Vc0RQYZry@37`|1^LVX8@MZ9+zoCch9N@+=FAjO&r`2(r6tPgbBQ?nP+s%5 z#tg(TtGsy8z_ygp+t#GM=##3$fG!a^Bx7{~5<*ApdDBbT?q(KP~?eo&W&^#F}yW zPvYBG4T-5^Z~9uq#O8l^$r{?0}*0Ixd2O-1m*$)FW@hmIqMP2Z-cuq z-1Ot4V-?QPDOe_&MuAj+!)^zHYg2M4IcX-I3F{b1Ix&SzVb1(mKXX+eh?q7KVNnt$`VEIio}&YyRHIhHxQvNL4E@#WLG;> z^wo^wXZ)00|aZXGU}&V4boSKnpEU<@i#l29`%NO$w%z70rNq5Oy{Ij6j|M zJ(&p5@)+ei-29iCypeYMBHTJW@zAB+D3^?&X} z7Wnt`=bz*N=NCkkZ_cVe(i1uDD5-S>bFj*$X^JfRH*!lr1o0 z&&#a#e*KBd6po83XsA7qHk90dW++AVih97AVqJf{rHPqB57jH%%%L3kfc`%Lu7A#T z7Wnt`=bz;OXF9qA)E()LyjLN02T);aLX}MaLT;%_F7x^qp^Mnaf-<@Ou}jHziL$JJ z5wxpPCKiTGhpd0vVA6l!qQRcm^!gvwpr|{#oa#>QXoBtZ~r0Ser z8hLMO@zx|OH!2|+Du_&sL+xwOXr1RqohBeVH&CI=(4?U;lc3-^NSH+_P&9xolQ|@k z;njn)|L@3@Pm4n{Y`+FxVqDwj3uRrwJP`4C$S)pG)RiX@{i_N;-F^poYeM1Yp@tQZ z^eKUwc^MRbMX*AnuQRMu$zuX~`Vzs>0n*+Sg&%bUp$fmy4tj;?3O|w1Oyn~Y>C7Y; z=L)Fs^MI`!Q%4cQIg!$gjuwm@*HtmZOM>vPB6%99{03EMDxS(u!Q9RNxAgy?{{Meg z@|UxBVE?a&$S%VEAISFzCCW_)2T4rW|LIK)jMq?FFbN-;|DRsyz|`@AN$Cduf03P? zSc=*_7^ILGwf~DmVjc`pqsVhK2ZZ(v&fjbQr}gIq$7Eptr?F6Kz(V5q@(=m{Wq>EZ zdWM(*%EC7a<8aN*oaFzf4M@gfnbie4z>!l?+j51YE;Kh$?&ntfC-kvOu!>RtdcfnjUHw>2|I8Ha<`%ZzrB!b)aJlU?`fi&)eyMtr+jqz1 z()}&>$Bw+S$al%1^xr40bE=Y9gxW9t7Uh&RkNyhG*&h3D zn&QOYS8BF;y5QXK4ogL2eLW|P%{bMwQ`K%8Qa8RiI_h5UnSx)}_4s9kqSp4fIbQWr zA5Y%HKe2qV&GcV8pA-k4zVW5S_UdHP4q0GaK;!+$3JtI*tFD z-|TMk-fauKnywK3noxW4?-#eE#?4V$&TrB(bE+uz++u+)HY_q~_}x#!$&1W4$5*?0 zzFksR!4*HV{?c}py>_IUFZty_^FPuzt)5`k=h}<-X2S+;OnEgrG$U``un|*+R4u&h zo6|JBzociM;myqFiw6!k`>gO{P#*`$$5lO~_11pU99$Z6(|O_eRUX?Wvs=gi+O1iX z(~Ld^k(rajk6+8M*B#;%HSk<8+Ofr$w#rSmzEay2%Fw?G4p?3r+{Yoon!O_-m;LA8 z5Bi_`W8SktpLXZlc3X5U<#R->D^c6Ga#swMe7;dQrMt+}F{vald~@!6bC*GrYgCHs zcIe3Rx3Byk?8^=6ar}=`GhBKF5E}poi=8cY}ey(*CDW&xXN+%yYoUN@ke0k}RZYisCN*ogs<4&!Sy*zp_ zr`3?~`9E;hxvSyG&5r991te zx9<4SgYNW&<|@Wu7y1?kT`ck ziWJPlW6H?{;{be=T3rzE2!Z0bPTE*STHIHvg>uObyx zZ#H#PJ!B%{xYXBrCBRQ2VG09U?=??P1&Bf!scBy=1BVp-5K%@R%~N19Qw^ zEuCrD{fvQ2sJcwj`Axe45kOM3P9x^ugYH7`i~y&X@d-(wkcbO0QKL!&0RGV9YsrDa zkJ(pb)Y;}1=xXZa7`+ZW2!(?r;PyPVMg@wD>y!$B{I37oQm+3Ssw_y}9hfI+ISzz~ zGmp*yo={L@8I|MaaSEjTH(G4R(mqj_nga3?EiCL40az9M3;YdB7Fa;EslJvv0PSfT ztJU_k><-!?KlW{`t+78XEMin~ITm>q5R6K0(bf+BbhC&D`G3~&u#B|a0t^edcQGPT z9zkxw9?+b0gO;Hw*F~D!-DAb{=ZSWB71aw+D_?df@KEl6KXpdT9?G=v;<>3L<<)XO{h%# z%IfuTCBHiFIID0f!Fa+f*?J#CodDr-OBWDAMYi1|pHYEr4N>>4sh(D^%VCp0hA&02Ii2CUdz#f53AE-cn@i2T10Bb^p9)!+; z@`vt-RfV|=On>(b_Vsa9#Ul9bkuY420<%uLty@|&fb#TJrYTc1QekTo0dPSXY9w|J z3w~1|7>-Ulokkgxp~DIKrIL~d;q|~>(g*e zJR>z1BS@hG>5Om+I9Rgb24j+UFldk%6FJH+Bu$x~K^-NHl^~4J1usZ}o+km8EI6`} zLF)|xtFV`5{KnYf010aa=Sv8Uj~rL=zQKO1ut1iGiNPVqF2D(5T+ly9<{|Sj3wyNC z=#&glR}Z@(oPTj>sSZ*|y^k^;&Ihf(Mxh1I30aQLZB&hCW_zF_4NP`$n%3$x8Aw#V zQ#_dY7!_zjAo+r2WME+ZpAYQ0YG!+l_n?|bH#Hb9u=W|$WJ1JgpYgWq=JmUr?U5(b z4lGzC*;4;n^;)9K7gvuIwM^j!t;?Ss-M)2$3{L);k5?ux{Cnrh_KPPz?NN|F`rd)u zj)g;akFhJnK5&IWI($N=mwM+i6cMI@U?O zz%Q|9_)edpoccks#wpwYc2DP7iuEnAZB?>g^GB=dao3x>-4GeX_RC zn~5(6EFK$fxpl8w_|x=&w7Po+?@sVq{$kYy$*d&vYtLF9`SauTl2ec7BpjRjWW!p{ z)a5?@)?LT8=f`!tP`c~)utz?(yA3|JJmT?&8Krj*JbRYnG-TY>Nuys4TNj%$C-9nL zK-i;K38$Bfl1qoYu|DTJ=l1zCH-3u<-h1}ppTpW+pKIyknX~mxrFHwp>(1wYV4ihb@74_FBrN^S7A4I>pE+OA?9;Yr{O8aXrFRN{bB`#PH_LtX>eZXvk9&k$ zX;<8x6WJhZx81^S^G3fKv@`0G`>`7w?r&YiQ&!xZXm)vo>|>Wr9i7*$dBb&^5jZ&c zte0fq%X8JPu8R27?sP$s#pJ4+Z>Bb#8`5k`*ucS4$Det$w~<2Z_B^L8Z>q(-KbPI$ zZ0p#6+0wT0^;h+sIHtH=c6HyHYdhXsExq3$>#@c5WQPp_+j~{Lv~=F0{=<~qt&cli zoOIy${_dAHt^Y;l?^rL!`Q8(&Z5E9hT&X&$(dqkJA17_EI#Sq=lAiwW;R%~e*LLCzE4iOZjIeth1YP_xOIWaF&A@6$FJSy zHh-G7TVB^#&(e2KTWs*y+U=uyx_sd89)JA$piiUt>mz1u{BmG%9=}ER$%_XrrN0Xc| zSJBgYHSf*z?V+Bvd2sRa&L`%)7t~CjIJeR^?md3`kll}i-&LhdV7I9DXSefsFCNqcDv{0Z_o5p#%=D=_@MH&@7>Sh)8-%TbJDu|h0Z-N zw@CTiXoqZmpc2*o8j(spzheX;sZkt);>|a)70amO4Ot7 zJUFv;K+9SU_bKlD{mR;DV0di%zL_b@5e*iCf5PZ7VL2cku?wM_f=LV1hfwJaY7X`W z(Gq3VwP+~kbtQTU0FK^fxnp_j$<=@?}VSvy9ZQvuWi8nDPP14fZhL;|Rx zB5Cj-h=WBjj)~-)GDHzUL;lbpHQLiCf8=?)+>--FSdilF&@2k3$zq8A`PGR6nWxA9 zq%q(J0H6?&j^$q+s1r}Gd3_81hXDiVb_Egxj0J{Dg(5UCRFD64SX~fxSUnV3|47B! zf4u~-L3D;oKzJ(5G;~oaeF9`5a9CQP%70f0U^ML<1oopCgk=cxp=Z*`U2{W40Vx2$ zDm;uY4dD9|5BASg0Oq45-?1cSAxxY5--#T;LxXZ0fZYHK6GDf|uNe8%2J@0Jm83!? z3Ya6S#2uvKkWX6ZsSx>wOhwNKNbPK_{x7uuv5&CWN7xJWt557I4|gIjM=4Qv)PO%c zYD1#%szJj{>H<&2(J&1fI%Sb>Eh9?28Z>mHHq12IV=T2Hfu3p5qsZHjPp~1-MPsx_ zH&sx!zM&6Mp4Om8t!HjD0X-KeBI?E(Gz`v27V8^|aUkOH{RLI5+gW#*>q*aXgoj=1 z^qk@^?djq^)XuL`Y*Dq^YScCY4vUya4S*6Uw6^F$5=kq>ObEa`)OvJPLoaR#tBxzyt=>6q>zD1~y}g|idpWGxyko$& zUnH+wbHDuB@PPT+uzdSYA%Ho%sV^;}Y2Bvi{X!rL02;Hw^t7rSS*gnW?@#ii# z`~$iiuH8A=J9+>9tlWiGev8^z-B~V)yf~?CVC6e2hxdDJk$6tAn>~I^p9lW&4&5Af zR2h+K_C>n`$|)|YZkH{;3`L9ba{PtUPH0)l5O9ou62>cXBj!w+RQI6k;V`{&4Guzdl-q7YAD}EXO z^W4${cLuUA?>IBZvI+~S|HWc~Xkx^c5ecjZh}Yf{*eFtfi3Q9Ks81u&N6azBjL0QI z0Rs6Z;R!IvQbw&>AQ1|rA^`o1nJ>+Cid1z9Fv1`Y;;c-I^Gb^c-lG&>DNm2O7y>|J zE#<)sEiLk3u9SHUxDuE;O%4XQhXHJB3WiGyGOY4KsdoW5fQEN*MK8i{WP&H4$FdC> zG$fxU^j=x;MYJX%12I%-9rDz@a^v&}7%8qyPls?z|7c8X2*O?^=-c?~M88CBhKNNB z+KiFn5CQ>TBoIjfjt{9DqYrO#TJTZb7r7NSd}`7V`~u61M*e0zw$Y}TiLPL3HTWI^ z!+_sLr*c_envqeL=`k5kJa`hgk;%ZM(xbx{NYH~H>1_Db#(MpKf4??SCszFh?0>g} z0*OQ-6Bs+-ribbRa&bu2z)-(39B}CXKu~4I54Z_V8R0sjzhDhIrSv)Qe5um zEwvdjNOVUGfOaEtpRuV=3d2OTePc7s=p-=l7D0*wG;x@oC-@Q3$jd~LKB3{*J~)_+ z#Ttg5cg)R6qX%14Aa(rlr11k}LBh#_F~HnhN8bAMYyPYA0;~*EYvU_}M&ovoE{v=W z)2kSTxByqjDK)6v!C|zB9u4*gY+Yl^x(wsX&&MKXdQw>m0Od2%D)^*=(A@+t5c86j z6vXq*lS)nE09gMwq=6wItbpJ_#hF+lShEBSyqptDh_2XwX<|i?*a}#xlv^f(Ty1>2 z0$iVIXwqUpfEzMP=Y z>8F+UJLVO5y)`-eSRB@27V9wQJqua`krTnc`!iTnBt~LD1T5Pj!AJ}U-50>%A;qA+ zf^-@X{)iyGf@_9k6^W338nXn$)kr^d(X1yAFSgpp)*!*vsLL>9ANoQ?42_oH$W%iNf*`J*9MnTAWdrj6@782b zVR6~*SnPJ3y6lDj?z;a!bGIUkA_<&b)}rxd$i_lnfDEscK39+^v@}Hum@+{M4G0$Z z2hk_=Za{Px(8pGQEx8stnL7>X#VTr6Btvg*JFWn^Ieo2`;pX^6C6p0*ALv8{6JqAb z&P7u6cAX|#A-ALNA!WOrSDHElRD4EuXL<+lUZn58Yej(0SuE}t0Ah_Y2^S&s0Wv&w z8JPm$CiT+R__G=l2@Yx`)CADW-*1yvBuqZ@Ped*IYu9GfW$E zoeI{!anqd=yoDuT*cJR=nU;nq7U5~oeyb-$o2eH_&5^x1yTK^{= z?;F?u(7cf_$-l8Rs|XeEk=kVacEL{y(MBgI=-4Apv_@!{4{T~=x%=Dkd3y8`W!gWs z+`YieCH$B{uHkYYXEmw#mOpAZpsoKv>Ufe(DYnMeznI4dmTXibX7FJk3qd3l@WjSY z258R!oK0U1t75E{8G68kk)_mYf-maIKcJK z0^$n%`|8gKzo81E20*hwY7GD>HD;Xd0&tJc(dk`zyY18UTDeOr^qxtk79452AWla!`q@NKqgzMI@sE#d~r@qQ><}6<9P*tSSZM z(xT-sF+?bH`foH<865nvYmkIjP~wO9sLM%FpaJX9Ku@5;O2u78pag(`fP*bVJeENP z0QyXVqeB)G1&Q`sp==Q#Bd9z?B3(h0WCwc#&N3EP$adgEOLN5v3c`?Qc~ZCpj6h*Q z{C_Unp3T|EUd6Wm>JISD4cNZ;hB3j%Etdk2n@L^l^u3f0r%S(@X~O`g=hom6FI2N` zYmIDZQ!+IPA*A3vULpxHhknCsYSU2qrRWVw9gG+_G!q})xLo@4{m{{Yft_NFsy#FV zAK7FJ`s0xv?7K$>H{NeuK?X~ZSoT2tbOqrc027*zg2EwTX=o}Cb8wnIcrA!@ZV5(Y zJ8drqJ+W9-(?(ej)e)@e>l~v@h$D34R2LF!=quWV(lkXzztrHR|6tbGb(KoGS&N8Y zKuPOnEtSZ+B7g9NpjHaNj)U6J59Jw*_yvrcipu{kEEf3pm7fs-L!*f4-l`{6#)zoX zozCWPci-4>;)u($pMCHblGf=wFDVS*KAYW0{pu>B8kd zssbC6|3I)jX_7NGzwws?rQ5E({&Xy3j&{wpoVnNX=azOqrm1%%YSRoydy8hxnzeV~ z_N)GI+o;^TCnD$fFA3>VIWaLjyhg@GYnPUx)pmWl@a1L)&CHIy3ku(@soTAIM0@j* zIj0_d@Ji0#*7V?kwd{KQsJ1-w(yh`Hk#_YOpQs+&Xye*;+ih75m%MKjHDvpz4Yx1Z z)_S7~yZv?muf@kr^JDJ(R&?e-a7=q!1^&Vtby?g42>l!-MS3)jqW3 zm%I1wod~+MS&@-9?0D;ttI?NjKizvQa3~RHx!&@zleB0Ve15hhE^P3wfg*WCxQF6i&M~!V~^Z2f3s=qVYA}dDA8b-TSMxf9scn}r-R(mrhi`>F}CHL z%Wp)Vx24a@o#UXJc<`upNC)$Q%Z2^YCRucT)Wl0?m9_iBZkMHtKZsU0>2ar5U!EwY z$Gz!EFYQat`Ce%`AZxSdoxhqqy?tS#%ZRw8L;qMewCMb_(#olqC#9u4=J>5y;Fa?D z#q{J8N1HWOI*wJH%5R-N;*H(!Q$lk2_b?D0OWjwouj-aNX; zfP~-Pt<~nVy(~+A*|*;KoIJ&vGfDfB3fB~E9~=MtT2lHR;oMI%9yYHMcjB+rOLoqg zGc~=>gZ?~Q&lcKFp{Gx`ZopR_mLrzZ+Y9vVQhh zl&skoFS6+JF59=8J^y8P*46V5yB-?!Avb$j{{s6O-haCdEg5iVRGaQgri(R2!TF~q zuWagkVV$Z*fAhL`JjDk-jxlQ=!z)dVTIl@C@e5DyT?lGFvSinWYkbz^@Pn&aog8|v zRXwhw^KaKVI&<>6^BF^TT=fs$`rF%%t7huH6otjVNVI9Yt|wdbNi%!PoU4vE9uHgm z^w6e&_veN>s76gu*Bem()t0JeMGxm#|CLix<>9--#Ewjs-% zcb0d~%*n}Fvt;Sg_>@2PzFptxZnLb@zrFW4#f#Y*m0G;Y!SD3$;}iO%X1u59!=iBVKG1ed5rJ#_`WN~3I7J@?SFT+M{)mA(*lAh=9B`!BoI3Tc$?sr z7)p5O>=Zk~J{Xv&k>@6Ag9w9XV2C>^(1F`6L8HnjD>gOAu0&>LunGdpq34GjY-Xwe z5|&08rvPP&6aW?iz_^7eb&2|ug%`LOy#k!%QJ9Acd6Wn43uxVhOA!X6IM~y<37H4A z4Ei93&+^^ImtIyCw1zB!#;oAsR0 z?k)Sx)a}hXYmzq43!C9~>lan!1udkH4o~VcO<@yZKBxYmSG;lR)(@+;Y5CXDcuft* zy|z}h4K0bKny7MHjhtqXCyykuE>bPaZjL>^c1X7Eh z>t;SmFz-HLWvg?WN@m)J{Z;&Ina6LcWqnG_UXEWWqHdIn*Tm}@SydZYsk8X6bTJ5rJ@M_pq9uPsJZn7e z#qQSbcRrWSZr)?pY`cu#n%?U;HN8=tzt%PpM~AO0InruOm)q`|^M~DgH+!1@Y{9e+ zjeMUK?%2CWcHu$cr_o0?_gnCyag_z*R=Bkme8_)Re7&&Edl%pMkWE%1S(n(g&vKuw zohk@D*2F$wutn>{Khv!rG`aoMwu7qs?TZ% zELF2rbM5P$sWCNX4?9!dCc$M!eq`2w83Q(FOywTk!B#CAbx$=aWXhVJ&vzA6tsATw z)>Gs%Su54%1bBVfaY<0pr)_wZ1qYTNfAQf{u>ZhwOP62uaxZ$(c=h!&e?}f@@3wbC z)|Od&7D^k$tyq<@*LHWzL&-Jakya5|i#t7lz1nAAabAAkw>Q?c=^~4M z*0&pUD)%?D{{+{H}-=btUplNFm}_1{(oHfBWY~M#k*p7$J-tl zFmeKGX~TkAHScU1I3|qMKXO64C-FY@6LwT;ym;c(S0B#ryXK~|n|*;DQIy`@=g7f} z?*j9#u+yvct2a$r`fBWnKNiMS7dCA=Sn;c>{^wg$`i2&5bt)a%IKSz-^P{$xgg=;) zIJfof)#7Tty?Ak6$Lp%)H4Dt@ypg)3ZmyYn=+3#ceX}){I@=8XYsK~ar)%nsxIbn8 z=^GQXt(S=QB(4Z3Ua?)*@cl{0HvOy?-+8h-I%nSYzRPbZ0*_9<)@|YhesaRYwG(}X zU2J-I%n?l)UAoUv-FMgBM$aCs=x*b!>gIK1!-LaZT%K;({LpLeje$oBCUiONeDp@) zzz!L~E^p;0FCSzpC^m5^xF$KTusqy)En{76hyS9$+H`r|X#_bmVtFH5snq4p- zJEDg3*eMg^2A!y}_he8?|I-cE%f7rC>ReR)bHtY(j#bAEzgn_p<7vexmpQGKe(H?h zvgA?E9hB$B-5l1W_Kb&z&NjPVeDKnVdQ%bkkCgTd^uVmd=zbojgz%Z@u85q7$hrFp zU#*k3yVxlNUg(}G&q)p<5dfS-29m zme~nF%$-q}k3fnfu85@~V=f<_P9fC;YCFry;PTN@2d{)1E}q)JI3|+I#~^nfjt-5o z#SnTN>wQRn~j9|Q@O|-Ds18n zfz)&44V5$h|ErS?4ql)CPh)`dk7NKAhtCv^!E6i3;-ZBv2?NIe@+oWO;!2G>>S7Mo+ne3XadVFWPD760DV4tD zLW_-e4osTT>CmBYpOE)2bQ~jfJ_;R<1N;7UHE?GEPmwDvzLc!-2328}#K5V#A z7~9Z)=$e2Rg46114Y}vWwyUb$@mt`j4}(s&=r{JasQBFE8RGVHGR@RWV-jn#MUwfg zr#Q~+6|M-A_AaWjg!9?eWA(4o8s2v7;C(ZC{`n30GyfRfGG@%~kg?$-oh^EscfI1h z;Pc9nCt5#cA4q4HOuoEq)rhBFvK1`{UEN#lQ}*^Xzdzr1GNt*b$fDhxc^7SMr!E)* zY_e8=H`(X2de#8zxy$CvT(o~^(^lD~_vakhu*j|;Nu2Exzi&XYRH$b(U4po*%r^@o8X_;MQU6 z_1SHCpRWcT^{BkKs0!D;$Nd?ZJach=_6<>bPEX&Hg`VB|`}E%t@o;hU((|+U4X?Ic z`(W{@qRa`J#}C%HbPFx<=>2B$;fEzUTZhQgBY%q;6vxY=cXz08l&$91ir`<{GvX4U-%Vd7@9CwB92Z#JRm z^yQ*$BOQOu?dR%uT(hWyRCYaPga5+@0|uO0e>G{4I?1L;mAj(!uFb`d#~LeqM1q(A+3>=EYoA9icgBhU9KohF}4 ze=v6ao)&{#hZUL-H!;nUAlU^w+#slf&X0jmiY;KNG`O`~MaL?I-?uEMCbMg$O|5 z;PoosPyF*Ze-}OcXf+ zp}^n}=yGKS9Yo+doziq5lwBFErKA0mpb&e3M0Slb7C;{Y9Z?H&UyY&>;w#Y~pyq?9 zH^iQVS`VpLgm~fe6scC=7w?t{>`mH0MS2EEAVq9X;B6(7Ar#SA6y|a0D+!QOC4Sd~O`Bj)2x4$5-T`o;w5P}b>@(bc$X@XC=YN0$LpyS6f$&F= zQZNGMQ`goiLbQ5|ZFnkUP653UYB~GyIRybeS!w!A1b(VqDHRFvRFrCg)+_-QT|b~r5qf10IrrI4tN|B5ak-0kZFRJK@^uuqVnvB?YZ0$0d|P7 z8HPZ?|CCS4p>kM)1$KXPJOBu~!lVqYama828V>N!ik<&ZMjJZ(bJ9~H6ht(xuTY+q z>l_*x2Qvii`~zfY9h`5CHfDDC^H9a9>eEvi^4KyeRlqL6e(uY_ldx4~GJ!l@$8ud0oR4M{*FV893gM6ts z1hfO;KNJu|L^#xS0g-$Hu z>aQYF8G%eR{)`fl9S?s-vhdLZ12Q?;Ft|lKF$Pb@^cG1ng(gj?CNV-{auoaCELfqX1?*t+k^=16c@IVb?V_CyeDmtD^8I%Hgz&4cKxC@!_jgVd- zgN(RtfbswA{w(m%h9lryH_;S+#pWTo?ba;oAwEcMdq9Q_ zPrsE%y3=(9`e%B3fOtXEE{y3LsXhP+1!zM`1i#+_fKm#8t^n|MghDYrbbzuiFuhg% zrG}V?_-bRAFZ#<+iIJ)r)_r6rgdaTXmLnq&et1y%F|2h>L=i{6Zi0MWn@9cLp9D(* z5Bh0wbPi!;8U0GRCH@TslL8}R8#)C%hSq~0TLs4b#Z7HN{tv9r{Yh zyBMlMr%}frSccW2qN)#el-0sGCemt2$cWa&Ipoor1;{N)jCTP$90*DUM|BD(^<-3v z8Q}w?(7|U4&WJL{Oz1B}#SPPLj>b9|nV-PVFXn;t3PhtZ4CNvAhG>a+Z?r@<7V7_S zlZS)+RwWEMp-{3eiIgv%NCs0qWdi&c7#hg^5Of9gBH_#{Q(8_e0FVLU#u&;q>O3Ih zUc;WgJ!8{IHiS8m1e)!D)~h$vX1^`~J-mW5_4uZWEI46t>ys23_ONJU8I(Kp14`Kv zoc}`=-&n2`Nb$oCJs?#|L>%NWlP2*QV+HZ~--wR}mAjBCbPO528Pq&BK73mh>M)Sc z0|k08#AB&RP;If4CzgQakKzp(1I?s>IwC^d*vvj z8Ol1zub2MNpiV0vl(jot^hnnx4+Ey2T?pV%a3&$$kOdX403dYJ@Y7O&`Q{h`Ifyj%ksFpw;3Qj^>eg>96WI2EHk9oF(2t^4o ze1`^jZ(m0ybN7=u>r3N!XF`ptg8Bc^$x(6r|0satPyas+N9f!AFyOpJbHosynSa9( z1NZcb?EeSxfA92lUT_^zh*umiq4$s#d$+hCvMvW|D(Ia7$e|skl1Sex&Jvj1nwSm7$X>0<(Z5XSvwMPtx|V@z0jyVnzKQRsDPZ)ZWn%W3uB}6C zN_g=p=IKo_Ym}u{MJn~J2<4U_1`tfcuh!Mk?^9)%_+Z@|Wr-;h1u(A=#f0!(h+g@{ z^e!eLcCml<@QE#KK9!xK3cs*TnbL?khYLdHh3!WjvzTegk>y_b7B=lJ5~jNtENl~# zj%?IJ{C{Sw2`tWW_HV4u;E(_Kr>I3W#A7C~#=!1tyQYg}`sk4`CCPE)ql{M|C!pkH zZ*mhK9N3+x4MOh&BG7|zHyqjC9KBoHiLIc2&Y60**s|XZDHTG$8b#FyNEXPK6z3BD z=+NuJBywqfdK>UuV5=@3)u<}+z|F1ePVNXF*fW~Uo^U@b)0dtytV*Uglv}EjE#+8q z_!v5s-tOaIPiLx}J~*ogDzD;D%ekS`$vhg?l7!Biyfo_b0taBA0t7Xup@t?EEJTS? z;4>NBJDJ2WC!Truc4F(>)7c(U>lD*3ZOx_ zfPi8$(~!^K$TF}3IwI?z!wzM!AFxBYmfVRZm;a=Y5g~}XyRo%saW#ay^vF%909UFf0z~FVIyTt`7|rXJC0iI%TYl7VW|& zRiQ-yvFR=4mJE#$Ne&ZGb7ZIQBgvUcm1Ef8fbfoXs#AtUxLl zZ-p@!GGRL~JOGfbFh45=69Hndaxuwoj!!XylZFPJ@3g`gzBKY4L2@cHEnLD_gdf=$ z)-HH-+~maEGlT5HBLfYrFdq291(EmE1irFXaH&`peH4SR!W#5>X!YQVY0yWVN)GmP z*l>BcQ+O)j$qA4OP!0dT!x}W^ktMvxhPaG??^OH$&-_0!N^m|nRDQwq9U~>EovD)5 zy+Xxl;JYp#w+)ulRE8o{fi3o5F_i^US?W=tD+Ocv7atAjHO61Pa_C@Rrb@xtR33O% z%xQ(2|BMa$D;C&)cUq~bECo3^U#}F5$bWEXk_z~IA;@;alx#)?gGq70P8>Y6?iy3b zF{ZT8F$6tw#XdPWhG0NKxV*P>y1$;l7g@PSdyoi%0L;8ZIm4@Dd~%!eW`#=8ur}uN z2gVv68VN?`40`bMi6bBU+{=;`Dhs9z9%-o zG4OYAa$p=_4umr?ISk?NAov2~WHE%l`|3o2^8(e3bt(e}q*h1~~F^NeebdRtM0Q8+WWo)!gft}hB zSC5Q>|76X!I`HNZG6O(vZ1onjp7EVn3D<>1;*Ab!=!@pA4T#F-~*qn zN|Rud05)@Nyi%KJqs!4`Xrbv{p|L^i&WLxM!6#;shr~@XxPEAEESiuHf^ z$qS{syW})6m~2HOq!5-|6N2ljY5|C(Cf`9p>gBoLC2v0YPCXdm}Fd*VK$WJ^E4zA!cHNiSR%(rlmuA zw+y1wxLBWqigjdRm+?m`0=hp>%EN+~7zL7vQSfA6%T=T+|0)+JH)vNv0K@|%5%Q22 zA(%721~I+^Gt(8;8$$i(AIrSS2MOfAS7tavKJDtD$r!jz?EY?72R z_)tumqmVF+V3KIx@#RPvvVwjCuPWbPyP8E{& zi>Uv-s0@(&NCse9>bI^BLk4_{`ftDhzii4{DI5H^R0bbjul^sb1l9jI=>KQcWwD>K z>c#;ez_`(YwGlsYTWi*|7SMGa8^-g+J!?r>Do;`|MLHYWNG&ZBQ0K#Iuv2jG@eGRDiov9lB~`5`!c!Orkw8j)}Ci(FIInp_c&*fs}__ zG}j~kIbey1j8cT)8nE<653OAbdAXecFjyVDV6h*671guQB?H zbt-_~ph$7Br$-pb=}`_$n|(a>K~ZxrF*Zwz=4fI@NJS{14x=6cW`sm+CpK#7NukaN zqdG)cX9VMzXfwhX&vGJ@$g46J^2~&uz(=t`a6U!t%Pl1Ezo7k(XD0-h59QqAczkq^ zXPSYiX@;%>_>1f`dUIcri`?B)6r(4oGOg4;xg77iDCXbDxk(phe1v z3IKo$gkqu07^Fo{s%9BqhJ6Ag$CC!Hw1GpzwMxb@k!FRV3V<#KbpHsXawOx2$Ulq& z<(wQC=3Y4!fUiyz$UMFMkH&z^3dw+PQ2-1WAXR&lW=|5j^ic+w*Z_EF{x7is9QeI5 zX8&uTj0UCpQsKs}Mqp4}@G5`7iJ$^oLZVM}4A{5fGe>F>7zGbN28d9&_7{hC;0$8z z$R*p#hGPLXOSGgQyo;X(i4cV8#wcbt;xTtiNdW*YjR8p` z4g^%L0ctp0)yM^|iumY^6dl$80~1z}hYG|hKqv>$?$H_@0&<9UjY;zj2lF635TBmD&K{nH405^axwvr4?xl| z$e-8@txlB+MiLhU@E|ep6$~TVD-JvQTdI1x#Sn%3g4}eLs3q1iJ==v$m*RA{@KlMu zea}pS9-Ho|r+f2px{J3ziSCnx)7{--()8_nh2nHYu!KxcummqqJ zuST1pR%<{UsTV*w187n_auISc$%uFDlom%)UcnSWFGu4gFv)C?ARF_wa2mK_G0X(; zbe1Vzy~(_b#sr!V-Et0&|KNQAvj+-bm8gu1Q=}QbC}LiLp@%9#oFABo?ns<#EP|^8 zW=k+)B0%K7NGP(i6A8;{0^kGKPQ-Bk9r8acJz1$g&3k27g}Z9Wgh)pDAL>nN)3e<2 z-?Rw;vWADI6_O8s=s7`6mH%Z4{4_29nZ{&N{-c@+D*qMKtAC9AM<)ly0m@McpJ0qB zkeH3i@ce6hvKadR48$LOqR`|&jR8MK{{LtO{1@_{ZUCsc7?b~8A0N1QX$~b9b?7zAz?82Dx*oSHO-FcDanD-@Op;4HGL8!(gN}Q0)dEtGinIE5MB!Y zV`2&*O&E_|$O{IM{9-(E73<5y7#X)9q8lO-2kD4OOkp=b^=RyasR=5@@K#|{;~nBr z#e_Qmv^T{Mqs)-NMM2DV<8Ry4L+e)+&Vy<|{x{_CSsXsclhdE0q~2wu-fO_MW(W9qM}`u~AmMfnOvLWU^>l+4_XH#LBqQ|{ zdpE0*K@X<^@6rsYX8<+afO-~Coeikx*v}dIJrCL%-t7W>vY>R*c;oq;+KkjX2sI*^ z($<4x#z?KiNUh9Bt-?qJg%TO=R*R8ZkC9rRk=l%r+MJQvf|1&ik=lxpYQact!$`Gc zq_$c8GnOua@-wA2uauUhWt_6V!&m}ruLBAL;0$4~Ph zY3|-B*g*#e&Bo0Xkj%8BVY=m4oOePIPXf}1U@v6R!Kk^H#d&8^b7vfrNzEO#sG(sQ zSYm2f>i_nZ@yX1?V6$NSU!*66uSq||`~}osJs+7RkBN8mgY|#rbw!c`u;xZg|F8D-DB=GsLl6NkB%KCKYDZGcK5gVA5KBD?fB->5^_;Bu zGU&Vp>5c}+fIKPnE+7og@Gb_2fO0E@QuOFg(m_pU2zq;`l9hh|`C|BVWFCNy8Q6e_ z=^(c?q4=8`l?kOE;8$a5zi?R0+YS7RY0rTQKhgzBJvXYIlazg9eIc_?`p;vtV)I=8 zKFQLGbeJpCV=|8yA1t+JkQ!c-X%>9{9K{1&dHAvf?~HnH-0lFrARSOWMasw!!tNjm zW_r4Bo1@=85y7+bWS|g~9UfAK%r~PC&wTR45SvAKN+;0+RlP!b208+2fdPLqzG18{ z)J+MmV1CC)Plz2>f7&!YcE-Kb@TD0Ub(tQM`62PaGWSf4Y7V1@;5t_rj3$CndJ_!u zc*^=l|CNYlEmb-Jc7pX^3&CH3=qzQ++=`~W6A8c~sUWyn#5?)EWI()<&NoJxquwuI z2TTjfjL@T;r-oLjNPHn*A`{yIzY(5(V0>RQwZ!#2VqW9;K2(mVdk^aL0x)(JSU;t0 z%w^kCnJv|{aRWSb)4CahVpazIG?l_#sS{ZLb;ASHe7Q@EGC@CUQ)E_DLaGxH1Wwwa z^Q4sSBz)3buaF56cDORayje3ONSIEJR#L|g6zU4L7utQFd_wEbAqvGe$tPkjr11w} zELOm*B;2tHM#(%aOv~_c@(Ai_<=7u{s8bH)?2xd_zIoAs^;IAmRT^!@-4jb@u<8D zFP%E&0CI6+v^EG&G6jiou=W}fRb!C{??H;tHjS+H4=y|W0Bda7A@+X&{~M5yDVIj91NA~R!>>sM{J6Q80`%aIDPto@s*!xHJ9jS{UgRuqu$i91Lc$3XT zF+Z~Jo}nTBdef6Q{73ekEY`;9`5h5JsME}61_4hBs=<3@{8CtifGQLJ&HY^51pk!u6_9-(0ftcZ(J=@T3zA%$f@cMku^?-vJm^vHhJw5kwSJ zP*Dsahyn>)vMnS^dat`FL@{i3H_0ZO-DFFufE0D3*acC+f=|Q-B2okuMY>?Y0*VL- z_JWEJEQsHknOo-Gy?e7DzQ_N4A}Ve~7rzX9l&^^_G;hv7>()}ul6JkloWUX&mgMItN?5aCIC zK1BQBa+t1_9M^bV4!vH6ub{&jKOumoWKUcoc?( z-K9mNFk9C~k3t7EsKj3dhz!3)@=_3u5@7z_4PIeu5k5T%UF0NP7wxsRdB<8jp6jBo z?i+=F5{JdcGQUPeRlBV1QJqm1m&4^wh#IcIKjWfmU?jxWm>cbm_5?KD9j;m?(E}** zgeaw|sV!U!tP$!qVz;H^3|1 zQRz+qBzmI2bHL?BVM5!Z%uRsk2HFu6wVFIPaV#mFIF@-Lyu{mx{QvRPf?-IMm`<(FyEtOm6}Gfd`&UJQD!N<#yHEEZ%G@g?MAq3Sj-CQZ25=MmzXbG$Pjm zD%}bMDVVB~pdGoo`0R~nuEEic{1TnO{K8a(L63vN=|oo{91OA34u2&?qtECB^vUG! z)5ld)b>tYLJJ0Yjp zQC?GSt4?vdT7i@ryM17<4|ZdgF3Px|V9yo2qY0w|xou6nt3@)5;`UN_c%In_5sYhX za&=-JYy>A2+K(ZXi)!p4kbmiF!TJ9Jhp*9@6b)OlEeZ|B=-}-Kc#aEPUpNN|d}~0< ztFR=Wl@$^$_zyW@0br_FW_I{&$jb)55c?rmiDM46#k0)#9su2XV7B17%(%MX(IVZB z5*u=)A&vGSEg`uE(8)|HtoWX!=rEavA?am13w_=U2lQ&T6OMIxpsqNW{+E+K)=Bd0 z9xw8hu{&#%Jr`vq(mR^!3*STmgNtT0(2C3m?QXZrjo-_(L~c<<1Az08 zKuw0F1v(cmWkzVc8E#x<*L0)P+{CR89=w!Qx`5sfx6Z+C3elzcgh8LFx3mCBWpEe- z5MTkg9TIoVr08&3`oV=|1aCQ5+OkafdECM;*Lp^{=)GL4nFU<Qhv0%UI}@Hrf4 z`I4Jt%v*5hW7jr9VJ{p~qBr0r4T$cUWL(2)q{0=z5DnPNM1@VD8H4to3=vw}!5Ni# ze+Fp2P-&Qc7*on9M8bbaXj)ny5j~n;%~7YrZh*H3R?&wM?s5=;yWDorU(61HZlJ`z z4a^vz);bdsVG(F3zG@5F0W)%$c}s7F=&T=9;X_WUpcpjMA!f>J@u;DWZ-0Y%s??WU znry%ZMMY7G$sQw0XkaZc$->AC6$t?(R>>w8^{CR67*RqRX#9*jD8I3hPU|cx6jZ}@ zVkB1C!bULi3GU?~b+`l%d!;sB1E+VX3rak+09hb~j#oturF8!QO#-iA1Voue4-=S0T&052bz}^ljEup@*}!j74v~E~ zJ(~aE{j1O#6u_cJ#434LATa_|3g@ynoX=f(;i7 zZ!0qx1&z-a*vcf^L@l;~>>uhl(rv_w=2i)0QhsXS?`U|iG7!ufR#B9MEuIqp-)@JB z2>&ksod4_`H4rZt9{<1P_&NsvKlrT9_Ft=> z`0)0pmgSta|M*txk{cK2Ps%9ok$v2H+l|9-?UnJ$Yg=C~vUCi+>fm1kj`SZ>*h3ln z(Uf~0x%AnB+eY-<)iU!Zo%hZ!9(?iW(Ot*q>c6Z0^})Ad%9qTzY~(KU=$*G4zPYja z)&7U=xoYq#upa_yM9FXnDoSfD6)+iJ|&>DCUq&svZ{v+$ zJb!FK-1V+`6<2yjE?$`Y>9$vQES_Kf*N1yo?Qj41$eYWLU95~+y4%wG)19xJ`(nYt z&f?W;sy!v1kpp+GsXKOA>U+DQR{cD9cT(xUe{Q|?_;X8({)$Q%2 zQ}tJgeUDxKn|tQJE;!_$`^k#ER}Fq8&8OM2dfrpRCTb7eaH#*0n$=x9YC1k#JNd?+ zk6(U&#mFJ;Bd5pq*d1%{cz4fltMz|un%~d9;Or*-^Gmv_)fYO8-@0o0i(?9#=e{&i zF=q9M)gzYrCcioQ-p)f`&ob3b%DU#m`;OG9XICA)V9}Qsr5>^LIKF*O%atF!T(CJd zanALJRCm2O=Ep-vk8he?+4k?XeHS;++h$Q5_-oRH$JCJ)6*) zUU;zls>5$wHLbYo%LB8QFWKL};<1eh9eSdy zEKk_|Me4cvSD*XJv?u@kEdRUusy8-Q&CH0bU-QeVM{d8wIOIt6uJ<0@I(1M+p><(q z=D1juY5I=K%h%i-e`W9Qa^Gyn?N1%v`R?nP@nhCyZ(RP!?j7o`)X6XG*z)X+*4+aps;bL=yKm}(tv#cT zJfC&ejMvW!pQhXuf78MBFa7r0ud7xaZm#p*nZC4r;*OpB_pf@VXw*kjrVl)K*^0-8 z?c2T5wQAZJh0|qeS*7dU5Vj(<_=WFc`+cF`|ADt6&Ud(dNkM%>T66X4(oM^j_59(} zHN6+F47+3U3kx%bUwE`f?TQJT?;XG2>^V!*f9HkQJX<&clvJ+SFPU|{n4{ivj_eA&ZAFd&)!j+s2c99iu$$3tZaXZan#)Cw?Al! zJ-qasu7i$U>-OBS_SJ}wBHzz^zw*mx`Z;$O`8;n{Hm!culUu*{+?OZS&g=5MH!0J; z?$*L_>nGOa_tUMf+Q03`%71^qvf!?7uRZU|$3NW=f%bp583$;@hc$*>E`*z^x{j2x zDl4PA7aP2I_hq8DJ3Z!q@eqf7DXjhGOZ}|qk^fRh$@+07<$Rv z5(iZ>%^G@1gR9lcw==zgJB{%Air|(?LfryYPNh(SCtkb;S!jbCBNX7dpaUX1+#O&% zc3YD=b(Z){?wD0vlbjGu8C>DW>G8S|?~{PqnNNW=pEcL=&ZJLNvH~;W%y$ zL4$KOJ1Y5@wD}5w1}8Z$C=D)fk0!0wDs@^y(#H-2vre2>L<;n$p3 zvGA+@58VCw_7C&TVfVaw@6k>3N2OhoGk3(%@FTNd9Wu&Yzv_|lx##XlBa_jK0gl+`&8Y@KJnV9eTw zF8prm2m4a)$^Pbk?`zqQ92+zKrCa8feR+{uACC-cF+B6UCuu3;30*_#!VeK`Ko!-zK*S* z*88~ok`a#%cz$@+70uq-MV*UkhrDpx^7}7VU%hDCp($e@`EdT|y&2Q*J?A5Bvg)=$ zJNlmc@ZI0d-7xr@%>FegpIbLw`}7UgK|{XO9xU-c@p9#%!FT+${_>2^kN+`m$TjQc zDSS&V{_@J%&4bQ+ zfgkjnUA-Z^C1F@?>fFgytvAfQVU=%oSV_&V^5Xnw{bQY4CIVK3H3|{fhLBFSfq) z(1r#23#?B+>)SYXef2lGz46u!R>+>3`1WE2qvX54*nVu@T?b&Od(OzU#{TtCRoe8a(;prMqr@ zbxp3x%b zSUm8SukPFY{{Fk$VxQ1#sC&A2#nZ2PFZ^xC_+b-c{`L9y>&@+tyn5YJ2W%y8-Me?( z-FK-PYQK5@?)>=Cao1$d*W7p8ADhNEUAOLwL5IG6`s%n`*SPHMFMj?0=+qxxTJ=r# z!~3UhpK{me_fy{5f8*4VK2z$^gdLx34Yym5Jaf;_3+B(7^1z0E$KM(DN?i21)pt&u z-s7p|uSOL;yH8nv$wz%|zolQw?f2bno%U|OjXR8v>)*d{nc=U)GgJI$AFurDn(=4d zar38Z7B7F_c5CYV%k70tz9p>*lYUIH@4Vx)n=U$c?l*6Kc*(vMTX(-Za5h?3;La}z zVkN?HjUj;}+Y8ve1Fse6eTgZ=5RsumuwFA{esEus<7;dpe0ccY&I9IMBV1G2TnK8w zT(hD?Y{E@%Z1T3FyA_6ikE8Viy1`&Kf!y0@yS(s*0XyHdxFGg0;;my(tLUw)bro($ z3d}9`T5Oz81}lH7Ckbypc5>H?t}ltf9IUkDGJ=?;xvMtDs#?Ncy;&)yJob&$O;2Km zkmIc~Azeg($7&UU8AS|wK{JXe_=08>bNB_#C??|zno-Qo7c`@ou8-4*#DbL&XJgPS zCEVe-f030JZ2kCF5ZU)r&EzT!*vuJ^UT*xL+dVv5$u$$A?ni(C;I7 z8KD|eeB{B+@%;5wKvOLLOjG;={VF_CV$Z)bT4tn(FTg_^If8BixbZ;1|4s0?EOxB^ zG7#?)n*;ypsk0*#3c+{>8+aIS4uM0}#M6i$c!KLUTK^3&?TL~32@M5BwMve9uZi>V z=)PpS)l$rtY&NsnXZIyF>8W%{d*~%y>9rN`&a}8>_$jVU4ZUP#wkfrME7@oZy=1;d zY3DewOBAZmOX_o~+WC@7XXqv44aK=sNp90PnnB-8*(f?8t|+#K%y4UT4n4V%-M#>j9A;2k5OhyGvX!AzJWtbOL!2EJ2su zn;zdgRv9*SVRUh8c^j`SWtk{THS zcYW$=5|UjxG~^+!3BC#yqL!JzD2paS zm=)OxRTw@pRpI>wTx*ni-58ZxIS&5|A=Z>?jdF|%{i0QlfeY(#_&Dcv2}=7s;0%O; z{i%S^2i|z>NMu2gNx-Rq*herZJDS!w@RO=gRdCUyM6v5>LTznDBS$n|tk+_D3%o3Y zEknPqJt644XZ`5l9VdOq^wfth;12yqcgu~G*Byy95I;7Ggdx&Bn4}-rkm^ zRJj4Qs z*Rv(q42+eY&cVqFJk?OFDv1H2?x-l`drL!ltI7eF0Cd}+!EhbG)Cd3)P7mBoffH0a zpW4b?62x)&PDb*GMa1iNH8AcDNZgZ=r5Wp|$cH*a_FewyEi|G?Vly@1?yNAVvHl+( zu`dk%JM;6;N+9BXVgVe`m4WO!`9cd|38VD88pK~fQ1*t9?SDD~1`aY7WbJ88(G#`* z=}2g@Mtmh_wPU|SvXmqHpQtgWQ)o0}>ML*myY&LUl9r*v00wP`bhE;$0Y$ z5<9B%a(Ihwsv;rFOPhGpI%1r{1OgipSmc-`fZ*})-q-074wKS^=n@+u>?A%DK&Aor zi6n)r7eRVn3a177|0JUSpPB#v?9|6k7tMc{uw%2h`7i$BgyuhikOgy$k)8iq0@gdx z`LD%VlhiOq^Iz1MoKraenfi7+{}qDyFRBnL_Q3fs&KTGY-OPWNRWScGF%yIdL@|{T z96kRh2!~1TDV_fj4l@6zheb^P56po7{Z~XRBYS*oS2fyf)CPJ8q#M{d?nD>9B{qu< zaI4lUuM%g2p(e@^^8T+OlQ_sOAUpq=q9;24H5iR7HH^{x7d5RaaQ@%w_KzhqH1rFlH6#G2{2~Z^L{{;&twuWzjI?{m{C_em;zKa^6Mx;3=LuFaEbJ_p{=F^}5aUn;0*D?1@q8FJZxUHb|9Ndp_8Cy)1a)|)y_ycNtStqAy+_@6KO8XDdAWs3VzW?bB2hTgSbC3 zaZqlrt;FT9A#`yF+!qwM*a){_XsM&QnaJ^2nr(z>Ix1$eL40Z;SK+8cLbtQ2dj&|MYl!g_lLq6<2u@Om1ftmrzn)){X5S6G*ou#j3Cl8-nHEVK7zm z49L#91fNHd%!f^=Cp-nLy<3Nm#*F2kyhCjG2Qxe|F-!i!8M9A6!#`)eZ%=}jF=leo zRABXE{$>Qh{rw-c`v(k;8cRIgS019lpH_DN|H=tjFom#C(5(Za{!t|R_%BOLjQ+;c zE1IiePlpYMEIyQe)9Ed*qiM05fq&lS1u`PL%laQDN@r-|CG~s=FNm0MfwGd(O=(S^ zg}?UcbHecnvMNgG_9Id^rt#cvj#pg7kSPV{C4ptsI_7wXb4fa6p$B-u2Aq^=GSLBx z%Z*G=2GOcX!?h|sVF5;uk#4$d@C)$$A}aj}1_*(LjpG9iq4^w(P=>CO7OM{j2Reaa zCDV_ryh5nX2_9gOs8!%v$DY-L2wL)_O2z>fV?*i;P8|~E>W&9E&;PUedOza-{-^eT z#h7@-xLCEC{RcwGj#DaChB5dTRs0y0Lh~Qh4762+^ipQ-Ml*<2F{pr819mAp8Qow) zhp~lMMr@h*mCTl?g^P2T1gvhL$C|?bjB=pT8(0;fJqehm**{Ki{lFj`BBvuRfRp5@ z0ve$C&#`lc;?2~3B#(gT4``q}jCHAv`7DhgFnoebr44wFZDv4LSTMIL4|mN5qZv^t z8jY={rXnxnEsOyeK)1*o>au_=95|9~&RTC>5-d0H8c`19*vWiPJUN-!hZY|+`o}%Ytvq?HxcZBTJUfSh7U6Fn*Q`3sY~*PGjEba$^icG$DAQ z%_F7RticgmV`|b-Xb}%%)GTJ8c;DN(`d{s%C1c5G!hG z8Z7T9BaHeQds|X8Jyj%fQxAsuKL&|blCPY`-O4qZ8F(NwMjB|^Vz@0(;hW*iFW+XS zII2crs}l%|8|+QkV4LmCfcezU22HV&Z$lx_2E#2kFBxrt?2FMoY1xQ6k14`!xzqw6 zYbNG25^RYrbR)e1j~n@RbI~NSi|Sl(Q9_p!bhxOR>250?+dS<*MfNY#JdqJO4P4JH zvdrx~FEu7cLHZVsbRm0p!?g+~joW3ncGI?Uv)c)GOziw2cSSH}@z?_cI|&fM(z|!F zKroj}9F$|zeC%|E9Kr*J ztFK($ME%JAiRKT(Cm>gvdTpBBjqd*=!pg$J$|7c=v%snOnW^%RMwkwD?1)?R=)Sg9 zfP+Xc&qfGuFdV|F$YKn{_({fxk3{?t73X|H@V9c11p#5Os1~Jou1MKTz_nek2Yb!Q zgJ~4R51%%v5!}E8VN)tXzzw7bk)^Ey6Rl*MD3g$+RSC6Er&k!T@lML7^f%B3B9H&- z7_$(;R>PtVcw5vq*P=<=nr7Zw=Z_;upb_^aqEXW!lvPKNt9uqKWdCkbQ~~KrCZCJ=8eEb+Fz?R1xuj|2Yi%zryaYc`{rMP{A18C<9WQ z=%!m{^+Mqgl0gjwt;h_@s4&_MrE`D+28M47g#CfKc!)kP033)mH@u30cMNb{AdQCE zwFM=lm47OL#fcmM_&ttBkMLV7RGOtn=r(K)w`l;@p#gvpBn%?*LJ1*2RPgu`RT((4 z8t>)|*b3BiEF%w2&&yV&!n$=#l6_FC!$WkeNYvxW{w09I|xO? z!N>y?t}4|9^Ivr){#O;6GaQuVO#Ck-4!m4or4-5IO#H9^)%agwVU@*k7OYcBX@WIi z@8gF;QX&<7*HT%mt4t`Zj;F%)>a;PLX%;HMkjp$;po!ZZIaHpwdPWmxZ@2~AK~g#l z6Iw#A#~Q5k&AwivQUNEHh7c3c>JibzVSCi`v4T?|B2Bjl-gP;0Z1HWnTcXhVH`Uj{$p#8#VGuT754wwGx2|6PMnGV%g6r> z!f$`-mm`vBk&;e6VXfIi z-eCy=)Qs7>wle;hU~+I(lLiT(N`sGw&*8NTf0mc_>IEN)0A0ush=ZEaFj^Ld=@Cwt!4 z%7yAwE3`V~vx^@dc;2rQgz8K$3*o%46g1u*hnvQp_a)mD!g=3|(E4~@BUvYPDsu#~ zX+0%JR)H)|;>=Ets3&)3?{>&wG4f}2kpS9GO97n7nLSVdrOov4ucZWV0%!Iu&)-c# zcAlC`di)fm0ECD44uk*x_kSWUMAyrM!^&?9L)XhxWYwE|x73mE#@%WaeFtVvO)=KD z0Fr=wp#e1$AS(S}+VC6c_^;;Zp*=JVK8F?CTamI`?6O~ch8uW?wto#~&iP!O7=i(^3cJUJfBH|MF!$9$f!%PQ#b!m~o!pGRH=9$^Wuhv=ftGJBCp61|B zG^e+6M%!XV8H<7lW^`4n;?+8XhS0gun;CnzFJ*GYBS@!J1m$Gm4S-u#`X+62*?LCa z7iJEB5$1l6{o&t4JRh+l;^l~S5pPAjAMtU-zKAa(zKi%d;@61dkv$^&MxGZrDDu+C z=*TN0<09iD6C%e)W<=&imPA%WULR?VtdDGpoF2Y9(jU1n@`lIcySDd;a4-D@YJ~Mnucw4wT+!0!RHZwAuuiU^%uZre}6uFp6kzp z&r$wd_`JlQ4S6Q}v*3-ze(=YR-04pa-xZd4ODcS7Z%GT^7S^vT9zJ_@Y4E2SKXVd( zBjk_xwo3tJM|6z=Tkg^>4fN!qF31y^+ogivw{(q!&uv{-K+1?)poWnvx_UtV)~-k> z`>wwezTNBx!tNgH{VkAwgWm(spXG0WoTL5qP{XVIb&&IeuAY#h@_XU;DgGw-UE}vb zsSRCyq126CXFjF_nKIfkbr6&2? z;Pd`2*o1n{?h1#`Azl68bCw_U(w;v54UltB7wk-tO1~RY9_oU4wUJgo$NW zoj)2rll{Zt^K*X;e179cvU=2yd|&)xn+`25(P1fL)HuZGX}{ntSL5B$lHXM^7YpD*~YgOr{A z6!^9njlJIex}Ys#z5G`AJ=%XgeA}-J-UTBB#!S!rt`YEgNf)YTudXQg{l2cD@Of_+ zw5Z3Zu1n!FtP9%MDFc7Zf{e%S?*>G?(10O;9%T_CHlFrdCw zD}tPl3oIQ*-r>F0m(ZY1$>S3owgj6np(a5@uSyQgr? z|2P=iTpLfw8|eR|<~V~H`wCB3e8&F=h=$Sp7f$ik1g9{eEkWlk^HgY9dTT`vKE9k@ zGn%;nowg2rO5nZ*rT_Gt(8OLWL6lvQOj~93YTWOqp;=MpVg@+fgvJp*3K8U=S8UX~>I8rPpY+aQ?zYrA}Yfz@00p^8A!M}FnoJj{RTQ#%3iDBDZ0I;Vw^79~2QsmbMAn zSRmJBLBy;B9v2M+Fu=AI2^muyGwHzPa02a>Z3tgCtzg0DZ$7p4uBUd+!TtDXgs~>vn z>L-7``KR|@U%6|f{ega+Z*RY8`Su4cou9V#=DbnoT&&dZyIEbmA@A%Rn;%wg-+j%& zjdu+>w5@ONuOe38829LIjkj~~gUipeJU?lC>%E^;+%V_&!y~5*{P@YbBW~ery5?USv<;OZGHXw?L9Z&f9>qC*Lb(4H@}$P^40CGZw{P&_1N{B zw|^fKd*sb+cPzVdv!iOwfX~WPUKqW9W9G8A$Ng%%V)GquJvQRwCvTdSbN=3!FMMe4 zvL9Y*Iri(c%u&CrdUQtbb1#0rwR6_xpZ?J7o2g6m^u77fvF|;*^z-VKA70+x>)I(- z-|Fsr>)q<2w5hM|IC`&kd+ja1UNArPw!-(HQaAYKWM4cxcms8&{6Ial^_v z8+7BQ_j~jFJ3qHCny!4|SNGv#LwE1EVZ@Bai{n<0p7s4Ab^rDPQ)bw)bv0cS5g1-Rr8B zt*S|T7Tfl`_4cUpf|x5dZe4L_VXxg2%y(6mrc5a=8`ydO7qf@We`@a2FU{Q=HFeGM zCsxG0wsqGdfBYO@Z(M%<^E1lE{Og(FdE1hz`VQ}RcJ%Y6N(O&)0D-EyDHOxQv z!Vl)mdTP--&$p)}J+b&+WnAgjYc3hSe%I6i&By1AJbT>qvNubgd1CZckMG!gZ|@$r zo|jxZ>W9N$R*yRH(Y5n$|7P{K&p!K7-3-mnM{~=*|8`#W{^fT)Wg9%}xh0FI=I5@9 zs~A`@u*ZOF&yIfKfPQwW?)GgPYqyWS_2mWCF%LXF>OHr%#(0x;WXIN94*08T&ilsq z+Qjx=&y0L}dR*B>Z-4Tn=l5~nEj$$Y#-d-Qs@AP5eRJLHM+V*g#BX5(b6@1mg?Et zCr=o%e8`V)zu*}DNQI*5!i)pnBa>f$weX`>-;1CA@MOO=$DbbqBO9iSbLTtX?|E+z z8QqhwYqURf&M%R_Pdld1I)2#u?3JHwz1u&!YQ?6FgJx9jvOK=>`dxX8D`)q4I&ahY ziSYNInzKG{Ut7DUQ)O8&c5UB9$-k)Y2}ApTc=!il;U6gD70ePPwZJksLXC#-JxOny zI-o#R7<4LzQc7yy?9=Q6k0Br=b|-b>0EAo^oj9P-jZR>FecgE13Jz5bVm1}kK>FfK zG9U`mK!qAJ@vvlAf~~p_EZ%5)>0_>~1)fB#wdC4?E0NqvGBjMpd96-%VXkjgH72+U zDvS7pxGb_s1Rz~QEGN2><8RR@Y0kXzEX0;OI5zAS6Vf2g&CE4a@Kw;4NvaUyO#hS$ zVX_xx@s&~KCv?;{QWA@=B*zTl1+r4uf^w09Ah;os8^0tNQ?TJ1uQkN041{xy9=!xG zUQs{;#*@_{Cq_n8l(a&M+5ApnItNgw5T-Y=`HS-e?NYGm z4Z`#)Hr*^ppTPB_N|?^ctxA~A)xXtEzjp#xf1{30w^`ZuHVf11*>qcLb5$7^^WNrV z)6232>0H$HT$dm{p3U!y7o=<0bdNAy%cLWFU$j7blHG#4RsWUo5C@d3){^TirT%ME zt+ZYY6aJK_|5OcvXc;n_A#65S!)Hp0mig0J{TE5oOSE^2d<_m9WUu9^0+S#_C(@WC z_q1S2p^WspKOIt?bArPIsD4j_;X$sMf=G-R9zi8EZHXNP$=O=Ox9ZhCTvjpKe(5@OpLgfa})0{qOG#DIv%cK# zZN7Kgp$WBPIxha`qm?Nh{i=csXFt@oE_znFs%z7X$8URY=a_j-^M=p-sppDUjCu2S z9$UM9??RsQ?8u-|8cfXk1C$F1( z+0Yryt5)qk`^&>WE}1bb^5Domx3u|M2FESvcRcgvz7Jje^rn~hT>IV+@3r0Xeanw8 zU7oUQ=ay~f*zYs6zF5AkN80xT=U@ES!^Q>IezUW+&$`7gc~(|T>}h}Nfz|<2emwZ+ zzYlJnkPw~sRKeChQ6o$r&r_$oIJf_<=3K9~=fCdS_~Ls{9J*r8#KYfTdF7x9*$*B5 z;j+sjn|@4c`SFvtUOhU|e6u$0-MKy7L*G#i|5v~ED;^p7^uu3WI=p1TQp?%TrDR!V zn*MnC*bfmO*O#Ar{P975?T>b@c{S^%i23U;=yS>Jx?$BDcU-3Qr0sn5xqEjF*gyNO zBKxo%2}_oG6w{aNN~%guQ{3hl@X^3q?z=e8F(ImT>#PgZmhWc%_~_MlEpNMj&R33` zvxnV2pyWBvrE6}#s_pJQhok#fe)@&?^92=msT%JeII3~+tj~5GS2eaKl`L5H?8LtI zym7fDYr1UDys|O%@q>d$T$TOM=xm?2-=i@fRP`?0uszOtzPWMcvx>Bs%m?OfJWHv5 ztE=zCX`|IcpG>RuBt!9iE8czgkapoj&#(6lj=cYksEa&9*FHWYZ0OpxL-+pF{-FOl z^PxL_owZ=2f1T#e$#oChU48Sq;mXs|&t(*OqYSx<7V~nJ_hB z!QjHDZeRcSk-hzI?*G^Jd#21xFM4IlXOAUa^5r)%B|~p9+K;_3bH>^|2dwvO+quPX z!Tuq0mYw5V_ea~hPrrIdKQCtGm?!*yZSpK0bN{~R@5=9R?2cX4diAcnoPYmv{=A;{ zgRj5##rJ6fCDhI@PxqA7qo6Z^p-#7w%wVQ=!-fuqb_ z>O=`nY;4H4A>LXKaiu}Y3`rzU8^%3>;YSj%NH#$TCZ0Ca|pJ-Q3dNt?CYfSe>lfI2^ZcC-p^=x{2MWc?-pVPvn zTbi@@^jsC2o|;=krRUYN=^5$OPCkD@8Jli1>FD&@XgJzn#&WoDs&$%NnIH+`YU%Mc zHMxNiEF0phfvr(Vd&t2cwWf?eHL3(<2e363H?7055A2%G99Mgr6> z+L1khmYo@^uF`5f^p{G-bXc9;YURjxsg7a|v}h1uGFzY@CW8}s?80a<;rqV`Ul{xc zXzTyRxG@$G< zf;L%Tg2OEF!0U;3U%dTP!B)!h`7+x@p1IxSV!XVhA7s$;?Xi#I5*#g&C zX?Bkn1p!IYb3jfuC&P**&d7r}$)y_B9X#cAHYa&4?Av5B8V^_l9EFs50uL2$B#e*& z>98<55cd`HAiP?VxEKG}=Bw8#6iRKpf?UoZA@dH{Opt`ocI3LexJv9}h6x@@xE;A( znBKwenDK5w{x~+>BS<%Zl~4>)2rG1mTI~_#wIpBQ<&b$ zrpN2({3fnl9$`ACffxnprCj|ph3UoY^KHU(Gn<|xOmAS*%Y^AZHa)kpA)V7$O3K}A zx;8n5O0VGBTbbvi(koNg{2o&aoo-^&jhWdE{PWFOY=|VOdPU$oX)17Soa$!2xwHjf18=Jpc zn675iUHO7~8rXD)AichjP45t#suXnKN5Ilrp->K)6@^)dmhRxq7 zNN-4E(^G`$+)&d9(>eL4Tj=*Ta8tC!+?dY~WfwOUt8F$qy`6op!IPS*b1<$cB}LrQ z(_|5(8`=D&MeTHcZt5xwZYtea%I5E=^BDQ(m!`An`tn9Pos(P9gqC(be+@T9bJCJ? zsq|*{Ik{d7m0p{}rW=Img~1Oq5}P(UN#qK7mRwjTL`T*1Hm?oNHB1zD*#9p)!~b3D zgs{@I+IEKjn-9HyhX4D2oBun_bA|x)3;`&@gfUwQuo;<+gw1oeQq^UeT@jq1O&`^pHh6e=BIVJcX zss<-BJQ%`aQh8~3SP=Y=U9XAfT>U7`_=(p zZF)Wa_sZg#O+9V9Ck#lr;-a^oPH~KV_NGy3u0NY5m!8#l?Sz}YZo9R2seeWJu=g9E zc=+RKKmGLFqFLWfv+W!8Xy13=u82Ush!e-JZ20O(-0zLUHcoo|#vgi>`ZYt}Jg@lk zthCu<|G0Ghpw!AmT{KYu9sy0o3e$3|Q-wS4L2GtW|H zCa%B1JfM2?W3lgF-tp~i-#&BAm2;kcXV3OSkN)`1k##fP&Mo|Tk>=omf#2VD=j&$; zxXHUc$@|m5?~B}D)$R+U94Nn7K^!RmyeGA|>8#PWos+oY(wD|gIaqymc6Qk+xj2<^4-RHd4p%f zXTQM%`s($XxN57jtvDaVo(ntQV=_u!}uP% zHg13B^_WH3civHH?CAGK#rN-#s~1yK>@_T^C*O-W8vHeb1baqu&`= zlmGQoZ#Lat-~O?)*tzmh$#&=3M#swhir96@hb!xsEv#{@=yO}+Ti1VYvp%!?+o`&h zJ0I8xzdYx6$5nNkYZ@*c0u^~-Pfd@tRWEcbnXz|J_OIv6d2??;<`(^DD_sXC z&)cy7ukf2XwqJQxa(EbU5QT+l0pbSsXFZ?*iM>sFVU)Nzq6(fM0I;DU*T8F-<#`Ed z&00Fa9#hm%TAFOYmK{Y=NofsUFU1#M?sPg28_+M*i!WS#jZ&*rVLepp;)`pcf;ctO zyB4|lQVZ)43aw~Mx}folM_`{8aH>g)7Cn!&NfrP|?hOe3uRvHEV|Va3!QG(b)t^`b zCp|d$Eeit^nCFQ0(1880+kt||1%bbz03ad(m`(=)sPVmOA6@__g8vH;K#Fjt5YPW$ z_&;ul$72UB&i~)6E4vc@p9c27%trPoQG%oXVFRIug_4+DBs3r~8#3X^MA66s+<_qa zGLA78%pZsLEIVyc_SU=4%;q;F_zh+bZf1x+-9$>lMSEE9GT$W zvB_a>Pxh3!ssZtW0%|8ioV<2`8f62ZMy?8M%vI(rW`elXvXOK#I0%*{3={%j4MAZ@ zH;<_ug*-9Bl^|goM*@|r7s|=X0>{rQ0BAz0xd~xCk~FG#l^(Bjfo@#IJ{Lp|EY8aY z9Eq3sXUoQ^W(GnYT-_p!5ZvM>dmz}eRU8RY`mic$TLv&;AYS?ITL<7X08e?Kju^he zK%j&*m5%IPVuLUM9so@-bTbJ11D(bOYR31xj99t}JZLndqH?_;3gyT~ETke-q8x1H zk&MEgHxwS_>QMbMGKW9bsq6POhU zTKB&x8ih5}0F z0kvpT8TBH2Mofw@A&UdaNt{hs?{Nrp9Te3Dj!)qA6mQT6qkJ$3y?a8Td@z`_RHbY4 zh@FQ~?j=P>DffRP?zS_xq=@eowJt`KVBmyhX6}B$LQJ5%omn}wpzCxA(O8MVdu((9 z|2rNb$T%tp=I9diN*!^-l5y3SI%gptG$15{u;fxi1Nfu`uJwR&r9w8QD}#Xy`}6_* z%?ns|pqWVw(jhEOSCHC^uOujL0vsBVrzg;8iFz-6puwCJ7(gZsWJ?Hk{AV{oL!kffA;$jS zs||7H$ZVi04dW9py>4su@j#cURk8#tlKt9;qUeBiT+v=c-G$U=B}9AR@C|3wZ0iY< zqQtYWmDz7t7NY=)4m_Ko@xdFWH9+gJeuM7ErJE)zJZ^JfON;DXfuu!w)J`K{!C{-q zQq6S6YdsD8jZ$M}LNpG>2MYB3`+GbwDg-GN4B%Z#S|cOn(I|& z$nz8W1iK4jroirBryQDUu6Nr@}H_ZQBp*5Pa&8OKc z_C~YAXaR>)@SFjwh_uC;wKy~OMb-%Ia;BOcjucy+xy9~sv*u1r_Z8$pBEuS@S&NVwGJD(_~0H0s+z#)O9_g# z@KSc|a=W*Vb0n2B_a|Ge9MPHwZdua7{_eHgJSG>29~_ch9-9@o*pi|n@Y*EZcqDS{ z3Yp&6844GWDkvA>V->84E-!loaDsP%*MU`f&_SG`( zU2^Xc4T&tc+W~jpI?j47H=L;Ug%C$pTC&tL&=$mwVlu3te~_UQ-*usD5dIE?zE7YA zN&s6P+-7vgmWTTm{N@^+OkK*%4xez&5}`Us(X3O zcVdM9_nt6d|A!y|r{?DjgMG37Z+r3@es=6E&4%)>Y9lF9(iqBIyZdi-!&zau~nQ60>|jEj_MQI`K-^gPlg zN*O?ajbJ1K-A7e0s%t9wu+-Ug*vu?(nBgQNy#Ghs|L}i^YS*i{n>%IET{l{ejCLz1 z{7#!gupx@B7Xbv24QG!w#+L>yedsQ;5v^afIZX!dp_nPb?f`^=lnxQFnnOEN z@$wuD?I4m1zn;SdtQ&1Ag8h%WC6*}2sT7_hdDX^NV}=;y=v{@aPMe6wBH6<|N3I8% z{f5Iy@G)30K7!ukL{%+vgW~9J2d^sFi#%R8qLtx%s$}#SYPUtzW#7v?=g$_679(1KLEtnGz0*~sf15F?EK z+s)h2qlnjhwiBxCFju=sCYCVNyT9A&&N{$497(OPe;;rvdG)g=ke5@jv7z8YQ4 zF?Qjrb$G#*I!T-1dJULW(7f)f78LTOBPMLG&+R0aY9tp)!F2*lBx@27Yry@-cu+ch?*G& z^0`bPAMwBlDj#vi6P8cF2(r|rwDR)tw1gy|e{ckWe13p2WUP|hazouR0iwh#N3hf* zY!D27iiApbmqI#0n#;nhq%~%X4eUz3YP<`}FV65*w4U-SFSZZD5-qnFiZmJaPxK%o zn9gl*{UBF78SzL3-)w@w#u(vbJ7MntTMF12Y@6`8Id04Pyr-`&6)PQd<*{XMuX>1>UgYJb_Xp-j#@UB0XS&J)E4K zX=o|5D&X)ei~bXkx5;n*XmEKvE{h%di#2p01&p$xJL1V<4(cKe zj*?@(xv2?|lo{ZePgF~6zF=pBuhCHk8#r`RQnV_r4XaZ=TZP5uu({bU=y4?yK2pMw zEHUB|=6@-Vr<7BTnHiUzMig}wTN`QRj2ZRZtpI;t$=ID6;j%P9bL?mW zyPP6nGVR2f%H8HB%JhaTS8^i+ABdPC1`kGq3mXmjq!89Y>6Y?#C>h65ynga8L&Jer z@MZOASan_Qpn{d|!%}8|qB|VUU?^DrLJ~B>sCFl3=_LR)mmz>8M+@2cFETqa-ApXe z2)|7jujLnRG|^HK(VorZHak6d_~hHXbuOS##QoA_22gK-D@l(p*rnyhycyLV4AVA@ z6$8?O$*O)>L4wHneGghUH*Tw7A;FiUfU^6gA5IP4O zX9!0bYyrq1jm9Yx9ovblJJGOefm3}n`zVXc;c}ABw->4hEs$4RVr~Gx&ZVK7ZtYTrY0Li90HO#w8Zku9Pj7R zmIQ%<&=uEd3H${oHB>jz>_di(Je#u?!r)?Z*U9$05_1+y9GIJ{rP$z#bqQixF_aO_ z|5J$nGic$qHAMWMtGTj@yX9|7X*x9kFS-Unpd?ZTWH$VtHUtOn0Q?^b8R$hP{=|Uz z4fWZsbRMNsUyo0HA;f)35I+6CsQ@FMEgc|L3CA#o5`lBY--zHodl3 z$%Q9sQYDDu%`}HzQp8lzegY+hkkItdONs^)7fq`BcGlVQ9Gua{R2-aKH9$i8(juy4 z$qCd}6<0%-qR9no7E)ih>>yT^%guHjMDc5eSv>8ht!CP7)BP)S4x|o5X7ra2ct>%Y}rI zn1}kWP4Z28(>30QHsb%%k1&*@#2unq2eAVoc)Ed!?8PBaTbuQHywyC}0ya`7x^X}! z6?=;fz`UHlC9!XV8;r+Zjqlcyba>?o25k&x2@eKvQ|PoRjaC+^Y*3R-EM!=!H~aJf zPfIY+V-rJSKrlYMz>C1l6NEwlpraK;fLu1sjp$Gx6nhyE>{6lA0fvx|(HK+)>?RWs z6p+*}7$}t$z)7PViTqg7F5nAu6MHWkNK6j-$Q(2JbjAziWh3&Ek`mIWIEW`2S7~-^ zUWS`RZ2W=O!OcjX1Yr6EBq<3N0CkWVP~3~8M@vxPvtT?TYPAy(71AR3D7M|UPu9hV zfjwwcc+_+L|C2U9o`*01XvyJvO&rxVgsA{T-EaeIVW35l-4U?L02yz>?fHC5~D+0ouGGa(wA;(tzM-y7+`=f zBSU&zNbL&Dn_)8{m6O;P_&~CqwZ#JI6ad8C>uNe-!30^t{z1X8>(@Uem|~$|3XxzS zYRmrt$zUBk=xqBNLk1R%U@nH-aL0>2KmRw?Jv`~%XWtuQl(1O;>kJinpUnu>Yp%?e9pNhZXGVBCGA7X47(EAwXbm6Rd}@P0MKCe^dk zmeo7NYhKU=9XV>M*!YiZ>24AJ!K$Lxs5Gn;L!T&22XNJpUqM=Q1|;^Gmkj9-7>dEM zBX1ds5CZ3lPn7tYL459n=p{TCh`0m}Y0ZC4M>Y{T55x&iz14WplkXq z3N)v?!Ofe4Xo}3H$@i}i=OV4$qvMClpfkv7_gp6C6|7?t(&SU@PAj^2O6r{8DO03! z%(luF>{`S)CDA#VA1okC88ns5R*R$ItdJH|Qg)fktZR{h6Wfnbvc|yQ~jWjOy)FeOp z80RUOM`1-AzQ=np{2O$X2WdFlpPa{OkXB1JnJdh+MtX9xjb)#V^nXibadUc5FcN|( zW0(zrJR}xr6mDcbIf8`wt+I3pD9j%q$na~>W{yu1YJ zgk*CKYP2Eib=V9py)OWxgifbcFnq*pi;=a1>b?RH;~4c#l*CTd>@4Bbmq zWpa*TfM`jH0oE@ux-p@yshWy=?DOKjL;aN0*^wuZYfg9ccLG5N8J0phN>4?ZKKjqb z{}l-!_p}rMv3yC+&;UY!fcch@rJ<&di@*rGy#QeN|JeSXwB`AKvlJsHcQS9iusKb3Jo?X&?Uwf3B%Qg)**)+$0TS!{Du z@g;Tc&`atnipwnQJ4;HMLNBTG84CE4<-u*>0qx8%w|n`L6H-HeXIyeck(aBj)fjro zB3%nzvOe^Z1>Woy>YXj2mn_zL=#u)-OHSx8QYCGfp_k0eHqj+pLoew`FRtO+S(D9{ z^m!D?R!-*iC(w#|lU7sA1}Z96`dF1b$eOEG>EPNysV1gcdhcSjAE^}=jD1>_UZGWD z4;hK|g+KqVi}+;HflH16{;v#T_p;762sMNtY%V9LiY&}X&heCiC*dO`htH#ZNEydV zIUpRMM!~NKh2TNva>h+06`U5b%ucHVF-p>SydWsq5*zDT8VI(c9aSilWhsC{+~_?B zt=bJxj)#oMBI6iwa2_+#%%a&&Hi}{h;74P(!D<1gihPslR*F!HK(Y#=1X!-G8qi`E zaExLy00I}HL3u?;R8eU(C>ZU87Xxksc*4MFX$+wi3{bnBdav8%K)`cGR_tICpnpXb zNp?VfsB(?H7S$=+S?KekN2K_?UT`zY#&=KTnK&Y~oJhz>M_#q)a3|OO>=4I3UO00! zf@0O$+*c|US7F7Kf#6YND-f)l6y2k{yab?%CxHaAStJamT3K|UCLWW;)QH3R2{8T) zeiOmMk^Y~V+?-NcQ4BkOfTmfMm1*ZKce(Z$Q9?bpj7cp=jL`#*a_CGFR7qFsRR&ey zLPwNzn@~v)#dssDq>D}~%taD^g%Y|MCO}vQ;khR}MwF1t?jk3y;m4E+RsP@`Z$PbV zWw}%r=6GX72`BKV2?iDQsNB{VQ9?U68sJ9A-hvN|>|slMD3nE+qSLzIxTqA0Hj}YE z1UWDUwE?RYYJ*C}j5XFIKrFzF?WVI(INoxC9SQ{$gF!|dOEDOv_T*^DnG0lQ5XT}V zv86K%Qo-#1oXs8^zIg-SmL*Q4|0DZ<@35m`k#|Q%M=Xm-3;!Uz2LAkC|MB;n>F67K zRV>)W!|v==BGmSB7&--e2Oh!c@%9Q%7c8!V%~q%zic6CdC-)4H?*M=AnGM|Q`dWo* zX&aYS@^xY*R$MyZbv=*z2hObLUKeE+>d#s7uM=zA;?e=H>ov+BHq*|&?v5L@LPg&o z|2pxpKwLWTbx+;WJhP5_-St(%6G^N5>%`|5ap{29^?cZW!OU9rb$7<-vjj({2Km>C z4_e~V0k7-z`YpbhHSFu|sBsI=cxL(6iBE*$(t)o#0Q&zVJ64vsBLL`|;ADaH3KJk* zZPq}#5s@Z>R4K$I4w^LhW81*ff$;zoXjM$r+1C{N?3M=LzR7gBJRYzYnr!Yy@D>7P z6kp4LCtkFbxc$&tljzNi4uo|%SUx@ZHWZYcS5ip@8qqyZvCoV=`cur7hFUi``B+oy zY?cN&4Uu{{cd|iVd_*2H=LM!9QWXnpc$J%VJPK46X}2_FMLCVWngDOX;JHN-q%tzBSm1m}d?k|zWjxcZ(xT#; zMt3ja5VE=~_+kbx{^WQe$61wu&^FN9W(T+ux$PF=ag?eiVDPZ-5e_|3UT1TnJ`jis zRTXXYK#^=LZQ-r)ntXy$XE>`JI+KDY7BqT@?sZBuW?Ov`^=OkXMwAfTViTa|THWb- zzLwU~7*RsGjBctBR-^gEbW^jiG$S84?gYU`1Um;o7^ZkY7BQ+VZSrAt(+L9IGz};o zr7tW5x@nNUWp&dawLf*bDFgN@E^`!fC&}V={fWugA}k$ulKQ_!p#>O-n!PWTk};qf zD2nkkpym7fG7eJvQzv5?{12!9iyQ#9Y;?2se}1|3d5{Bs9Yk44g^d{KSMV<9fz1K& z_@h;uTEFmjc#UznnN^Q0U)UJPq~MOKLU&X6u}O(yZ91XG5>J~{J$UUwuMB~*4eE<3 z7Et4M@QaVfo;+QkU##F zn8+A7y_^Ku@fE1Qp>OC^FrG4OOoSM6GQt1d9~ORp{xbXmhvNEes?0)#8UFL`&t0P6&8-6avV3 zU_-FEyl|>vYhY^jI&%5w-WC2)m4Peo-G^~nk z4xO8wN^rgv=7usFa$SKuv%nV%h{rBIhVmfs(EVI7j_=px)BJ^>V!)>p86l15zq!BgMST})cQu9Ri=)B)iZGb82JKGrr9 zbnOe)Fz^NjCpdY(Q`u%QE|-$6;w^FA)}6op+vF`{_y0O-s=BgbEagf%A)8a`@JZW_ zVxNW-%}=b5$_0Ipo(J(t00uCdwv2QbosP(cy3FmY6Y6WQ2uhoBLe5^Nx!tXX)q$=6!Wk6k4v{5%1=uYE7u-|09q^iBuq8yW9f)cNZm5B^3%VU}x-Iy7 zBfnK{mHazPKh=QXEeLhFmS6hO{tvff3{O~6Vu7W@$YXzNeF@dVgq8%IFrhWUDNJZf z$SEtOp}Q6MJm)ahr8ed}$OS~`v`mf0R#Q_Ec&%V-qlo`VA_lQ2hnOL$4`W->W8+|J zQ+_oQAfu|r?r;G9ilxOGU+Xqo(Oe=F;GFddoSmH>{1Qt$z!Hh!Z9LjfAlfbS7iQN| zGTuO(y{->uu7Ad+UFCSY#H8jP)}p8RSf<9Us( zViadNwjHAK4DTqn7ErgB|1+jVt{%VB`$Dj&k+dy|3m>gLy>UT$VUJggyUXrRvXf+f!@d@eWc$+vy?0+NWcBmT+3lPIk9~BQ6@9WF`Ut0MHSMVsT>WiRP+!_nVA>FzEd4q2hn{ss%qf?yMN( zgh9r5XS-A3<|zAGJsp7-KyII5bWVO>O42fi7`amltN8>yVd0%NkYq0-ImON!9NHVP zG=iA}by_u8(STf&(bnjbLr6xL5T#}DWl?^RjV5IKVj{d1xm*shDF)rhbL2>;L|D{W z0gN*F4J~waP`FThS|D{P0(wliBDJ}hjzhxBlVH}t@v1oj2w4k9kiHTfqsY7uZVv(P zLvU+RXgRY$P?cEbjL7v^P)kUt##B9=pmnI|gcB&~v6WIW zUMm{8#so7w0dpJtkBnED>7m!)wV58z#QP#gXhL!PT@#Ar@S?7~{)P!9tHDc!laI5< zh!Px}u&8CRGrhcp8k`WCRIn)imv*xtD`c=v2Ff_CR(s;KO9&bS^WP$HxuOt+2;Ygq z5THwBhmBo;M8HT9-q8pgRVJ{t+q~!y0nU52LMLP0gZnZZ;~b)JC%|Q|jRmdB<(Ed* zqNRpLVaQ=ynI{+#lt>&bS*{!^kPH#9;IJ8z<0j6}8VG3sGHITz4S~RIvR;Mg5hvz@ zM-48{L5_ENlLtKWA;r*fg9|2>orbq4=}zbR8(*VKo>qijd7{w_F)x@$FtM``U>Qft zsji1{1P+Jx8jN1{A|*<`{t&n%-|TD$d>DM@xa>|GH8c(8NFxdgk`!Gf(-1aP4O2hd zdC0}ehAKlwF*cA%iveY40Q%W!!r_bYKVc7tg*_bc3$g|Nhkhb&i&ci{o;%Cc?5O0= zfcXl_2!Wb9$2hJx%J{6dn~eXDy{`alBKiJLQYaL6I5?mLmr7DEEl_uNtfWoarjoQt z>d2wR9g4fV9PWoZ9PaM!Qsn<;b~oAGY(jy%``-Wi`}V%yC7I0b&b)au^XAFtf$R$2 zE*=m_jFO$1t0^EO_H7urO1~OI`$5te%fXq3R}HLTi&rDBF&|h=GPP$azd_fljQ{$qF@w3V$Zj;gQPScxVizfg;BsP?E3ACYLkP>8KMRPC_IuBvB&wmxxSB zBJzX+lLx~0cZIVusga>-QuJa~w9!_CG&EAk6NxqhHS&}vUJE@ZAC%5~l}_)Q0g=>@ zp^|KZt{fci(-DR+`5;Uyyf?)srz;aA>7kHN8`30^G0%h}Emxu?JbKBHz<}^=#329N zws2c(kqA=Qi^K$I5+br2Ee4Z#MK&NvWV7NuCnfRBRqR>Jr~+{prZqP!Cd-IxGiN;s5Li-pkx`Co}vOf?wm z91UwJ2+%X*S_=AL8G!nviMyZ$0Dy2h02*Xn(Dq-&1;>aE@j4?H-lB-Jy1sG%b&G}) z4b^lfJQfdptJ9P)OC8qPWYpEz}~!QY#)3M@HKUHK92+7qy2cxDntAEe_+YOP*)7v{tiWB^o>d`VNF zQS+5civt*P6;YpEh`B?rm)Qe_jwn5uC2{=0#0|Dr|Q zkNd{;bJM2^G&EW+!FdvfOff7Y>Hb|weLN7&fx?G(?CmcD7R1`L91eKGT63WRc z#@_Fx&_FCT(PdqW)r>5oMio5hy`J7+bAZzgIwGKBEF4X-n95BxbVo#DiY!Bd4mW-Z zI8Ga@4nO;$>JqK3J46{bpZB`HK4ji{;ro-_X?kps$(BI`^C6(te-tGbg+E>$B>_D{G9<^Bfm$X>F z4@8OKp1_=iGbkcLR<=^p9%T*$PaWWFoJxi$G<-Kj2WP`oSTC@4va+_f5?hN!=5(T& zn~NNsMK;bh4yAo%yPqPxBSCtHzm8dcn)Jvop|qVZJ-s6$-GV*c!aYm-p{r&x9jkKF*gva*|GY2QIo=WUT z*0$Dw5>wiz|Kv0xIg<2j8Jv{FHr9rxHuapJ zq%SE*i~a#g`upii_0iJO@}~}AVp2vH{fLl^?EMDL?LXe5UO(3#Qc#9|{bhpkw>j-6 z+f0g}a2_x1Os0mvG%scznCRbhpIxjHl+yeTSlP$Gl#9o2aupV0O-@%KvgmacI(HR@ zdqsqX1V?yv6}BUPLWX+cCz(VF1{U2fGL=fH@_@57{A^8CmxAUFzMJE_iN_;dQCA^L z|A8M#Q&~_-3hMF1P3Yu~Yf^`8=Fg1NC)CsGrF1VQo?o<~X{#u|vWFQQ@i1VrpO^v)7b!V50cgN0J}0`wCs7}`$s z!k>Co=!M6f%h*MrS2)?>+`YO0;6wc1Df}@qSr34YT}&i$GK9Gwpi`MSX;r!eXo&9OpnMsl-;gRPSdxM$FkOB2ii^8au~?0_-5 zIW;?|_y>b5%;cZ{rT=84lt-CJ|H1U1L>jhmpL~P?L%PD1$|=PdzkVkNNE00Ba5S)d zYGiqUQ1E?=2ldh)V1Gn&N3oqq_XzH_(dY&vCZY;;YYWRgfsTPwn9A(3t#;A;5A>RH zA>-IqVg9R-kuqGF>jQp<$n1!GoaGAS2ucU?G-mW94bNaRCC#45Y>(q>5@iaBO0G0+ z^CWLa%?Qa=0DgCxJQF)?`zgG^o}Q1PyOZQHDe)^a4xZau43696Q^7n<_|pTIZu^T|AyqH*xJd^n7-OjMxg6t(XkWk4f?iO@V|# z7>Noce5V`|h*%M@S!3P<2YUhoo^o6MhJygPys5P#s0B@cu7H0->GKt7d*K%TTfBx)NE~Z3s14G0H^ovdkqXo<<;V|Hp16p@; z8M=Pw-ll!`5A5hLQufvkIxKIFy$u9!^k;+@0EmSG?BU{UxVbc8!H8)9h|WRlREsnj zII*}cCZu%D;B15rV9JO@Mv#(rg2a9qqJe>NG$|DJxDOK zN>n3<9#YBR(!|0-t8YL$Ci6n`M1+*%Y~Iuw|Ft(y0^5s4k|aZ(p6_f9+g!RbnJiR8 zyn(a1NSv8xPUtGk%`Gg5N4r?zH&PVE^xuxEW$YBlBsnB!srLNN>v)X zQf2O?kV)lgbJ%v3uzo64`Q{L@m@ZGmYpI&N*^0QE)Q?rJ058#GplDD+*`Ph+Yh;oP z^kgKQ5}$<5)F<;o%uG3u86CMwlD?RkDkTmBlWK!ofRTwZ?6#%zS5Jg2d2-knGB84h zt{gOI*h7eXqP8vBa?C^ENlttaI+GvBhDfQLEiwgyrXpiwz`b7%-@xe;u0m&YCF`4+ zDU*@_eIS-$UU5)y(n&47Dg4Dk$dEu?*M7Sg^5ooQdlZj$(Zx*0>tH3o7< zV(&Qw=!D(#AfHBx(zr{jRw{^*CAfn`1gb_H5{0EAYE-#pDRSE-#Tp27Vxj`6WJnQ7 z3W#pN#QEUNs?xf0mQIY|b|fVcuDF|p;{Qx}-68*%pgw;o--UOS*WD-{Sem_ipiVSM zmpRLr1xe2CI^kikcp$HWMH35N7Npr~RuC;pbf8k8hWjdX0ZK{kVvz@xRy878?3Kd! zZttAoW0RDDQi^-X<$)22NzFveXQdbLcxCg73I_D4U{ROI>LrK^lLl#D7v`8Ag+jHm zqueua8VdBFFndo;GNV!&`z9h~622Al7==#*R1+4d2K1(0H&PXp>ra1OXU^9pgfS|` zv2QZ`y0T}BQU~iR;6^-cUW1;AWbjInjmz>55lBXqw^mn2xG@@9AZgP95S3@Oaf-yH~Ebg{*4}^ zjoa5f2KI@}bV6S?c^?nT93;2ui~>47L-7XtjyhN;C_)2cGi z$`4V|R7Y46J(vg3aWqmHiefK`x8}Rr5CA{GRE|YS0}3g!kL(;CuTImEU7W@_%5Ka` zsqC8!k8=6m@XBtS;tE|$T&lpbTxJxM3dB4be&U>>KiO!Zz1;KYU3 zN>!rH6U2+zfqQheDwXkpMFY*hldXvJB9eBvyKO-Hr)WlrK_G$s18k+Fu@%h!-@N7N zGPSo7C08be1XO}-!N&9mk#JCOgE$Zfi;$>6gTtsx3pxNNQg9|sgwS%}Ej3;=jcUar zV6i3DJ!mN<)piU$8h7*$0!RY#AqgAQn5B*Y0?iPaeVmRsq|>cIqm_wre4_ZKhD|FU zddk#k5RryYE2WhX5kVnIp&O)j%U8?Qq`>G6C#!gGe6n&C5+^GmI;mO4=(BQ=AeGDuZLO)(>FWQPBMN-tPRqA(cM z%+iH?ZJ+2;IT7$>ti{?GHnj#t>BGb#w27rsq2npW`C?Zg<{ESkh53P@N6_OvVE*^S z+Dc$9IE$JigR>F-A{Ct4SK#SPrz36Rqtg|@>?q)jNzw&l&mxQoly;rqK`Ei%#Vjhp z8!81p%7`W-krIsPs>AVu1)6y9>dQtDKW<4NrIDp*S}9csahn`3H#Jy2P;wq)H(~KlYd0_ky}>=vb*mOyBz3D5N;Z*e_nBTP}w7IHHs zBAlyHQwE{}r(x_H!G{L#gN86DNLTM>s+4eM(7@{KEYy7#I_o}@LLSWFOn?xg>6`$a zB?W-1g)CW4DpMz_e%2j#{UtFgv_NU7wFHiNDEyJ=M*=5eWNxwd2B z{_eUt#!ew0YG+NufK2spj>$@+-RhU!EY_3$EWh|T$TZ$^Fi=!k!NLajY_yPd6TN5D zI?H~s_7-@OTKl9QNGOhQM5(vSroWBwtEXu)-4`0Mh?uP}&mWwCt4!VU)Nh0GoKC{yGEv%kt z&+R}Sd>jr2R}>FGBSBsZc4AwxBUUx*4?`HhmdGN)0(DC{Ym8Yh(pOWJO6@^Xp4B(x z2Bt+;Og- zfMjK@PK`7i7^AMIN&+C;$c&*N)w{qR3NQczl5BFij9B=z4+JG0W6)`YqDGj;*T{=n zt0rZz7pu>1QYl*W061MGQ^P9jg|M|i3j}MS45@k`X{*%J+wA*}EgF1*rLi%A9Nqk;0As27;*RMB~))(6c1DDudEt`vH8gCdE_9)lX0Y z@LKW~@Zf)2d4KZm^SC~0 ztQQ;++!VYwsbbR1#Mva&B*~-}0=xl-hYMWRH*dAp9AI?&5&eHa% zG&41V<^q%u%c&~>&ROcgS?bAIid&}F2HXq1QeVy|JmV~V%~|?}v-B-z={wHS z_nf64IZI18OFwaze&Hkv*Xa$1Lj8QCwQ<=`}j4bPB_i~cMc*8$g114GE?jEXEG zA<+`0#E`~@gp)Z;f+VUmI0|7dPe=AY0Z0IXFkVZOT)bU#T?VvlRKShodYPBEs{Ae_w% z7zSu&0TN$N83PsoF+e*w z5h1B`6*G}~%Yzwe1d2zbx)M6$$Vl`N6VS{I*ir*REM$;zv2!qD%3*bkpe^BekCjJ8 z)Y+tFkVE@KS15hO3Z-%84H2i*wXm8m#GJw=G+ts^jR_)cbUNQg_FY;to~{brmdDXF zHgCz915T()4RFrS#-)XYh-tcXdv7f(7$LHwJwDtT)Mf(MtJsccwLY^1FqtL1RLlZ< ze?R#DFyJM6pptO|i>)I%;~CYRS6DU{dfU^R)a!I(s7MaTdj9TfBPFD8(^wVk8W4*=kkI&fPjV)Tp? zNa5t@q~oD1f0UsXLmB?WDp`Z-W&#udf=93r$u`zXt?ayvr1-$*Sb2;MeFDx*ZCR2z} z?y?9-vIyb$9@><=>>Ke8s8{QIH$ecx&^dd^qcO{US9({yY^FwXlEQHh`=v45HPGG) zM-aNRH5pLo;>OIe8tbf=-Ze2s1Ur_zIKK`-4+cq%w`Rtm@7}-EVY? zsx|Po&|+C_;%ycyEb-e?{ghrhDyF7#zKuPm?3?t*Q|Qd&nZytFs71W3=_n0;8+-O~ zz6}RqN5}vcLdC96Mrad1(t8JeD+C`PPvj+-AmiL$X#pkF?#a2xn0TY9peV>FBI(bA~TpFsEDx6{8C$ftF1Eg`Vx3l{>$T&a* zqDGLLt%>uI8F7w7qf~lnHboUn-)b9$S~Rra=17LOM8yU^6;gjO+ilQzsRjhoA%z|TE<*!BGZ!S4T%RW2n0mrqF5QO z)y4>SY2W+;x>~7+221TaiAM^gKZ3a?qn=tu$S0%6c8k zl0kh%4~j@s$pA}(sdQ!C?goZuq`5VqY&Kxflw01OiA_q4~S(xKxYfV zNQPNXn}bIDXDf#REa6_Acz@=h+D#bd;-BX<( zijmcj&f+lT5j*G%d`vepHV3gH9+`yY9+D`ML6MqL>oCN~$X6;theFWmx9H5h1fcGc zrYFL*XmtLw+>Xd$COpxQ|E8E84Q*iIMiY&&{6unt3}vG5%lRN zKqlc_K3HRfZg7tvw*Um(&X{1@odV|nNC*R=*csi0SQy|a6Ec7D>m ztVqWzV-tZhmem7{mN;P|3Z$jG{TU#3IGM5$@U--ZO?Q=|t+G*>U^7PVLA%vACQ2!1 z202<Ie9bsg^{2xVO$)W8{ z9Z$kDpyx(Pl9OdpeRo6Fupm;B{@~z`L5m$=Oe&U-Vpz63F%5iQebSX`wM3O4DN|*D zv;fSAWmk~Ig;HYMr&E~qW|9VYEaCj54w51F3LTRY4ah)nj2J{%ldp?M_ehZ?rWxZ9 z+QYRg882bdc#sPzjRELlsm}R`RiC@-OuwFB@U%|G`G-s697MYp zQ7}?TUs@Pr$Ng@+59`QojFv1}hjR8u@=iE~SRmC>8O;D=?+0eXy8x;OaVCrx)5uE9 zL2Yu}2;+e$tlF$4k@i^9D;^FX#97Hzge{Xs_eX8l=)+6+Z!vPU5X*}($1aG)#4s6> zbigYFTpgyMMr$TW3t;+$ppFy>nutk}DNqL)q(Y;L^ndnFc47w`ZQy0;)f2XmY{JRW z!P?2VX%?$kVJT6`lJT)A05S}_+7p;Z8sz@w5}276oMvh7?UNWAn!igD2Q{8 z75hmYEbV>VvfSLfldS`sBbE8F8VWKn+|A25DbdzVsx3&>5SW#)G1AY%f9I=oM;t7! z5LlENrE6hSW(uukX)y#I8*TPMBh3qu7M5;qAz;NgHJv*3BNOp~ONB&^k(l(`;54Mz zf|Y^J0-PL>1=vJ#Kw($ubto1=El?osDxxPe2MT5zx#OYPEieG1Wu${5YvV?6%!CBD z1t%6n=pZbUvz-%|1v$2eXM>9k&+6sc!ccg|uM(1>yhO~dN7Dm?A0AVbdkrGfI0R~r+ zEFG-%AqwIQ1dv!D^5s(@A4Y*!^ko|mjR4`KQ~;3N3vnn4^dJ>ZN>wE2;j@!dFkZjj znn*hjp(7j>u|jQH#_E#|r_F7&vm%rCckZotLzaQaj}#P?z0KI3h;DEL_!v;4mg?Y! z>r+8LbOD6uo(zX&0ywadQw4g&_sQTi<|4LRhVQg-fk)?3j0I!~s2NoXSD~M$5&2L% z-t;{H-HuhcD0GJR#dB)ohT@3=*9+0x& z>JR9jI6~GF<_I7|!>THbH_{PGRZX@J4<3jCIWp83f{Tp^q^LS6p!<0;M-=A&SQ5yR zKGL*-qS&8&HR5IXHAY?_Q5gqI=AH{W;oP9);^#@e6Xq!r4}=O38*&)1WZdT(vX9@V z@J^Csr=ue@1@ec8MFiW@WHlqiCA&$U-5_5HmcPG!10EEGJS$s9UmAejN)ePsg*Pw{ z^JGhO!Jl3b#VA*&7~w0_2+AiNAiheglkh?@iV$Xqj?cOlGZ5Rp9k1x=O`P|rLp!jh z5vrkPH=8~sX;Bs^- z20cf(48$Me8*>IRPtPnp2WmC+5=C?>6e8D7YHR>&E%cV_D z)}e*VAi`0b_FyFc3o~GE)c2313!@}JDTjPPW~Otpp^^;ggj0swmq8a7dpjFQQs6`Z z;c&{HMsqk&8Ou0DWvx>{On7pR&M`C|K-Q2#-)OI?2md|27CYI9wfU(2u3iVJ!sxv& z086|E*zrHwZAUw63a8}n>UM#5oUYq>Zh&>92D$V<*lQc`@BIniX9v>##sG`M(eYdO zAH?+3+5Zo$+Ckd?%XR%9{wHWXVWHN_vIy_;fB2txE5q9#qR0R6KWT~az9D=dW(wy@ zs0Vv7x!)7A3(Z}uFjE`C_)p@0qW#|^TN5t@3j@Z%` zqY`!66m8xfDt{04ua=($8YEfP_WQ0r_>nA{mMjf8&6c6G_mO7ASwDrAFfogKU) zf_04Mp4Uvul4zzTf}QqU3%u3b}b?S5bfyE&kat2B$4zN2mKRD@C(! zGMu)^bY_vdUwj3N)&yL50^kDCa#Cj6Vdc%Bru9)ARRdnrVWBvViXC)EgDe0X4itRH zu0fy{kUts)E@0i6j#X|(^adgV3(|nDgeDg12V*9vlH+v2XGxh99s#Qf8P4&(jt3CoVXz`QcF}M}Rv@0iitbr9(K_`_N`k?P>@Yin6_vrALv>HI z^wq}bbDBtP0Af`P)^0mu;OL4A0Jst9}|a1E^CBsl6XglkydkUanMc%(-G zNcxacvk;a<*1E$HmHXcy|y?DcQm;BAR?O2kaV`0B1MY+_>CvF4(l z%+EXln9+{1YS2Q^Ph@_CCk!&FY2iM4f&FjBrG?ktZF{z9h0PbodrtSZAKx_dp394* z7MnY@sa6}HFhb))9_`8=wPnRb-q1%my%Ppc9x&j|{OuF>?e0-zQ8M*QyNe&LNWvSO z=`gKhjDtGqVNvkRkTqrfJ}yXk{^o%PMyn$~ytlrW^2U$ugG#j<;k@AVFzr--km@_p?vYU*A2_e3Gba(v|#+{VLf9l zT+Y5}lzJp&wQtxTGdhpnv2Nyy?V438*}fqM${rf#8&?!n#r)x$sV4?pnb|8yy_fo`1M^(}COjT>Iu;t`XMt z!{BQ1^>20TZf99`iN&scvl@kZkB*M6c(%iglrE{ZHPh|w$9r$>a{l17BA=6AS6Di3 z3>Cd?;#YRxQ^)dWQ`BoNdbeKoZpEyM?PC7?JnD#V&m9#LUnIA@do!XaDs}&rWc%Dc z3pXT8tvdK(_Ua|gySmLubi4J`GQ?!wwn{BKUD(q&q`vR{`@tax*6)?Z49uL6Rro2g z_r`{|@0FRuA0A{j;#rUECuh4Qn2j5LvqOBF=KXgaykDF>Il4#FPhX}M4?iIt_~(30 zoX4@`_m#67yJa>G2pTft{OKh(lh40Ab!Y3Sr)9@|$~~DE)#6pAMaxs?`uu^{-K+SH za@)Iq?V0zMITzl(eY>wPFsfy*OTC(=+&^*dT=Ddclcr8BDOCglpMoFBIFtxwOH);nsiOn6e?SMwkwxxuHNo^cc3PAs`obz{Bv zAIEx`dSBf2#k>BHHE*V_T)L&6_0UQe6xOe*B|mKJIC+uRZ`-C{xiH2u_k8<37ovN` zyUew&>C>y*^e*q7-RQTtNr35UMXiNi>v~0)6?M*2w+X8`C)>Soue$Xs9VyJZde>@{ z>F@JyRxkGJ)j&9U@T)6|#x`aleZx=nd-&&$)~&m?bWmRHTC(Mo+a>#v+37YBck;%K z6>eK|dg|LQk&%(&6W2O!*nRGNp7rLXOVi>H4(&Bf+_Y)`?5-_ONgvnhJXq3Ix%W|5 z-z>X2RjQ0VJMBtHa_xqxylbXi8qb<#Iqt)X{yv>Ic{ZGsH(wPTl@xJi*9ry`+N4}Wf z6_7s*1X95H%n@V?QXwzUc)=9F`&=McCRiibDA+F8BRC{DAvi0z4EUe-1y2R91s_d# zCZ;BpOlp|aGid~PplwX7P3%ovOx#U;O@d7#O*)(OF!{sefXOjI8$lI8d4YiciT{@W zod1x2n}3ymp8qF*Cm@Y(=Fi}dWZiWO2 z7DK72U>UwHEo;IP2$taMVtieMue0%W7QW8J*BOGIyfXOHbbPhPS24b}!`Er}ItgFr z;;T2lI^e4aUnk;gF}}{hS08*GkFVqKbppPQ!q*}AIuao_rU!Xi>r^?ZA%bD}x1snt z2wz9z>ll0;h_3_iwLiZ0!`Hs}`WwFX!PnmS+6!NM;%gy#U#wd!El;weji3Pkme2WH z9{z0szRttf`S`jJUjy*fA76v_Vq5{|DC_!^0?QTQ5-uQB)< zi?4C`+8JNF;A>ZW?S`-2@iiV_d*G`CUlZ^(5nrYFD#O<#d`-sJ6nvHAYbw5`;cGg+ zX5gy=UzPZpiLY7ss=`+_z5mlPk_1pioGXV;& zAj8a;&5sdn&RCxlk+=eQ*4WQQ#jit&HqHJjIjZjnN z;lYV^x|3#MVfU!ux&l$&VNp<#K3ZhL9^TtMGEwZ<<0ay{jX)8r8 zmC7g-TN)!#z=z4uB^qUQqa9jA;a7Zag@8xEh*fLh z3?ra(5>2*>w2c)gA!7*gO>nig#8X_SNu@m8u?m(RFaU3&wH&>PG#%ntI06wEr!l~R z1gI5yzN%^zO9KSe2F#ODi}()h)98n8=8}jlO*u+XHKU0}(ML+)D^vTe+D*+JHcCYH z;W%{Ui%6w9g&w1)2S!Y_fMTPeAbbdR~o;$svaP?eJ$;+$D-iw#~cPPU0z{HqvPYY zDdnp>#`)U>`OH1pDYN$3JFi)zqP%gQAiTlBA4=Xg$^-Mt^acl`2s;x75T3D*;b z4dkD8cvAWFg7L3@*}qNPclpaVh0%*QRj>M{+}(CIuUya?-Nau4l7`&F$52WV?yKE{weLa;M_p#I+rR;v5q! zN44K^&%@vS*~fl+_~Sjh9NzWG!t|G#iGQ>%`Ms#ROG)VDx-Grp&k2ezC9Zq>RmN{z z=D^$He&g;BTw>)Ihp*0C*nh%= z3Dr%@@7dI-NItS(UH5>B7o;18T~1%!McwCUt0%QqWv0nvYwYeg;l{*fZ*N<>{330> zx8l>SuSPaMGv#2^qWYiGTWxsfv?~1I){>{kt0mR)?Y*scw`SA$mAW^HJ3n-GahE3D z0(#9@_eSiyGDY=r`MeX`XHOfjvd6S7&HkA8Zq#B` z@Rzv!y(ufkn}u}isvh(Ev}Yyj&O}WeaG=nAc>5t~Z@s&$sWz(P2Wc%^<<$6g?<~Y4 zt4F;lXm7XU^3;ve&uypQIlgA?hZe0hzq+;aXt?-+cdv%Yrtu=#1r9RUya`G204cf^0+z00m+P3F(7 zdNSwG*(F;~)ot?OadNweth=i7yhFb{IrV-W8pf7)I95Y zLq=9u+!*2W7m!)!s>O;Ray}Cph?ZouL_bXo!pc@L*8-6%|AMK zQC`@w(K+;U&;6qtmTVt8_gbM>Inz6{Z{DfB^TyWM4&@$S{4! zlXrKXZr!xv+1Qo~l`ETFP42U0;K|xWhwjyEeecb|xWaS2$0zq(H1ud@#gprX_x{cG z_FAWc9dFm*+H)HT&&g`0rg5?T(KeJZ!-3wBsFe7kf5|KHOyLyru&L zWdx43-R&k>9vHtPWXq>j^*=X!H`D8YSFs}fr2Ey9zZw%_c zXZpK?mt(Gu{AKsAAC)7kmCro6Ziva@84IfJvE6c5H00cq-w*7}i|AA%s%2RAse<9x z4zya-`}zJwE4QffV;ASXNjdIj)8u;1wJo|m``ETZM7`nJ2ZvZ6tolpS>8YNp);%q` zUb48)g|{s(dh9!vbM=eM`L{XF#S0haioE5XVT(rd#;rIoP|H_1$3yi+@~^+eF_;APf}A5Y%$W%%vN=Usya zPdAOO7r3`=VcjF=U$jZP>gd=gPzp(PZ@>OLEk zk>)yVTfu6JT;80-_jZ3;-8wsT^lR_nkvCqr{+Terq)SMS&>{6$`E11}>-e%`a-ZFf zA9JP5)3L=F%bOL~`l@JmD0=MV$&;&h-CsU8*m^~iewCB?o&L0QsONis!ooGNP1d|o z9(AvktGX}gv^Qh^9a+}swBds)Wgon{>V(zGa($d?yDXgEw^NUl>-Hxbr6wo8Q@uDk z)xit2^%DOjRuc<;ZCAO|@`tXP;;$ zel%%Z3t1#g)g$2#N00M(Wf8vd{Ht5899=nV&dJC_r#mZ~i(e$XY*i`x=-TtcB)`7y z8B}-R@k0mFM>M;*eCDUlJ3?=)>fg7<`6**3zblHnS?kTcTD5CGx@0=ITdP__BPZDS zewranys&zUhyAa|E}iZeCh2vi<)n*)qk3KoT2Qj$;`N-v5j*m3T>8~1^h%XG&ps{Q zo?CrM*_B1tQoFBeI;T)DvG{nJ$;1;Q{ge-GkB|I3XP~nGv!Y*nzn`8neqoIQPs!c; z^=7w@t?Ss{>6h5cCwdgdpHa$oj+xlyP1q=ZzDIiR;7_|>&7M8mt50&r!ji3zTJ3p! z=W*R?=2hD~H%TAk5IMehW!)N&mD>`pJifH#YyQFbx)1j*zVF(;Ww6PiM$7!44_Mml zTA$OB<{n*teL4R3eklb925EZiiE`S|`stv7ErQl`Y%#9ysWPLSh8*6wa+Cj}`9X7( z8|t?jlF%Ug)z_NCIt+e(d`pdM?P}KPp0sHSzuo(Ey9Sz+3$vQu4LR3kQp1_~H7!pI zP3Kv5x{}a$YVx~1s~*1F9nkS{SV*gPBCA}-FBj)T&QDA{9G;!(FwHGwzb)}B)EQY@ycouYlJI* z?{(qc!!|6NIyi*}#znbq@napPguZHiXUj9)(RewAm*%PsE))NbaV zwft#O_ddm~&o^_4+q~FX5xV-u>1N^v-L31Lo4d8%p0zEi%G+tOkG3q{Z`WsbIa#G6 z50Bpe6!WE#`o*r=pB9hr@OHq$=xV#le~9zEc*t*J?X}^fAQ8{Rixu|%vUAO=#1ZeW z?HSj4@AXUW&AT~_DxTWOV$AW_)s5;c>{O{q=GtS&8pMqa^PW8aY?FkSG1J8HOI*Vu z*Hju*t!mee6GkZFx23%~99^xQ!|<4ndk*IPTKDLKJ~hNEpMAEUFnr;@bE|*qsDsceOG_$+jG~v8e915eBuZDKXP7GG`2)sq0Gs8N#lZ_ zoV~YT)2B=K9zFBfyy8uRvlBBrwl#GNxXJ%D#S06Gu+zal889*}_RBpFTIJQ(@xF+~67O`)64{vHi2s%&}L_ zjcT&$$OfCdI|~bs1zwp`&tc=CjM^2OjDMBAXB@A!-^|%tZ+2}qY3Z)kDNKHrpB5=0{HPv4y0Su^WhPLw77QCWrgijnVNh-StdPTBdtR}8@Ze`x*- zc-|oY^Spoj1TZYzlqn*zXN?gNkzG|!zA?yAED_O@Ia70fkVU>JB8qIqHiXt(pG9(* zECMNvevU;r`@~`q*FqKvReB`+2@%n>bB2@CW{_%nm+Tft;3uL-37{qF3)RPNtU$o|_? za0K%ICPCg`;=j|7{}(<{7DnWcz|QQ-f525L=)+mso3peRS1Io}S1G?5XK548(g4oV zK+e*;oTc|TOYd`*a?*kSh_miv&eA8GrBAs^1=*aXIh>`~kAyvLf;`T;*e^)0ZUJXq z>|dl;wB}IG+&4SsKh)x`4BE z9?Tkr-rE+KoaWWhE1kz#8U{~jr&o6&lpkN`*64RJ$5&oG z9A`cyMwMH8=Uq(H)!H^6hp%J36gWSm3@@4<}d8j(Dy(dH+b@R^gPP(}GT3 zHBF4oJvOf3+4&Y$U7psqxGo(2%aR=H_TjG=SRO3Qw>`S>jc2)mAr9fahqiUNxrk@o zY4)m$TW2c1X4lDmw(-!H=g&v)lvR7W>tY+*=T-XOINH?v!BoMv4!v$)^367Db69TG z>FS{^ZQrB~G7Ym+6t-V`?8DO@13z}G`MJ{akT08)*E%QMy%$mBI`&buqCI0Ck9fEG z&Y5BH(dloOAKbBH)cY2RZ>@XY)65nn?T($(W$1(P1fNB#wnc=9mVEhMedw+A;RQwZc~Q zQZyZRY^t5WuX$sId4qeOCOn=r(5_C*j=5_G4EpeN^tfG3?{C?Y(W}a=7azm} zNUNR?`}?06s9YGay=|k+)|u7nx~`3nj^Ekp$sLK`haH1EcMi5&8#l1=v)60S{I>dN zIhT$6u2nLO8CtB0Po2!GSq<~QlBsfA5jZu#|PjVJPn+Q!gjh}dy~_#pHmPE?s9EK%e{@QRsT!w0 zHm>Ni@5qy1maI)}6%!D2Yh`|V@tMY>wjVQpU2p9p(Ino~FC)90j{Cj6i{JbTs@t1a z2_A}ErX6wm6u0bob;oNDO6FWJIT~8x9+-Nf#&geMi=K$rzgSLe(GNGhTNgfdPvt+p zwr;%fOj|$6P-W}A4T?-pHCA74@n@IDwoeXK4>&N*XJcWj%~f3Le;8{QF|KC2aqUn4 zGOI(!*uWrDdA;iE)>N5zb!FQdIh8dlCOTBEXI3yJ@ma5-Pf9cgI!^4f12l1iLhV`> zEPoT*rJ|K|*SIx_4f`F6{d&3CnL#H*H!U0Yr|i6FIqyw9x5Y(~F7n*9=^b$6~?r!!0&nzguVbjvI5H ztZQGe``qg>D;?7!YSi*+DR@wKWmne$btYU{KFj{M__iN*@#-CkxKW~6UA({2;^!d| zhabE+cDsJH-2;ZFBu#MZ`>4l~299$^MlNjmdR@!g5#t;6y`c6LIJ|$;h{`h?L`ZQ_WI=1U$A6&=Y+kB@~WQ6+m+^JC0KoUs8>k~*Q?KV zHCcAQZ056hO|Bf_^Com@`E1*c(H-u3br)T1)9<0_=X!7Xf_mz+edm9$51V_$``(T> zLEbl;CQner9s0_1&HpG|aq`5c<)`f$y=^ygPRNiagMAwf?*R_@o%+7zzmPAzB3Zc3 z-m6*T^Y`}NlJp(1WMRi%TYKO8H0JY;-%c)<9^SrTW%1S8`y8kL^0LdP+=!ba+mA7y`wT$>K_ggP)*RGwgjpZo+qHNL1dXs*Ald|pbjP4h^b*OPZ{uh^u zWwV>th?{A|*=y<^R&p{}kx*_v`Fz1U00Lm?E9j1fvNHdV!E z0B2(P&epCyUbQt!#x2~^#&^3FdD+T!`2fNI-_t%mL{|_V zDZz$2!~Q}5>ID{*JXf&|CV44Lg>37XbC|t4N0*X7>WJS+dG-(qqe#yl0(pjQ*Mq?$ zu!J~2Kqe_a@khqbf6CM;7|;Hhq=VHRW)bY)s|-970y zdCOVSoWT$d%9{%UMnDF46cMpnoPaH;ioLz9lanF9mT+bpvPvP%xidSTi@S!hcF1yF zg`9n&NporvUYmg1b`^>&MV4aRaUyp*l^DTA&nqz`I?10(S?(ASD08-BHk}|b;Vs7M z6)7!}&(=zcI>Q-*)CO{MFe%BMnuE*dPCEoRsM1WAu>!BK{8YJfYmmV6;0Qu<1NKIKxrs7`LE~0aWu(Nf%t$cq7U?A=Rzg-VlpIZ17Ed|uyU2!UgU)Ea&dlU(WH+g-i5>(5CB%_Q z5@hKiDmnO0W1-4;zfm$Vz~zid@(WFYgo}*(jHQ6`Qb{qkeJGU!qfmo#4#X<}>a&-T z?y-_cctTuYA~Rhat6vaV24bipK$h>Og1qGj2MPzs8O`0I?G=FLtCVu-H~JPMQ7ItD zjFyx*XBFV&V%~#{enbv96041HIp-cc#6ILS=Lj2wu0+VPN}sP#+Y7%?iL}%sgH!9u zAkEBoS{y{e#|Ewc;;nx1MM~ zUSjfXq5*{_rZIh68EgRYUP$~2NpEF8K!6wU);7O_9k5X@K=_V{ z*#iUu(}*JHZ@fKRhTg90`q6Qh6w|D$4SH5mYvRhn^CQ}Gt~533^{HyS=+QR4QVTdr5q3|x0Q=!o*r zTf3xp-PwD%(`3G5m&&K-3~@@X8*b~Z zxn?@MnNvxfpq90gW4vE4U-sC4oTi0jp{7YeH^s}wt(}9MqWiVzHcE5u=8ZLvO zdbgM-xn|}1^csAn{3+khWKRO_)-s>0b0#9`r z`T9?Df4>SnQ*1^|y}r-AO}ocS>O6Ft?lSKFm2o3{=bY%X;g>Zhuh{lGy=_J8bNi0P+d}IEH#dFf zwJ80Up&kF6{57-rku5d)O?%55+sb3!*O;8pcb+-Cr?)Qb_|QPMU3C7W31R{~rDhy^ z+9c6uoPVc_%`bI(G2p&)t1>4-x=GKcZ@+p+Gxf{WsTCfMxN*8~^VBbQ8U_S;_+|uK z&Fw97Q#|dtZsCfzw`#{byguGzTW_C)8|_UV7v7uNKm5#&2L&H`esJVZUusg^c6&+U zFinM*Zw?OXU+eh0%vT{5+c#*@C!w#*(I@*!3;FfDYn)m1c#hz?b$%Pc$ZNeUmS1VH{(jg#6W6r3mz~RXuCl~`+0H&s*FQ?V zy{5vQt5q)rWL}lNl2_{Va-5*Yd-v%cU*=_`Kis+AzxK$OlU29KY!Z&#KL26H>o-d? zdoR27$=v1s$7OeBU)J} zWP>6dlq%^?)}>{#vQ(PH}L9%qN143 zz753AYlaJ-FPh%!&nfvc`cM32pntdf51v*TIbvAEk%O%jyN>UO$SFFozs3+nPE~Qc z)ShQXwrRb$?U*q;eLHua5)xcJ`^n&jgB|6quDqLIRc%(Sm{Z$FZK-#A1Mi)+gTiIo z?yAiz4s;)6E*d_3dD~H6>$lx6Z})X_R;TBJ{?6I0AMWjEpZm1A&En{lfA;8m{MmZ> zs9b4$gQ82h%?ET}*Py)IwZUb_V5hY`ljet4eKLI4)9Rl450|gfwrr!jm%S^Tezmzr z*2n%sn`f+;_%8bP;kfibtBRK$rq#pM^y072T*@Txe zZL(iUeB8rUTc)-s8B|o`>4$i;0WM!p_|&`j@nTlxw4C-KO}bC(lkPqD<`|XziA9RG ziB_#jCcN#sDrwrwDR=)2i?>=-^|B!NeILhahofIA@5vO^qGhLogO5*+99d2C$wg5AM2^6E^pi)6-MvATs9^CLrVD(O~z!-sW#jF z%Rc3&DUOG;KeX!5$#s24_u!qEHcemn*7mpf8jrkNj9FjI=fxg)QTf5?KYDDa5;HPM zq_#Nrakr`cpkc2T=Tsj7)xOs4*R{a7i9tX1~Ry|t$JucVm+OgNUI?ulwM-0 zpa1<3p=^_qGs_x7L{xmQ=9$!;M=C>+!kLhFLHX!}akKwhP{74Rl3dno(}Msd#o{0Q z$8;%9<_n@nbRSKLjae)hZ z6Zw-%Ci%hK9NQ^#m8vv&rOMn37ACpc9FE;eI6o;>`R0(ZG+myE&r)jQxh_`3-7Z$- z6X7MAjC6P+J}QHTj#y+0H2{K!o(!np3CH`etUzq$IUW6H7JS%q&Ba2bm`eAjKz+`v6t58TnLK7q<5( zspvE_OF&zN_HAa`S0(~GF6Gvvwh61d@)>Z)S zfiX-+t`n|>v@gy=+83cS?F-pWLJde$x{?QIY=rXwBOGKzgw2dnHsI`eD4_>V&le=q zq$rK2wA5+^Vq^*Kpb-IRcpQ3;wIbB0lJ6wXuhhH9V>hYRfa4q_YL&T5($fjWnhGDY z$a~y?iSwi2;o4M=c!-QX7^^=ZlcgN-KTgg_C6ZJdQ{ACO!`(eB#@pTvXAg-`dH*l} zkMaCJV8EqzZ)EP$ej&_V+KryHb^)>#RU2>pT3m5z=l}0b03=O~fXoaOVo2 zc=xId={^Mfojm?dLDmln5dRM9?5S`s%Wt!eF2$gBy)iifz(-NC?W)?{Cv?2Wdm}W@{~^!Fizt!iWAB(f9RA%3CR z|Fb6cI8qRTW;_G+8ymkt9axAa#^0mn(+b2ttmGry9RC?LpIEHT=L4yLbg6y_ao-*g zrb#6wC8=o({;$dVdYsDE^!+aZpdDHWweFTi5`GdeFsuovr}?AfczHu$sY%Kzp6 z{$KuYmbMCMlXPPK|MGuJ(bS8PYf|J2P~v9(cjf;^YZ8~x&Ful~l_D1Dyiud=oihSb zXazCKX5q|&O!8R8z;h}Pi4q;sab6y=W4Lej_e$_~woZ=LPT&NMMJcXhv>2*5I62sn zk`F^{B~muB+K^KU2|mf4(RspYobmu>J&)* zO-!N@fJ6SLCW<50L+RxITTOC%+-mX)R1UrxZV_l}h=5e~p%R5G-PnDw zNYdR@k+B8&M`nO=4vahLBz`by)QIw<3?<&_;0B!Jl>@#8YQGFf0s%{J8d`qUAqrn2 zDhh-88NJaxTaE5ehi9kb`{_9w2oo5$F$J4vbZ);9gwHU;85ew0sitE4krMhzID8_T zUqp6Nl045bBv83`4CScKZP4)rF*1+Pqln-SKM*3)(+Q(BXbj1rBkl#YY@jSh zmJUW``m<=4NlX_FTd0J#MRzVC{+lkXUEta^jv)U#?2QQi8 ziWEv-E5+=v~Pck0K^1A(h*VQUL zPA=Px=6@o%MM$w96+;t9#oE)v2&QC2cBZO3C4hEN5Mg* zU2WKwAm^l9741dw{>@kg0aVQQwF@4YDy zAan?XUX>6O!CtXoZ>U%i6tH0zL{u!;z>a_-_JaDK-A!E*F1~x;d;b34hwsfzCbPRU zvu9>b`J8hCh9QK+8C!CU-^hT(1gXi7OiQ~Ci(aKTW6|tTB`pSUUripgdvg&Nw0rTe=;(o0p$qwhBEegki;3f90Q{6a z_v`TB_c}zOSJrd-IU_sC0OgPy>Mf!AAX6o;LPnYF-h=u?Avxbj&3 zj5`o@5%!-sPhnRCh^+X(c+7GP-WGQqvmCz+zybcte{@r8DBO&urrvxD$hJ7z)gy(2 z?1m7s5^ug}l)n8;0}Ui^skuSpcyGkA`PW6v0J z?}K4BypD&zv(x;=4)4O;Y4SM;nhFziNvst_K6;vjrP46@pf*GxlLC@J$cK5tp()76 z(b}6#g}ft~2P66>CQ8gUAb{qv1v-|`~T?qkBF?C+odtQT|(<0wHpq13vtil+T=3fS^alx9Vm(&M8X~{x*PJK z`YR^g$bdl^gXkjHo%_HI=SLO8J#cwW-9NM5QG20*vK#kqX~deW!Hd+Ddzk@+3}Ay8 z3mJj8A&VNOkU>nkGxya zE>a*SLQ})JNR8k{$^hapV)UW86!X1O>2&B>`o7G*?>=btO=FlBux~J``BD4r}a&R=YwPS5ibg(EP&+bwmY^ zfr*CEJ>JFw4#hcSG8nGPA4G;=1i9eUeX^9*xun!oN<=Iy%>HGB4VHh14LLO)XzbuO zHe3%8XhPG0G8rh8vr}VJ89H7&AZCWz!JbuSO5ha$rx(ttK)n>U&YA?0x|0E?o61v7 z&1rBz-GcF42On1#8%hNHc-Xy#0HX|ftKw~wAV$+QHYqkSJrQorsk=r!z&kw|@-uZs zoK+z644Jnzf{)h@BO(OZhnj6oN{fi$B0SWy5H)~1kN^<`vj-Vus-%Kw6F_(z>N@V6 zM&9H!HwrVN%P_1iFr~A`#26^-mE)Tfo02}{vI5$DYoM=piA_!O2D1bf7Gf(JI+P6K zrSX3-!y*Owp#kB-2YOx5?X7_fP!r12?!d11Y2I9jRm$!4MBM*OD6LxbR+Kw5a%NaWpqLH)-nME!{Kji z;zO>CSHsvE;6^SK#6vOAOcRDLFc#JC-yned2JC&2GltghY=$<#q6B$3x{|42`-P%b zBB6~?@04pd=Gg1OUJA^+Y2-+6PnV(0gG_S$t(&u~tuXaMX9FP7LB>``U~VKOrFj7x z8a0*W>BsnLPX?U>fqQBN>DMJ(EGJ@OdzdAwu`DO@fX<_zIU;NcoU} z7?4pGT#k8k1#YZhCnoqY98r~YwX)#jAe|f#K~4bq(ilH*(iNJF zfe{{>ODLPK8^jYZ9YLu4H3}&90*V9$yc5Yu(P=Se`Y?z-G%7YB0d$rbq|_psN{WOO z8OS!k!q5VHZBi#S`oBK`I6C!{Npy5TBCLaiyca!<4dEfAf{pWUCIB~v zttMG!1Vb2rHkc9z*#`#!8@zPw{n*wyZzERM{!RJ64PZMjvZWa5!C%l(f?45DSkX^d z!B3FQ@K22r3~X=AEf3m1L7ZjAc8?5qP|Ot8@BfMYlNBd;5OrF6a#AM|L6`hl+XJ*# zAmu!#q=77F=oijfz|JwySV&ZWeCYfKM>L4Kn8^S1=VjeN*%RW^eM8y4-jRs4o2?Aw zLf8B)JAz*}k>f$p`pT#Z1D1Y^mogBBr0N>$fmr0==;W<|BI@ZG>KYP^`6>VXxQ9BN z93Ml%hIvliKcfuHv@+nFEN5j@g)3{a1}g)Tw79Od|2<`3DEHNlFq6uu4CJB+einQ1 zDvb0E2>J|z2p9AihED*EJ?cqL!%_zFc_B=TJ?fs!-VaY12zS{4KB<}N%ijU)z<^yH zLtO&AhS-3eCaXFC*j>#EHN%U%^>uU&baaf2bdW#>2y4b^24vsv(mk25=3R%O^$$xX z3b|lG{4UcK1vVY7VE(N5{{%gQ={hr@Qj-9BW2~pc{-6uOd_jj75FB8#_zxunh~Q2L z;18eWfZu_(BUndg4Wtzy7s3hz=!8PBhFNAt@Vgua`!FaYfQM0r2S-MLIQWqa`9NO} z=1P#mkKu`oKI>U=ba;OF1DJn)838~_E#~PC%_PWU{wJRGgK`2ybLRx0!td*kI_u#B z8(Osg&>!UgFNisS!FAvgv8S+;F$eHUzlQ}Kl(}-U;+Jr@IgQ^TE9TX9D3SB zP1$8i@YB$iNIr260HhnhuF;K6l#qJvv2lmfK-+dqP{;s7Wws$5gTZSpm@*UT;gn@M za1T+Cyl6f>OeH5p!Y&qS>gX!~)|ma{Z}k-ol2PUa{ek%;lth~$y_H#NgY*`K^WoE5 zxTh$PCphQfYVIC#a;ku^*Of|}B2UDum<~S?jbY&PM07JQFCzG;{MX+bgpMUTBU{Vh z6IvAF#F(*0oVekszY%MM+t$jP(FGHv0Bs{_5zcH^yl617Ls)(PZzuv`r~tpSjdhlR z=wIB&+8`C69`pDAYoP$FvJ!My;-FUy)g&h-)tgOZzIMv4+*IcPuZQ2|KLOq%2^jNG zQ9$*P^jg28_0vV-YyDA2Ju~zdVt7R@JjiOw(7%HKa{x5JND0QF0)l0K7zmgEdy_dL znN=V|8OvJ*uoFTj$Y8@~ST|u+K5rW@!{X*>gBUM#nbrv2tr->`52hKKZFOZNk4OY3 ztxpUoiNLT<3}uatM3O+TEUE~I0G1tiKb;E-z{ikEqDI8Tx&oId;H#Dt0m3GdZAp$G z&00DLC(AhvRtc1dcrYAb+=e!0lre=dPPmajm6Qy5hXWrb*vJa(`9oQVAR7#aTOOo? zg*?nTs)k_)T>*DbagVu@!2n&$ow2j7Ee)PS!pq~afzZjm3S&kF4e>9B}QULLtW zFvBq`{woH5mca@=*CsC9`Qh9UNfUrWAf^O5dKrL*B*^H?f)sK>FOUYTh~CV|-pv0Z zbIlH8l?tMWo3T}VsW4&_W}y&MBr_+hApxX>HJo8cg#QC?W5ZCY4st_85l=iKIiNMs zzbi@rVU7UD*bRl0+OR&@%yiJ)qOx5gxeI`W8K$*AQ+fc(cCZfk=(!>O{~TCztcVtX z&45@~kd7`Xjmc)<;8!&yd%_=h$gtrL#R%K65V#Nz%s}2kAV>#NQe* z801$-BS({|Y6Km1&b7foTZ6%hX51+xd9_6z0WSg`$VSv;FuKKs(`N6yjMd8G^SLm- zFbj>qLZLH1JO55_&$@$?N~F!joN1XW90GcRdJJ#XsY|$oyx$n7Q}OG&`85J*dig<- zq~)UDd&WJkvh{POYmU{jv#+)~8#r%j<5AVddx>HvjwU6l32vP_sd#E*y`##$^s4mr zaStWOULe{PZFzQjuB80PMc3w5B5(ePxrf_ z-j`E#*E2x3VLiPu=uS{=n%NO+(S#gdB zrk=3fJ%2SttMXt%?VE(muYT8kJNjl%TG7)}Gbz(3n|5^n(IoGx^_|DE?J{jWddfEY z^$N{jbkVaZQL|RlLVtC;-C+^uxE{B-k1`L+)~HO#4xJrxJj%~$rG7!MMg7#8ddE?W zu|MmqDj40TWR(9RDP-)6Z%WNl+vNhMppxPaCY+2T%n1@1-uWFj- z_BMT#-}XLAZDy+0`xjvo9?)0x?=#rcEIfr&nbA;n{<>sI*uA7h!kGhYNx!nc_pDKz zLHzMy;+I#^b6Tbf-wa!dt?zuX7=P4Y&4nj>PK~q+ool=_$!G4YF-6DdTR(Q#%Ef1I zeO`H`?5W>T)mx{FCkUO;?6TbGzfA6I!bC6cnp+OOaUr52??xB5D`%V-Umxn)`_o&z z{uYtsU9S7F^UA#vQq9Op_4{#4!fKDL+jXc%>tODHPH5QYfoGpr9nuKhbJ#S=OKP{s z$C@qnW{v&}a!l0_b< zn^(WL8@t85X7h!!Qm+V$Y7`B856z<bFbw!o=P9D+nevRY{XJw!vjJ` z_Dm>=J?i>Oy**+3Gz0etnG1J+NKCjp((gi&{VtP=JuzyJ8di7L^|wg3txVAKTDE$Z z=ZNsOjca$C1)4pPs2*!^Mss)ND9@d@40r6(?OMH0Wx+$WDJqzoD>B13Qk0^^wF}H% zyU!~uU*a=v#Sx=l!e6bQdp{>&F%jVWZz6*;mh7EOia?H91}B&ZP6^XcPVgw8_3Urv3D}mA&$%x zHcDFD`T>#>GI%9a6#Hmq{UD*3(O!pJ_@*-3qXbEPW<5#*j$+pPqy^csGZ45tGV6UK zx$0e+^)cM_-pqPO?koVT{wH$RCotRlaR;@}Vb**5P&l#)_}Vh-^{BR-^}ft{imxwc zeT)(lqY#NCECPsqNV@=?)1lx^DTUfPb83kYpbZJ=&1^y71`wwV;u<(#^I&}doGrnC zXau5M!$)yUY-A)W4>ia8W=1N6C=LmIMP?8RlyMl4CRc2DKy3h@2s#fD8-w@ZkyRMs z8u{pxI2An*29*FjkCBc-H)gB}NRP^hMh5E@Sr27%S_}$LjUgpN9uF*KJ!6HzSFr8| zBi0$ih8PJp8e1QFY6b-aHGs&UIUUgX4~B@qS)mJ%A^kUCz}AFFTNc*Vn_vmcbyg3L1tg983!0MNe7ZPp<>es&q0}OzN(3@gt8T4W75n2WE|1JiY zsWZtoW8;01p|Za-^eKM`{@2c(XvIROdpV~g>J;e6gx4BFnF3r!fRxHOI(hrJWFo;U z30kan5o~vvVJlmB#SDjt5kj$XiiXHKhPSUFvVy^IB9KBaIf0bT3?Y(gnaW#iMZ|E} z4hD|f9H#IJgiyrFfjkmOE+9%IkZl=GOkT|Ikvw+eP%4Fz#_(i7Yfj;b=7<+KC@Y-_ z`c!0wvOw##P0+_A--hV(i*G~p0n4```b6j35Y0mHZHVSU_%=i{F?<`Mxgfp`(d-Q0 zhG_nVZ$mUg!nYxsL*d&H&9d-qh~{BfMh7$n^PpPW5=EbO;2 z(DK6E$qwRo&nz5Uzk6+4MBI6aYMJ)SlV@L5y}7HtcGL@5y4dHW$wcJ?(sW9LT9F2typ}23#%T!O!8Zm!upgQU6+UL;|pG8k_S<~(%J@20B zPfhD1;;+wtpdteKn#O&w)$G4_a&JPkw`=rMOJa*+zuc1cHMj1E?Od{cH;(-6ZE1)? zztZ#l&*Hlmt@b>#;^O@0O^wa8rmZJ-&!P*d&EA7K+I8b3{V9+at(P^}&zWHVMh=zy4j)?T9#UJ0_8g9ijLd;B_A>{_n2d)m1!-@^*$ z)y`d}?Wy~O-E333_3=^rl4{H91S8K;WmCfYiEd@jqe#blFQ=6mlo>d0n4Y5h6zioI zQEI#8#>RR3=WJ_^3looj-KyphqffT0-r1{I71%8})o+RAS`%0AuCS+_J9Hse|)}z4Kyv(Prf|iM1W2d;sPs>r1 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%0Al>SNpAT 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 z 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�YL;l$N4&b9UTeCXHis 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

SThdVLhGMYlt+3*yqpR%lU#&~5q3LfRhg4DqE z^8&R43r?>@EgO!LE&%&=b7aG5$_Zw}ovHT*u;KJdj7w#{H?cc)zn2#aPNxb96gn^| zA)2~BJdFjHQ1GBgALjiu$KVEhQ1@pfi zn@*1_+Bu8?*U)=RuxAey-0$7d1O7r2X-*O=>FFFq!xc21a~;xS861!17(tR;CMTqy zADC{>Ony#}D^cw1r{}1i^qwSQbCOtUyxhc>6kNvUB=maWmPn-Tm$ErY7zI}f<*`(M zQnHfhsG8`|9*mtLlcFoM|3e~Jl}r)g=M@fW9#Ad8iIU}iL!IaFZdD2|KvkF^NuPX1 zoyJ1~$A7{LFeBMBOaCIR$d&_eq}ZIX=q!PN@8aO(LiEpo`On_P-a+W>$mhG@F%-80 z1ZJ~_sxhW5X4nCWZ2^QhMtA`lL>@EyRTC;%(W03{M(a#g+FGHliWi zNq@BDx$3`T3PDcXq<`r#Idm@d@g8G<^#vEIczL zI$keiC3;dUfy=6yVScdxU!+e7aQ2gfZW-a!J&*CjmB;wOb7lM>C!DbIGw3vgIFcDT z`XjxPUPT@#11f+M4pRw53eS{glGe&b%Mv_M;w^9%E2U1BRAgYC8rD%IiX}2nu|h#; zP&G27#Ux+nk(Q>&0o5I3Ay*}3h?7jJ_{aw%Edf(ENa(MSm!e4>tx@Fy?kL%xQYd*r z#Yx_b6nV&G5DvO4u?Et!u{G`-j52y3oCW@I;i>X8oE*(h<(r{UER(;J61gy#)n=lh*Qk<0BsLe#hX-d z(W4>_Z6%B?TC7olN*b*$wEv^re0&ikQg?AEyNd_a5bt7!gagAKKwR;~HPkVaDKrRd z6Fs6r^_#m!j1?lBoh5>`r)7BIi7>LS7;yza92$HyjfwyiY6K$u@Zur>rw^scB4T96 zAw&Sq^N2AqSS!~9NXYX4;cV(2<>LNA%rS`pR@5x5ZZnKsk~j!=7rn`*EdDlp z1?B(XAfS8_pnN3)9C4BwMgo+w`2Q3`07phlXCT0^|Bpct;%M*DNQ6M^U-oi%o6BQ< zX|dX3U9DrU+L8fQtl}aQM_=I87hi^gBAo&|@~*zWKdR{c#&HG7kNWkFYquBb^DfPq zS!vnulc3tqPt4D0cTtDl1eBRmxX>ylJ2Lq!2m~NGEqTPvpmj*ZtnA1Q1 z+2;=(-%Ixho8;$g-`q6#l{_e4_<3a)e%D>@OZ)#?;Y$A5CEIuXE!+^jpj%i`w}+Vy zJGVq$o9&Z$Wah0r|Kk(YtF!y=nm$@};LOK^=YDi;9#E&<_8sq5E?OyH*ggE5>dFei zyOo~?jrrAg%3m+8G~Xq8f278h-@8>m{ULL3zFOVo_<$Z(?-_n@biA1$4x!C^whfje=Q6Q`Lxb= zhM@0O{?%*4-et(-m7)1X2kR+H8y!2wjK?M^p0CuwytW~ z?DOkF$?j`K?$xL6uOwV)d3fH`=KI%t*XjocRdB;+pEZsR_fnyCFF&rT-XJ_{t885d_sab%2o_C? ztg-=mShca8|Iyr{(e*dxy-dG%@yXL(mpbm7eM0qOuPpiJTDcWmDohbA`M9}h&!A+; z^rty@>DS&Do({OWGyT!q=LgTdX&)V0xK&kWil|*r+c7r7#;x;g*PI@B?#H@SKYsdn zOucL6qpZvw`L|k+%e%6+X|p|FSr(^Q4)(H@n~N`lwUVj!zxc+XMUWOndb9#-(#_Moz8&c+%|y z(-z$wR?kZo+jryGi}P+(7&>}#tHd3TJ3ig^I`rCxE1h~>da`WAqcf*@?R05v9me?A zF7n8<5&SGz(8@1Xz2V3V<*sLHr}ismJy~=1=O+6VL+bZv+9M#R-t7gMZCn;jQ}q)( zRA%MOdHlJ`mW?0RY5k^e}$w#*c3y zU17nyn!dk99lw5K|D>ewE-Aw%gin8LRj=@B=7J%Q`@eg0{o<2hj~6)nIL^G*ZF6G|M+l759B!VQ$zIQu1;jF|4gZQX3ql2;IJEspW z#?FhSJ-&0EM~unT-i>97GUxwd?{T_#j9w!D4_52THsfLbN!NS=7j1ydO z_t&y)e`x>bAi(KcKmd_oih+Qy+W)zd@|W%ZN4nb)`+uc}aDpWqI)dQjhg^V69mT05 z9&*Si?O6>gh~&B)@(_g1zQGB|j2i?sa*8>ia1MybOU#6gtGvrZf)vHKo2Q!+E6 zleS70h%i{PKsI@x&t+OIqav6#muOm-E@i`MYnKZf9!x2!yxH&&BXHVgl)}EB)`3T` z;o;PCIB?p!$AL#u_vf;oL)#Q$*l^m0V$X)tHY^d_c1GJig4l2xKTx0JA%ia;h<7R| zg)lENFo4Nr4I_}~j3gCQ$bi5gC%noIw-TK0m7qD|R`)zUb1IG?B#Z%}bSQm(VT zF~xxcEBlYP)7LT%Klby;1eN6y|Ch_=2)VZ06;gVD_44723`!s(T2fUVgJ_yTZ~b8!@zOmA*) z@5FbqCkzzD*;+1pW+6TYZIULE8=9F})h}=z9#7Cm%ZWV^eR#5;A}Kw6Aj;sjrYQsc z<5$r{M|Qu&|1p}-Ix-hXOtP&r8=&CO1SsQL!unH_t88}Se|D9@q`iVyz94+{IQkTh zF0F?=DH(J~$+Y&&Q~)?r8=;XxtxKt44>?o1JjOGS&Wo+gK(SrS09bv%CNnJBl*Syi znI&n=krIcni;#W6!LKYW3>jcd>F7XxlqlCIsmN15DT+1pR_jcSlZbEi$=1mrV}M$P zEf5iEB9vxk=e(=DeBjJWM#5*8PQdp&piGZ-BVN9<(F#51Vej#SL^pC1(k0f z-8)Xw$>mOygvo+i`PZpGt(yAx`=e_2&W&F>xtQ&ldHvdUi=Dcb zmt|}3B@dl@qeykUMVB?tJJ(N`+teoX-<+e^P;z}5RAWa?#`h0D|G=C!~f)!ZP>p=Cq+!`Rp=Ws?&`Kz^%nX~ ztT(;2jmT_xJi577KE1-*)q9}BeeVF8bJ4-yd|uDlw7=q@*VT9TYrWxV;p~w=%3Yt%^?$s{-Sf=RW~&dp+F3)G(f-DT z4^Q6KJ(PE+X2bSz%gmyL0T(?x^scaD+xT_w*PL$f^2VF6QBI%6jtu#A?!ijil5FMO zcAvj8b+KTA|IKsS_UEVnF>~R_>0?|DPPq{6^=kX^({GjxZFaG4jWu;Y4;{6pv45SY z&t9nmpLO|lY*vGl^(VxSU-q!=0iPZD2UdOf<33ZpqS*k31W1I4bmP zED9Y1xj!u$p>>6fCdLISsIv|1iMEU&hh%V&m#LC;$?gtVvm2i%S11_9_->HpS1N}_ z;V3-aUC@~bTI{AnS#j{=BssviDJbmj`ga~cEfuAhA;#QI1(T`IYDB&ZW0gcI6+h1XBv1nAB->~q{NG(bPo5UfC7y4L zY$a@R3bKAV+9CB8My%WhGn|-Zx^v?OSGzj1^N@KGRtz{DGUt%((Lw%V%pS-LDsUu; zP-DS~5Fc{f1rE3qYOq8@)(08Hb;!Up2-z=?K&CG(ua`Ko-5o?&4HU0{6cNnH2a3>7 zzhMf_B4@rMP19e3?_{wSj>YO!QVhdLrfA9;sG`5;%S06ih! z@D`tufHF-a$Ta`!OaPW3QnQww3!sMovVkvS1KiZF<;Vtz?WrtWKx9wsXx}0i5E5y2 z%byEyax0ot?s7QH|50fGdqfyx10s4Bcy&10s*~!Q@mS9-;D(Vno&>!fiKmb z+VB5c1%i@yd~W7iTW(U&a$NgpVt|*IW%1iPe!Gib0AvKi;Vi#WZt#Y;eHzMM|Jnbp ze|MAXUkJq*$;}?tzkYceS^pw?>hAyf^&iWov=mz(HXyY;*FUjf%Ci26NRn^!1OQa@ zpIZKA>`Bo5yZ%kDe+NEFDjfm+`GuK$uw{Bky>rP+qF0YoTkV)>{5U}DO+{|osz zQ0ae;0T9;s|MkPa3+AxKgnnAJQ(OZ`@9&hvPqT-AHGm8yVR4m}E0-UXAny)tBZ+_AInT3@_6APh54~xDQ zI*S5}xfUBOj#}KbcxU;YrLCo_Wr$^><&TyVESFmDv^;D109q>G3GFP7TArm27lpBsd}HVA!V z5c<|2^qoQIdxOvq2B9AfLjN)d{bUgO*&y_9Lr`-wgHUsWPz!@lOM}o#2BDP=Lahx! zEe08c4mJoKVh}piAasO5Xn{fKNQ2NZ2BBjOLdO||Vt>EVCH~{69{!JN{sh7=H32}w ze@y^iW&+^q1^!t%;D1gGpq+95*N*@K!54Y~poaf0{x4hnT%*80y7=w+|6{8k`~QL4 zze&%3N|68e^Ir`7i-G?v28KruZeGRC-VOq7%*NSp>p~g`q2QJX;PO-MBj`8^i2bi@ zj@Q3gCo}Z_Qv3}I9Bfm?t}*W27~3GW2bUjNo{QTf9s4$aZZlUNxLZTjpA4-Rgd#$A zi8gH^)C4@kA$@{P9w>3Sj(sILg%O%~AraW8Q-{b^ILQ$93Cqwz{VMDl?gbu-;Bt*U zr;Vxo>=>p#J0YzEBwiqZ)C2$It`CL0P!eQ!H!i}tiZ!Cd@IUA*7@wvF?*=H~<>=JG z*|D>O5d9a2-4yo|EIIs}OHYKv0RHT(i7u-dA;+Bv2#-gJ)QlmPb5cphug-QCe7jPE zfRCLTfb5aSKbP?H%f9|w=+{4|v~0MM_0PF2lJh(KKehfHutxu@)<3<#bKHwBfBS#- z#BY50b6Gy7(r;n;i})P`d|_viJ^uJ&2EZYp$ns(UM98E`2EZ{{5S$e`h{HDZZ5j?s zv;JMo%;5iJ`Wxmom`8Jhnw&b}5+j^|bDNCwTLPRQwOqFTa#;FyE-uvWZea0?{@DY+ z@wIP|K);E#Z{I;EwC@Zo0DXM%+UJmrti0B~z=^Q`n%eyh^3`m2I{_I7^DUYVOSS)h z+4^s(U;nYW%=+vBN+avvfqk2r+pOHye|d=h&i|F`-=Kv0Ev$bK`h_52!wCR=eDV6{ zl2Ey=e>jn#J>1m#H^^Jj`nM-|$A9hm?`mcS|Nnx&VeW%%X)aJf&R*VGMz{dywj9py zELWZiC!6?c4S;}%y#A^a04?Ji5CWX&=(vBx^~bmnzz(hRClZ8z3rxUK&_N(@?hMBO zr!Qv#9D2zqFBafPxP6=A07fJL2$TG;mjC%?X7K;g{0$2j+=^cCykPc%7a3XboZFl@ zzaj0WTIGY1y|#gPO|_<6w14*Ycuu}*?bD8Yu=WMCG(al`=O9;fnJ{sKYC62Sk-^2ROI+a(+Xu#qv4+m&e+-6GA8`xxmrZPkZ?i%iody{f%w?h(axSW@^0+DIwg2 zQN29a-hd5>TTa@E-9*HF1k;98K{Z@BSWv~Tg`FAXkQr<1#crE|(*)sU@7%X=%c{Go z<*c6eu;sG>eqLr~mg|Pw4i;9igFo1>#&(Nm-vVjB!Ki~W7^+q9X0?X%&>J#3%QVx^#ElpQBd>mz+Dyh*`rLR;&Y2b_O5MuqaRpQ&W;p5~H|un$T66 z85~6yA&iI!q5DRphTfK9j3C}Jl>Z|QR&f`etEXHgh0J2!vNV|r@{&n%Z1TRC z2+4_c#9nn@C5hWP(fW2++V*^U_a$B-u%|_Jl6dr+_diQLu`0ELlLDs&6*`AzWD?Z# z&!z35E%8Er2YzSxkDa}POJ~TJLws@McYxG6r4`L%{SrMAMAYEYMXr89&OZ9lW)Z9$ zou|LjGEXt{vhO3n{zi?vaj6=&S`aK{Y8|92(-Nt)+yc|>ne?A)Dle~)eghF?O61Ry$|3b8S=30M z-nppJ^?q=TiWyN8;jg8g+0+g=6>M&lD3oRDsbi_gjM}f%RUgQ7=CfG*|;-0t#5*HW8Jx;)u$LT{s$3bCeF%&|lo{=%3s+i&}?UjyD3hw2|d`@f{1uuC_4o)58=(A2(1l_ZeTWZ-YJC^>P;jEUCex+~O zo!ycyn^$sgW$c!c(!aAOyQQ-9Ed}hBS*3586_@HmOY~vU>e9FL^YCP}j44BJGgC4d zE#v4Ch9%>+r#LEA-`j+As<-ih-h4egMMlA6wJf-bf@|4uG9NwK&)pmy(&M8GnH9>9jXQ1G}^=5rFeQ*bXY7Ce!Ha|chsa~;xSHMCrrMAwrHlH`Un;2G5Y z#r9$1nCX@%cJ|{_d@anAo-R>Vbhe%`^q}h{`Xmb(3?s^sdcMrxk+FTu7AE0FgCBsb56-Q0f=n zE}4}>15>h`DU3LW1<CkvBb$>B2fTRqG zE=5Bxo^WrPN&5Q5Cg|Zlany4{*%U)>Yl)sLiV;%x`?D#AUQyhjaO(cFl<-9TdlTqN zpv4Lck^Sd-d{fWi>K-XLH~gNSImrM2R;7(6bA*Q@ty`7~yy8vdN=G+AW_pbnQvO0x zb0{~clY>Z%wSk7~?2=CpfOMoiGn8rI2X3l*0M4O~UIHn>L)2Owq<94}8R>IX(gd|M zCpZynK_PW6nSs89x1&b|Whf!PshA*m8mx~dsghLDWh4YZuGKpL%R#iVz>vpl^Vt=Id*1EWdu^Iyu=EHrz}~VDOYQ#stKmh%_;Oi4iRM(jwkXro9G02 zGo2ioXuvLV$wiDtoP}QLp;D3KNL_dQ=vd*7jRZzwFX3N7M2-^ zqN=+x&NL{yZ>h>cf^eZ_Ezq(Lc5LuaaOcSvSTJ`T+fE^0u8tdc!6QkIrN zmA5P*BZ;)lk?+Che=}z&0$Sq9-ts&)_zmkhIH-yp*#AQH_CHRclSJ|zT&}~pEt&H> zvApbmLNM0DVe{)WjsiZNxD(m`X#FFhar{>Nf4L}j%}i(3oQYO`GYUt0=MMIe$J*Wj zf8cdh3X4p!qbY}GhzOi=P%+w*nSarv$mpM9xaAfO!noBNnEo66|Ka>!gXG$*O@hH_ ziE8uA@-l{s=NK-8OjiHyF4KYXEFAy0gv}rEMXgpXk;A4*F*{^3BX|AcRl|+*S07b| zlB_x&AV*eV1+^cVGub+0sAQ3CU%5gCl0t@3g-wuDk292tDA>SZ%W~%lI}&9LGh{Im z(B^BXf57%T!}|p;C=M09OH)&ZQ|kup7U7V{K18gNnG`!Mf~;WC+F-(bPyx5F(B6jKtE{asnw%gRRs|`O8EhZstA> zf-8%1jFTP`qNNla1HphC4 z9+Tji?O^>kqNu<}g>=z@a+O?}p~S+437MX(P)>?2Xu@TN$5(<(5~0cQ66++%rf@>k zqYs9{41`N?hz$GHE##L>(1_b&h(H=74>Ns10r)Q`hpz$UIP$d*M zhFuaWe8B;mCrpZB-jj^SVZ=OQ&^p2w2n2knap_>xsATVA?;vszIJ+1t-OK5Nyc9;I zVpbfB~C0R1g;kT4mO?E_1^y2EKH5;3(t%zx7y`Rkv__o9U~x!`!_rn5J^~;{6atmG zK&m8EizbzSrnFJ0Ae9jN#FFN86r@{F+8e38CYJWq?R%b2^>LccZl$V1#EIJ zM!g5+lnhsMG|Eq$w@n%Zh=x0S`B%Xf`5TlF0Ex6fIWZn*!7lyoJR=C zL>gaos0@^8l4Q7csKGfkc030;0*-fZScXcXtmVkTq+x=A^WEm&Amn~m*V=gx?l~e4M@lnCV4@m3dufF=7H9om&cHb;`2%=43VSg5fK;wq>pvm zzG9^u&ci&wl0r?C1vgB<-GS>R%Oy~z7SWPi42Rq#)|%$v8Bw;L??h2>Tu+GG;tNvLP^Da$iMrvp`kp9&dM&_t3CHN}R|1Bl~ev6ClG;O$UHX7q%b3`yY ztDra35yO<>nH-yO{UjMUW$LB+Jrhk4xmO~UWe>4Rsz4$RQ&VR|N)D4zmSw%bLmjl3 z`x65@kx+_PgWiLrRTHF!7D_2s28j+@V;-hX$k6JLCBg%IL~^upEjpuM1sGPql!Eyo zQvy{FZp%rg;cuCEf!-jzA+Vc#PkLHf=hCXFS<1SIu>z#&h>T2ftzTCFhMe=vy! zid8ufmIwds33({7WZ*4R$d$+_=+28bF%aswO7;)4Z=_U}q0R(|6HHs<(Cwrpz|Y$h zt&qkPbUuj?!+Al9_3S2F=pPOKD>Be>kQxBiq9P3-1XIz*hi1qnso?w4U7?2Ls+yb# znMMiH0uUpfC#0Drw|x^RA(tB(8|FZSrdv;>LX)qWO(1642UD^P-bVel)@+*gA~?(18)}1aF!zKWw<0+rWB*~>!*U{ zWa@Ux01aox;6!hEk{rCDSQ3X}B_TMiNiyKTZ)5e|pfpFS1G?@yS z4vCsFZAp6toKqvjpghu{5OmnQWx*;!{|Sd!C@G&qnHEgCvysHj(2;@VuYLIKC#69E;gfvVO z>(rFEj8GP)&O*+UpjE=>Jvlf6E7M9~Wk%aUM@YO7#KCZA&r75A)UG`Jcb=>MyGy!#`+P1NAOO}bH_#1rJBE^G^4mLga z-aH-HV>(H++TOe_a3A_}YhhxjLy?!45+nro*aI;f@&&J&2vTn zCGWSkRwpKEWx5#hMPMjainBrJ%!L(>B^iKNlTS4ouxM?a0NN2STwykhT9B-m^Ho5v zMD!0Lp(7?TYhocH5Yd@fcZjk?YZLzR^WhWO|BK~QBZCZzC0ZikoU}c@RFFZ*hHD>x zW`dUxI=rIfmcEhpj3hSUbf8GdE#*NHk&Frr35%d3=@ep?qbwcllfr_>(7`V~*zjO# z)WsZ9Ni3xa1QI$}Caeb?9OM|8z<~S7spoXpdU@&Z59vw4bM14O@MH=u@%Hf7-=9ba z4asy820V$5Vk{X`O5a4^cqtA8<%fhiqA>LYqnWcdkWWD+S(8c;6(+Pb6%O+MIsh=v zMCAE_V!XwXj1C|FIslY1fvA1`@IwxjAFCwW_FuM-{rSShRmc4<{q+~)=gFDaQm`wWr z+kdJ5zbCuvL{?J5|6j=dr=I2F{!U=hfyk;>7XN>;A>r<>cSj<0c?Qm>*47|gviU}d z1jlfHNB;kuY4!gm5kMg13lZSUUU9h$1Q_uDrh>#YxdY^X-M6kr z21RxnscN>d!3+@Tj6rj)vGIX0OS#qtY)UAnhgy?k1LiXY#Jb{xJW@lUjX(P14$GutA`3WfU#up2zs%%47WCxiWr`I(H%rh{1G22y=lE z|0Qe>GaMp9HsqYs=<{r}EWuL)VTLkIX?=r9nH=ksaI7SXyzqpWY(j~mks;e3`8tqT z6genRIRJ8^WI*7IDI+BLV02()+B-oEU}gF>66S*BfPeFUkt+Lt^MBbO(~#T0`M)B) zJkXC!onAAEhV`i3S&dOl2TWZX)=(r6{ucN)H%yrl*2Tc@*Ml9 z$o2s=;u)lOriFKbTL$H=2JS@~>??;^Dj9FW@)KeBX>+OinJtiU}UzvfD!9Xy2{6+=pE9F zo}8EfagI8Jbe$qy9J+}Uv|wD)LGBE~mn}R^EI|bcAe9Ev;kZK%DN^M|-AVaLb8gg^ z0DzDeDdeO%D^gk((3#6gFU z55Z9~Nfn_+j!wk-lz<<+8atezBqSuxNsXKzDDj4kjKUkBE~4 z?!G}iA~{073o&)Y^@yB4K>k~FZiOo5$O9gIwaG(5wKXsR9r(jS#DSa1JR+Ru5o2N; zX^ZC(Au^m0`oByy3nOXep#I`u;0uTYYiY|X{|A>)80s&i|KlJa{aZkQJ+V6U3ed9X z|B59*2^0h}7e*2T0ft!*&i~L-eY8(ICt2+4{f#aBP44=G=} z`Z>=d#$;mkLk^*i1ZN`Fe;fPiw%|HI9=#Xvw=*1w`Ewf;c_cn7rruftiO4derVZf0hc3Ex}7H3+UexO&0W z5dQ5Ue1^j31NiI*_le-M0bE1j%7$l5h0kQTlHih{+~V-r1g;<9ssX<*!e<@0>OtGS z@cS`*uMeLu;1j*mAK+s6eHXshg3tT#*%V;i;4=bkEhxb}R{G!!I_}vFC zsAp1n2K3n6PCGF*ScbsDY>aKUpb zj)M!{S8*_0FzyOh;F(cyLEkII!W9n}%uS_4`1fsaZGvk(Tx;Q41J|!`t$=GWT#MjZ z0M~rD=D{@!u90xT*eec)YnYjtSu-;;^Vjg%5oXg3olo0eY4@TnLBI zpGxq)O25GOJoqN`?#iZl#1v0ap@SIdH-K6+go#YSSCO zr^4qzxCX%mt8?)*LRtWyPUz!F?6I;NSm%`+$ZleBkN@zt6&_C0u|A ziw1+3Y&hv;dnb`kP7C~a#i2a|F8qlE}wm%LIO27S44PCy*{$FhW z=aLFz`$PLb2LYmg9sw?VLPN(m0DYDG|AqTMrV_pUf5i7Fng6dP5s2`gR9*2M`ObWM zf=p3a6c*QetQZY%!2!PQt+N^xGx3QvEe-_cmNw7 zM8UIMnD6z8q~PIfcpL?fV8iMD$FSi})ctC2=JS2&e)8Gy5bFMPHk^L1CmSxO?iaD) z+LBaeJre?Y>gUCaNR?h@l^NgJ-rh-AekwD93Nf?)8kHGCkK|71;>eDoU4+({Bkwz0 z>;~_=_{4)=s&_vmwc^@jaT-cHMW;$9mBzRQ{WPZKiETr}j;on!xkQFEx|9YHoMO@- z!3R~6TjHg$;hg_vrvPB5LUsZy9FM?i7o>TDy} z0d;f@rjj4U3b_Mm$Ocu(Y;0F+VjEN?lN+c^eLd56!Ic;5uk% z+=6DLRal0-#Dv^`b>)%&z;h*U#0WNYh_)BmyEr@96G?Od(_j-WUdWRX2kutvUGg|q zt+iN_0e}K~7Z(sq9Yy4x$&@>(C2q(s)`93JU*w2*k%8IK@BjAmd-prFavz*Oy&$6w zX^Wr?{NccPKOF3}5kYft_{xB-2G=dGMi)wP< zbVWH1Jd%1&F8euA6g-9vkEY=EY&c!pPn65VPYfNr9mIyy_>r>V^tim)aJoMcY&cz8 zF_#Tbp!%s{!|AfGDmI)BZ;oNZ=|J$%0M@wp)bmr>aJqJ*m<5le=TXRp(|C(#!|DEK zX_)VgrTZW5#DvS}Amem4Jca6~EQ?tqQ>LTfQGP5qUBuR3&4N2n_p8~pIYks)%Z5{d z*0BAX1^O0#aFhtn{#^0Gs3(7p+VS#cmFkWzXG25I|F~%U?M0byJM0eW-q@>Y+OV`? zEiSjHwx#0zQRZ8J>NvORz0B%Yc2uozvA=;sgOjUfy$XEV{J_L%@fYhqtUI#P;E8*d zeQLB^elO(0h^)l++yBa0nHA->$>F`${BYwn2YLlvy(JQ5UG;vD=u~6R#NRJoc(dfr zhj%Z%ZamobXrjyE42`|}7XM)%PH67#do}UH?!W%N?cx4c!KqK(KmCxjV8+@VjVn$N zTJ6|oyJJWAwb`1&+PD77lZ^NC{C-ESz)yANdGf^>qmu^B+j_LdojI>3`L?>aa^0Y- z$!Wjtyj*L`$H3Vy&SdTS{QcjbA0G@Z(DD7O>TaEK`D*+8$H%JmE_gp>&$CTEs_vbd z81B-@tO?exw`Z+c(bs@!$7^6duJYrprkRgDg)SWiN>IkuZk@Sj*v3Us&);lu7}ZcY;z*<4e$)Q; z>Diqd=NH|+UHIWU?(Pm0 z8zr6l?Y}s3S;0x==+M;9UPYstMHgj@N1d6YjT?31;kKQ^ryUC3MEK8b`m9C7#{El& zU)uQ2VN_$~$YVtp+uyzTv1q{IPX3Q~G(LN-Z;L8pI?Mg5tQ+^o#YWBloV+h=ud>&V zjSh_7c;IgOr@Got?)jf29iKltxMt($gdNeppMJkK@8d?BO07?Q_pWBM4f~hiM~`l- z9DSnb!;!lmK0e=ct<&TWoj)(=f5)lap?RwYcNqWt&7bSPk=6MsLa504p*C1Oe%YAGzt4fH!sbhyhNr$We;($#nMs;^0zR<;}?#_YThtfm< znY7#+9Fu9?9UgX>>wk2N(K4*1+E9GVAoPDJ?CL9i<)r>%;IVA_Kh%&2u4JgczRLc` zB{z=#kAr~n34nr9GI~NPI|5vA@iYbk{t5jb)}!d<|0BMwDf=Jkiu#{f=-JLme)?K9 z8HB^Ckpq;eg-+D!FuKsedLa-I0cy;{5)taxlii2FuT|0;9Fxg~PM8mLC{Y&<<|G!Y zgx#<5qE!Yi%0{cGp%YdpF=+KaxmY;}D4$?Gdxx(>0M}dLs}`#XZv{ho2C`U>Gk39e zFQgW$p$+`2V3pg~toLW*TNjqadJf;z^Wogn+k3YxI&m*=a_oOLIg6 z4{dKEoL6(p=F=xH)#_E(|BTgNBX-wY?>I=H`r%BHdrfJ4a4TJ?T zW2Sf}?jPLb(9!;OZbg6ol017{>h&vc-6MZmyrXi>li|6Zd*bD1;t%(&G^%pk*-WPf z33Z!RopdJm<5imi??-1l+}Qab|90i5paAW>TI(-~yVsl^t(M6qzxH!%8IyO z3uR9G|D2#TOUrt8|L4^^e_7q7-ht-7{ds)T@Qx{guNtlGlXYh2GGXzQ63A-l&q* z&mJcVUs^>UUwQEIJ=w<0!ex(CZCcMbGV=M0(8InnAD=E9QqXG9>?LdN#s`hM>*m;{ z-L=OJQkF=!zMj3LXTSN&ng}ab%bh*0QMI&A9WVIRJXuj^-ltprH{C8fjq(^|e&r8^ z|Hq>KPBWW6EPCni{z=rVve1Q#AIVFw!h+mg4=GQ+fik90-uU#n&sXLZPSj;e;$3gng7!5v-I-JIf57WW`wj)n%3=dHLIM|bgf69 zm%p^Wd8cWYsx_T9)pFX`bT9nRbLg_d{yaF_!qZgePhQ#fQ|oSc1aE`^cPhTG`f}j* zWBhqJLr0uF%D)}Iq3gAKuYWwm|Jmbe+|XrnSHJJsvh&QRPhx+n_PzI;lU*yF>1wGz zDQj&l@kv?PX`R!IxxJ@mt^WJge{NQ>F-~J-IyA%b7A!1!+fOwH#hHYX5QVh=U1_j|9g!m+4xsf zF6mbzGmUf%$EF%>CvI8jj+trNMZCa@%EQuP_eLi;%Kd#EJtDx_ChJSY0o__oaBb@$z_fdS|{e*zU_p^1U zeGc~DG;x&bz#yG2>-4AhAO33d=yTEI0XuT09KES?&i{C)C}3vLuqg{Jr6-LGP`AEc z6p;Vw?x_y5s!#OTH1WHjt#b}6c>X*mt^f1;(M|eZxt!5?!Mhe~7bV6wuh{kR&it3B zmxs4`cYA)%YqN_WnT|GE<#5>=t>@>&z*~7aJ8hQBLOUd2VlA8xVzDm64zr=Ie-I2|2 z-B{GzcFgxGYo%q^n-F&knUH4aA z8hOZS2ztd?yRF&%>TdL~K3BI@>j!uG){GmX4%wUGGh)QnmSaDT zt|Qt*{{MV1Z#X+Se2E7zYDi=p{2DC+S5UI`?UBIn?#*%{ZE37JqS3jbVYHn&G&}JN zwFpi`!We^x3ko%)9lH48g42hjuRUCFo=1$y?!h{0E)RBTy|MRF~GU@-*u||!3uTuE_2Nzs?KKi&k zy1GMAV5R9hCcgje!fzv&@~XTjSn^}|`=2X1^N%)(8hdK(aN%E8`$pufaPn-UUNpGn z+a0aySnq7naM{KA=VTB3?g*6%%j$)9JqNFIufMt1fG4O*eq;Mu-MwiE9>d$8s-Vr8dbY@&M#(8pA~J| z?eIJQ^v3Al^RgdS-pmWN`@BD>q1_K=?e=+|xOd9Q+^k{hJ>i=l7FOG}CvD)>g3FWA z-nNk4uK0NDiB=a!6||W0`|W$(BloPGrdwPyW>?=@_p43WzfO02Z5L#T@H&%S5ZAQ% z+2=t`8!uYwF|%{+yK@5MeQPYvI#Frj;cl;_b%BT z8NGCBVZ~6{rbj*-=J)!pL*ih5t-bs6Q~6Cgt>CTReYE4))WwxAR7pC}{nN|uI*(a6 zEu!Ca$GgL)|B^c*F?!|x$vvO02yw2j@SE86;Em0%*Y904rG1|dFQis~UQkS$`Re$w z?PI(=x=na!d%tb#{@#~re*eAAVzd1fX8E_DRkeOp^C3|SR?e^QCHMZ*Mc`h))#CK< zJ!ftEZw|ecFl23$*m=BTVI4fXHvX+ZnHwM3f5YZcgO3l%f8f#k=aE(Y^edg;_ zJEHt2cAc|ri=cgUj~^Cqaysu-=k~(20rHO1A9EGySGtc zw@Y<;o*DCTqM2QCpq2gQf`*e1d8C$!<<261fXSGugZ+Sf@eWtcvr|mm_Xg%X`4WBwc-L3sorIA+~jtp9Qe2BQSlDFdI zozs6N-tiH)9&$7+?d-Ce{s%^c?{V%v`PU1J=QUqCuFVF&@Xq&q_&ppdwA|RYcdg;= zo1}ZpITKXReVvzm=IYUtG@)|OyNmqVA8c9!$Ggg(g|JSSyw;oG{ zvy(-kVVbkHr|JhEI3E{z`c#LH*AsGATyvZnIBA2tcY0d2g74?v?AFH2tGD=vltm6?>G6acXYMD&zDbDaQg1XkPbH&Zcf`-zxssZ)Bmb_`LCr-`r8~*RaxiL{#5Lc zpT<`^wBM?6Fw>g5hD7r5{SRJ`Tbyb6WWh(NYtzfC5C6XP)V?7djyFFsVV$CA^d|Q~ zi(l-#(BFIMOvP{QZ?^8V?)S>hYi4#>D9*a-uj_CaVSWFWx_)yW1%4Rb!#k(Lnx#LE z9P&%`QJo@leo5WYV%U*WW0x=T(e+T>sC#@?75msmVXoD+p|&;e1PyMedvh!}u-#S1 z6(K)G)$_C%pV3Jix7Ov!kc>lf{#=xDwBnB7Cqu^0>3HOLN>Te}q9rd@bXYL2z;=;^ z*1i4wi^ANHJjIIkXv7V>CHa?Y9%?`RMc{`D^)BvS`E1RHf-%2MxZ%_wXnn8o(`~m)2Ylz(`{J;V z?`Dc~2LJIx<)R+m6~tY6%N4zDe4qK_lNT=+Xc{_B+I1)K*MP&i#S?4At`1RY=MG&N zvR1mMy5CRTvRa>lr`?&h|-5&J% zW$XFc5wCv>x*@L7`%%%bJByBA7uV?dpr&S@nf8J_0k0qPE34;i%Nk@wY^=FItm;^I z;2WP!=VAQ|tB(;as2umWK=r!f3f+3AX2;${KMGtreO>>$mo0Q_#nsFY)Z9A7nn}I2 z2@PBBiig*I9WYmM=yEn?6bSaWmLH`p~cJo zf2`PffBBBn7E?P;9lY*Dkb2&pXTxmws`)cLCKh-o0)C${zG*_iuB?q2 zG`r=EW&ESFM7`$mJJuck-QrCh>MN%%ivFRa^+E* z>i@Dacac-S=nDs4N7QX%sk^aaduDLs1+#n4vuxNRp^3I_OsgjyBkdjUjCkO&WU_K< z>q7rj&)F`ErN<(+eAu|~O}pyBQNuE~gzTI3AbIHF!WxGMbp7E>tU6&p^Ebb%JG@?f zw9}4^KZZ#^@S6oTe)?ldr&DHC3iF5L^q+CG?|{$)iE|gupL8pBMx#A@2J5_9c06$Y z@TXS_oxLiud4-ta-`(41+pza0Wy6Fu)ta6heE)9nnH7E?4(sYnOBGMrJ2U@ipH*qj z{VxnW@-uHuo2DlhoLKNoWIf2IO31{1OIMt-vTWk;F1t&U=qx#NGY?OpX8S z!K%2MS+KcsZS%}CG^ZVhDi|Lv%~xjbt=W9Es!!a>gAV>Zo?JeA?iMd=c`M78YkRz7 zOuDmez-D{%vK!l440|!!``Wvu1Q}Cy8QltxJrC9e$fkA+-cK{FPAMIFhqd$i+|9;y zZ|AH#xa7nXcPsOyT|;dy?lgzKTu(7XHgBit7M8`q<`g9&rRPMX|hDggOz-?$!;rc@LqwJuA&UU>G@$t!ba z?oO#1zcp&BNyV_!;|vT2k6+ub?b?c?HidqsCQ-?^hbT;o;3K%mP}sAh`@_*KTqGxX zhwHq=gZr;ytR0-Sn%Cz13|TUy|NkxPKQ$V`BuU647^f8>>hyL6Il4FCuj=hISg~qQ zZ?~b@TH7Mpaecj=GKRpvtG847L%vCa-VR0XA`ub4Z2zakxW>RNM)ZFrnX`WXkorsD zKW>Wk&{D0gfM0793 z^{<`)mJP@5!SIuXot-6|`INM55w`%!7~KBu2|(!&`6dk~0L_g4*ZhAd7#Es48~ABC zj$@Hf(-h}_X!+Z!_()e5(HLN z>_&7ReZ>3oX~RJ)hZqk1!q`80Mfn-pjLKy*6&3C?PZu0_3A5Pca$;fAWh-@Wh+UJM zH(vR=C%uFjXnCU~%qcR^PA^hcoO^3;v&#NHhkB*GZeCR~r}S9zu^Vqs-Eez)I`3Wo zd&zru*PNPAyMN+2$ApP_MTO;^uYb#(Ja5yAJHU( zIp~}Ffx#}_Ls#oaT)I!O?veba^4prqO@od59NAPcd_c$Wxw;(=AA3f(@5GvMC)$=R z<)3T6>{{LY>U)e?Ywri=tY)>CRjXA11m8{RQLdlFgXgbxEmQVqJ^RpF|8a|39Siyl zX0-~LtlOdYxm7bQpS;++Y;B9jh8AJk-Mh8x+A5s){K6mSIyqi+GS@z-Y5&s_o_P%+d3`Zk+iuvt2D0k zvdtT1iA{S0ur%goviznPFm8N~c3UMZqFJ`=y8W=RPuTqPL%P)&%Tno<17<&;x2bI+ zFVtnOoow)>t<$p?84hMFjOV65xVdcfh287EDs)PQ>BKHh-Z6PkwAroNhhH+8OM50C zaTiLpC+1mgD^C@Dk34J;BJXtZVdphJ#ypgLS>9>(8KcQzYo4~VV)It#H&Y0|ygoAD zQ`&5I>F)QRYhN6fe43E7jXvm*_mXn<>lllDfiF&N{H&-nn^z>(FDUV>sk*$WUGWQ6 zx3wk@1YT>0t)9E1!|*;9FnASvamAIW6<3$vG&(u;+|efGZ>kRzR>h7v&mdU;jfz{q zn9sH0)uCe_ZAeO(DtOrY<0Igt+s1rp#wy2x!Rft^Os*LBq)(w%^JWK*J)_xoVnBq^%@EigMNV)=fw(98pBbfAjb?)SI`JEOeKTzhoLhA zDouuw-{jMfz|xpxV|sc}`bI}%N}t&15wS&;k(&_oN=cpuLc1Y1acsooI7iS8l1*?N z)tvilJm!(anq<^W{TFx)$s-nsK!}Z)%wx)%%>D@;Q~Jb4|0_I3BnDLWs>fr%e>nZ* zFWCRicxIyazjgOsBL;ZVEq-u3_N{61wnpyqWYNr;aD9WCY~Rv@PumrDH#W|^%$;)L zZEDj|fx8B8|1sj~y+6G+b{#SPs`;8!hr?|)ZDD_oZbu8+iCX5a!)PJJXg^B z0B4rIq3Z@I+q&!Ae4Co`Ly{K*56s-vH+egMUFsuu#gaA|&yr3Zdw<~L=VK+kqIyQ3 z9J|HriSGR4!~Je7_`Kt+^24p*Nx2hBUVk|BMZZ}8UU_btU%o*&CzSEN6A6!k-q zTKDiC!(XaAGMvVie zw{GKGwr*yyxOmz2d1HJH+^+`s3?5k4qDL-szE9U~frIBhFu25W$ZyY_n<4wDttxvd9WChGRU#rlkH8qDVm*QhO~RjFa*YsI$np3{hA-z zaW~}Ihj%FsFTPzUVYONQ;c8%~UOV32;$D;HZn<^8Z_VDC{nrAXr@g$L7PY3f@1^`5 z*Z=fs`PBK*t`{?_#$KK6U3=$zxuR}t!CCX=9qBc$_kyc#PT^jde|OKZ-Ii;89`xtU z%bqQmzI9r4pSb*CX)Am#1VqiudD`-fHGjp~tV0#Mi@u$UT46nul`hmfw}&;*d*{^D zM@jBNW!U9?WeGa^9S)RzRXTkB;%bp*?Vta7!|szxr^y$eq_QvQwt9~;}+bNv& z%$EMyd~dnsw^H_wp?oKU>}h6#W4FqA4yo29IdhM+S;~uKwP^LIkHhrI+5@wBUF%qS z1k8>p!0^>$V`9$VnZJEf&yDLMzO{=Tv3FgU`-0b#s`h8C?l3CB?&MoznQosW*%jj= zeWMKmUdI;)7|w`T)<@B`N6SIS035z%FQ?F`@2mBJn}+x8-of&n@S$NOozC#PVz^fb zmi>szv!{;Tyx-mUT_@A|HJA3UdGyDH2j>|JrG8(^hWM772M!K6JE|ga?yQ+9$HOKGpL9RY){-AZ~ zz%lZlP%To}INd#Q=Wun~aZ!uQ={**V?YU;%lQqlcZa;bL>65yLr6bJ(bI(jYa_Dnz zXX)Y_y?X>i9s1Ljxvc8mjVCdGlmz65EqTw5`ToAwx(Iut{@0oxdjI)h>9S!zdiGj{ zgy5ZNoBPIDO5Yb)%i2A%Cu>65n`b9J?zc9a8g+Zj+#O5qZRvciX3Om{i*Mhq9NRO< z?EN<2ShnMJ^@8uq$DV(6xteQGIr8&|ho$$wM&0;yP3Ojz&Q&F)u4@lh4Lv-f>zzAo zJ+8g(Uvs|gp)INWHwnwe`k${V;9Og9x$U!>7a8vhKJ^(gQSh)WW6~_v@U7=}_qw>u zaHPwaSziWL_#C_+>s+|`_~st@uZ=z!oHM-SsTe!cGw-=Rt@7E98R5O&jy9NJ;%|`O z?PKCnL*?OO)0tt0{wJ-NAYZ@-1)&>ysbBJsO@1jky0~_w2x>PiO5~d+u6R)3=+~mhXGCC2rIC z#O(`5Y|L-R3^|#3zU8}tWDNcK`59RqOSS*2Zlbmau){;WeN&e?sZw z>8TwLZ#S>n`hBs>`{`#h_YGU;!pPlpzTY!nhheAJ-BmX2n)LaM@1VWvc9A3{W6RS&e^QpFg1I~=3({ z91P_oFdPqmzth`!&-s(G=G#&WyJdVcs?5y!am8`-_VKARS7dBWynf{3=*&(9^V1Ta zq~&<7zLsmZt4>krwW!;m;J)s0K_L@H2AdtNjkan#w#Iyt+`R1~vs1fEgWOpLb3z=H zTh`0KM(6H_M0QtLkEj?Ilac<5_FQtjJ^ zQ{G*aPWmA%ztd#V>$YE)PdxG<^zLZE8-MfGBU;5IJAS)Up0a)|&oE%S-R zA9m${{XbkK<^8U^^1oGg1omF+~ep zg50pA{N%Lp#!X?BNPS5i^btI7bz^h+yJGKv)4B)BgrF`2tkh}#ZoGnzcaT6T;gg=z z)-X(W8P8k7`EDR-KFDDY8$i_a zLsbjF8I+tz_Tsa7NSX{C4R67}3azvIlWz-S%OoJBK9ubP?Eeu^$xuWk1$jN-_2(@? zrv=T6qN)B6w2~XnJ`O$y6axUm0&820Eo%)`7Vt0e4r22fJv$&Bi_>c7$34SlCn2J_Dt6e$rV@ri3L#2e$F2UyU7G44rV%gI40 z3Rk#*j3l0z#Y)o>S3gLWG*~L)6Sn`)Kl7KXg+RN0kh#EOJ+0g-W(^#=_iKq=<%W!@63PzKsW)BD{e3ZWtu`DAeTVa2^j zOgGkaNI>s(^FS)-K&gG8S@eeRzrSh!qb6i*RPIhTljJ49f%)}U!h##N|3T>|Z7o1l za6|S#Yw{Sl4M9C13tOWy_pALT4fa3a0RVsYFWdhGiEO4S>_H(V*#EuB-xT=|n7=4| zY`pysd(eXF{{FY@|J3D1vHw#T!2T~Vfc}r#|1~&3Xu|*CT|kKc_eAY~Q2Y;^snyoj zj*jM=C3?$fH1Vn)gfU>`IWj!i$Sfk%uQH*LGg-L7^Gt!>-N=bBSby~tvDTbL#2T>1 z0x69`_2_H{oNU2w+ChH+6hzGF+2w^e86h~vC)TgF3p<}HE znnEQB6M$~UCFccvdD&Mb$G$jp89H zm3y)L{oJHn4%gqssYSLi?^v~Dbxk1CaF|F(z}aNp#Yiy(AH6L z9#bP4P%DvKonVWiiW6WgB>#g-r!t?$8|^6>)-6pa1U%*7&yDsJVJ*UhLSQ$3R8*f> z6AD3~!TwJbHGa)DZATVVJm8)EtM-3Lkb-hdSt0Jq>TT1MH+eQBYW#}*pVB8b`o`O) ztt^n+ts&bqYX7IYh5s6lW65ew*k9%`tPmnYyJ@<@2qkt|a82V- zwn(Y*(j#;Ym!XscIR~y9OS-R6X$r&b7)l{G-kHbq6iGo{Q&}pDFB2np5CZ*!VwXHV zjAvFjP#Gg0@mHAKpD&J`cM8J)IL>Lc$r6 zaC>Twe0$1$T-%UDW*3n3m$Ze>Ct*ff3$ zskoyFg^-9(VSOJZ>4$v zTUcAt=nd_E=NM9n;h=`5wz2!40>spd`1>3CpGGczoMJ*Dq&1%J6hMq&LLuN434VT* zP~(nFC?SB!OA4z|C|APt{AX^pscs(!|?0?Fek{enizq0=+ePW~k zh5c`VK&F3V|J%XW6H)%RLFIopg@;hYmwQM>K&C}e2Z$npa2E|C7mJ^wa0W(6fX}gk zX*8D9mLO>XD3C7`a#>PXZb7WeB>0OW=|FN3>URLC3`|8TD1%Ox%ha2qA+QX%TR`ds zfbkk_j#hbG5DOcr96i`_xs)q}@yMULd*JR1ZhUz>faN@7&=)`@o?s^q%$A}a+kqGg z+-{sDY*DIQC`Tnm2#JgGV2g#KR7YdLDv>l+gLKXWPB7qwF!~8z4HO)CV2rl}{0VwR zK~f=Ku1Ri0-%9`jqu{|KRqWIc-CY7n!2UvsP^=Wgtu-thRNEm!uDGyIT14>2xB^WA z7}WtTi>WafILMNvy0R5q0qGgaVnY2|BzzPD4dTmG-WM9iNXDT7N*5r-+>12138B1$ zS33SpL=hkHKF~B`+@V6QU;;Q0Y%Bz=qig}ncmDLnC^*dpZa83nz&bG0J@EhOjr9*u zM1F55x#5QL)iF`0s=5!i7<_i4i8r2z(F7tDa8_G4e}6$6JDawts#}* zMazh;4anSU@+t|24=ey10LWX(RY+ydF<{lBR3K8wH8T*}GdQpcAXT74`tg(aB9zaX zW(;gAsmxD^&zG|txZv@qCn9CSU}YTzIixb6C_roxCjgrTG(U(^t?8wzQ;P)4j02Vk z2pHk=qX-Ua1S%Ki{Z9`vh#l~80iSyiKlq7IuZ>^_I@f4RmGX~R5`fVFK?K$Q1rub^ zQ>K9f09u5|#zxOuxE&ZHboi+oq54G$G4AJpy;?8LpIE^l?x}qa41_d3LglJ)2{I{g zM^k{)+1^<7cVm0i-+LqRXJ8K}04xhH_NS-ER#%QpPmjl!b7ex{5Z6uy&P!A=7RAbbr(aoI4VLcC!t40)MT3OI3)KQ>APgcRcH0ZU4?CKNKZL=Mh(aEhj?sL99-)6DR-^;af9z?L(Cp*J3t*I}-s zYm$Z}9|emZvWQZb{RC*wTYW1CG+tevQ^K0zy}93j^X9(XW;=@aelN1fXh<Tgod-DxO|h6M6gPfjQG}DZ^gPtD3Poz3}d!(}oW2yv1<^18T0N z)=HSyLcaF5-`R9xRn78>PI29>qHdg-|A(Ial{0aoKc?^k~ENhn+4NUak(@w+8v}M|rOzEf3QV7QK8q>656r zb4cl!rpbGqdRp$vUvqy=g4xI)(-P$&qYu(x8;j|UKQful~>>V|r=+>OA)g!ix_m)3en?7{*H;Jq~c=qm%ThCQ2W-hI+ z$gJH}eY2o?MC{9?!o$^B(Sb=%UG(>CxU`{T%#IBaTQVcJJdNF|JUGI>@c8Kmd25^5 z8)Xh_mO6djt*+ZL`+4rVnZV$TEgrM-rewhPcN40(#qkpld`dY~{PIBlg}9*bCr-l` zO)U&txN^+QMcb@KFU?*i?!2LR+_%dIb8bjtbrsJ!JFB;kxxDXjX^+!W+I8$VVBOwb z4^Ce-GV?okrmiLP%e+^wntVC*Tr50dIx4MeQb^Xy`(I7AfHq!NZ&`e)9t*V!{_;|3 zdHr6}@E2N#);4qA)4}e`^d7u;v0l>kv1iW|$LAN1TUJzEm$k36p|!B6gWZWwr_GL^ zzIxN*kCLM7gD?9xnGj>~MD*o{PfIzFkx-?KYCM8| zRSyBcA426zEHnZ3b%1Y}MoH5^RFZlK74ShQhc#8W1AMRu+yJu#{d%B=K%w%Gj|D6? zO*br27 z8F0UXR6?{Pm{-W99xIY^*rFgd@KREssyQM_p{&FVk_bcL*^33yPKeAygMU_=3Bl6V z(%h16Z9zlAv#qyU0}i$6H}^8V49{@%P5#{hN{L&RBcf1Ybh?xKxbJh_l7$VE=oI zq}+J0sQ`EbX$mOwFt7#NZGf$??NOzpLU0ucM^f+F$5d@*IF?Ma8m<8YgEy-MoJy&& z>LCk~x!|wz=YxXHSnNy!gc=1%9g&)1XlLrb8ybBupUa1J0~CbNXy=61!+nw}@6h2ZsD6k))j+k5-pD*#|X?h%X7y)<@;2)|SlUg?b5TJ{ehz|HkxiFLw zL3T&|^$x-nKGh#_zJ;tGV9f!HGEl~YT!awbE5)Kv z@HJpw9E~l^Qegd6$q(lO&02>20u~(9pC~YN3N|0+h(TJ&6gFVn1SP|}1z-w6Yzbea z={d~(8ns@)E+9bQ0SiRPWrMI@(zB|i3%gdK0e$#k1lBNL9w>U}x(vpGh(59e%MoQF;Pr@kA^`}4 zG%QM6bcaEYKsOza;Cut{U1vECnv-TheJ4N&1b)b=&OomRaIRE=owUSJQ=U{g>VZ9u zh6mzSEO@c1S(jk{hk@{4%Ft1VWHs$UTj=|M&zX$f@Ucu88QMFvpnQ~KpkHV{0&o+S zj2|SGE5HTAgYR$<4ZxA|Qq|hCG?;Rr@gm~6H;AwoiP#(|awwpqL}o*d;{BBCh_Ld& z%0WK_IW$cHHEt3a8#viRmSYKO zQ3Ij>5+Gs$ohMerN376zhhX23HVc7Km=%z)%OKY_)g~h{nDWiQ2NIefCQwfKan?dQe;`0R2QCqRL@xbdbj)>YEELNpMP)&*tQkiLVGAQ%@G zh-q{v@(!Jz2BaB`Rc?HaG8WY#(f5G;ANX5`{l|4!8yhQ2D=TvwB*Cv7oIv$-*v6@> z!AXJ?SIln0^pavKTm;L5NXJ#gh;T0-ReL{NMNCzO6X_vXKcQrLBt{qe(Gm##Bw=)> zoEu4?TVr(pFlz$cKMbQQLjsBPIE-#B=Mm|2jPB-YPVmtb2V(S8J6{4_;%$uCLFz#P zJOr~MqBZ~}F!oLkBQh)mz7QcW2yA~~t*tjbYFz1{7!3A0!2$~mB6wUv`EmsDaW&+Fv*Em(xg&xFw}>mxfvR=3ZzN_FxVLT zxX`R@Jvc!2@?lBXR`gza`Dw z+?EFHQFgG}wW{?sPX#W0KBcu z&1u$j^S>=D2>i?s#_{SnGCd8uROvEOdmIm!ljuxWtUZ%V$AybPGTk3* zPb1SqF}e+z&cWzh4skrp1dQ%UrYB=`x(%^C3;!-TnQn`RzSe|A%Rrp>e@QTdPDvX(TRSXx_U{X3w&;%hOM;mVq>wctl@Sok2qT9c zA_`^*QUa@ZxaCuY1)+vU*k;?1ngn5HL+Y^$HH74U^@54|yge={B6HPIBaqo!n7a`OLWPO9oy^rngt}`5Gnt6@898M> zNS{V{C=LmTXvCJmG$$*%ovoFX4b2+eYsh+o{FmVtP(po6I}39f2p4QHZ;vZ^43-Uv ziUi3$mHhD%lH{)Tn+VdK97^Q@%=!ov37p}~EX^!oN%)uZfBBM!j+91++}0e&*^+-n z`$y8>kT^+-n!}C^we^4SDWImmfzJ)KiliP;1&PO%qr*=Z96SlgVukAeP#9qGpJV`{ z52(|m8^Hh$dB<517*Jn>u)wzpHU6)SVRTL3Lx}&ct(KOpMx`J2TU4cT@#1H=P4m~OD|RL7wfmY8=d@(L>}>}Sl+5-0qxI{#8xY#&8d_xQ;13rv*Ih5v6l|I*D( z>40mNbodYW;A@2QZ@q|g>KcN-4?6q)rt`0*Ei4QXy?^WRma0m_?@nWuKRmzvu*S`$Auz7VtuvwjMvDM~*J;p_Dyq%{!a_7)KBcEorug4U<_ndd^ z*^flsV+CDinLQX%74VGix4%Qghv${LLTBzN&&UqkF^)|h6fOx9Em&|>|5ntk!11~ zlP+bnU*MG!7qcp&z&WhCrJ&B*wzZ|Z+4~FAUOb6lKMpQiaa7i%&!X>5Oq%cgdhzY2 z$X*Xu)m(V~l)Yw0Zhrswv&|38>s7AE^NlhZzL(E>UYZTv8@O|nvAm;L>*Y)I{_Z?;COZ|iK2JbiX{i!0x)rwso-F|DaEzvy|hghc~8 zuvZ^BJ-E2DyMrO)IEUGzrQ08md+H_SKJFCU{#!@4hwpc{-kO#FVQ4CSd=|ynayOOc~tv+4a{B-kHzrOvd`d+zMe6aj{Gq3(*SoGVY%(do- zJ?pIJ+&A5{sQH4s?mZ{%QS8@sZ5?QEWAG@qKK6s(KC{nxd~@1jqu92Bj+47pU)v&n z&@rH>>&ld;-xE`lCJD~iFluj%dFJ%EXI_(t{HI;BFCB9=IPL2?#i8vDM%)PJ72O@) z*mgek&hl>Ips3Og)%(pB_A__SdBU%aPhReubB?*lB$IdlcyM!<@9R$=ox=&9G-dh8 z1-ln~8|vcyeZ|$97e~TVZnIly**y@+MubFO>Hcw~TTU(GaNY3Mm#<#(iQN4yyz_;u z{`cEo?oBIpUs_~(=H5x~MRV3B4(O1)W7@4t1{sMCeLFQ>B>CF2WB)nPLE$&Q-+=6Y z2>AEg0T?lgAU5pZc<8@~hyHc|{$Fzd2Kyg&=usQ32zxi6sE#JT1k?=#xEY|0AqOis z+~{{L3Ruup?>&RMr-sG+u0;XYq5xiNzw1!Mg2D?*8QE(b-Bp-NKE*i{`w?;}LcJbH zK2wyZ(aj3A2AL4_wP$Lz*OqB%@79*-oYyYb{-C3;(_6<{$4~o?4o4?NdzH>ZT?5@< zU4iam%DhDQwiAQREpV3>$%PsEoY{mh(WWmMw8OXecJ$HC#FBs*%ym^3HFTAo~&~oPy?-{csvio zf~`Fl>NYq^9*+ai42yW3aq!C`JX7$2G0bH;I=TZu4^eUc=IC(alBKEIp>W&Sa4~L2 zW!O1umFP~;8p4bxjUiNfn(ihoLuLZqSvA~v4l-$E1E;6fb%^Cu->Fpi`7|%KD!Mt& zsxveI7MY7FI-pLURyd38EmRFQB|yO3G@hU2B4aSOlC!0Q%DQNZC3#Q!E6 z^k0DhEp0H24*-BPJUAZ#0BM#u0Yd<-vF86S0;m?Hiuw;AfB^cVf%q)|1VVrOWdxAA zs{Sbi5C!odED??lb#$l)0ICI7Apj7zL;c@~{~`O|NlOd-*Vun_&*=}<($YN*&I7Fs zhiM-8xuC-du_U;*G35=M3m)WV3Dn2zjV1i#f8rDX)qnoBA%J)Ys4{+_Rsh^8fSm#V z5i0

~UxLpRodfGXQwQffK;yuO9%^& zNN@v!t_66V15F_%1Qi1gpyTt15e}|+bW$VL6bMbgB3Y2o5)h+7%3`LHwboD2;8I!P2Y(m zD1)8Q@f_YPDG~;QxTb@+BdEe2C1|SN$AQp4JU~-jL>%D(cev05h**J{D1|~{IM5qd z&|#?_tAO2o$@Nt*k~=S!ABLo{(a-_PLp-jn0hb~TJz#n+hB1MfG#VqM;7c3|x`fg_ zKpBdNYlAY^=;#QsM9}ObB?+PBqKP$7ONw6dUE*uv5Q%2&cPFqBe| ztC8X74Z;Wz&wI)oQwE7HgkQ}xr~ca@cn(COg8d(fR9Ml#d6<)G+|0;fd>SKw@rVX* zP+tYMMZ=*Gq&fLX^b(T|J*sIYC}$QH_C$#8gj$e~67|a+gt|j32P9SEgH!=fkpXhq z%?5_5Dg&wMcqGmv7lgGVDWfn~8gxKV>Hz4}!p;`ddk#icr83CX2_)%gaJv$HXvi*q zaMWNH`1nz3Lf=42ByIo&XFC0Lfzz*GNkIaKp?Z6ohD0KqJar=)L$?H)fp!RG_IB z^we+sUlj$7b5IqOuDM(ZzHI7cIwGHtlqLHc{|90xp%(RT{9i(e_fvm4Q3N6ENBGb| za#d-%Bnd|a!2iMhKMHJrM9vKWmEYk?CK_Y`^l|tCXzc#Lq$70w!>1Hn$On>?pc_ro zcqx}U`hLih4GbDfRrxc&K=q6SXETsnC7`qsw1a``DV&rS69W05X1NGC{-rWd$q*#a z1~)gzW`&atrcdTb%Qp`JFQPTr>+ zHdyinSQ18#r3sL;J@I}8iD&DPjP2`@V9!jVBYr5$H!2i|?8E#q4|E=x9)Qv9$n-#r z?nb5uH9%)z^f+?+V2sWp)A0%i6gsPc_8}NOjoeQtMh_>`!!WufnI4YOtwH;qZ4cu}`{qy3OF)EWP2R|dk7QzT@JaNgfe5`#K!^>4gPr}v}g!u=Fl(AUs zf5UPA^+EvHAT^dBUlI!vZPQWcR45V=E@h`!8Ji~rx+CUNhq{!h&xEXi0 zi1N;o!crU}Ql_FyQwq}1fpi>v=Y#}400}@C76i?z;we>G3{xB}Y+#T1|D-j~8i`Us z_M)aZNy=`8q(5}8!l_p$C<(?shahtcn#x%~h~N1XCj$J8WcVc$Tn;SoGcVXJAzamu zHGXz_rdj)`@|MQpcYBbPIhKc%8_9@s#m*|{)Q}`qdn;T<;QPx7ULYR$dp$VTLX~%j zSb+5-b#-=E`G?rx#R}6nem=M_NU$p|vv3pA2+k|sGOV9ea>fq)&X14xB|7)`VF@%~ z!5WpnQJEV12@$8UpXy1{qO?T9Z79KSTPvFssp|BO!Dk|b7e_eNmAKU5AD3*4wYLrS z!*5kal9gOV_r?3SvvDKddbpHjE0+_}26^LBhcd*4;2~q}hH;9kh?A*qB{y%3?%^H5 zQPJJ;nTWF`%@BTT%gHk>#QF&%&onOG*)d5g2Imirw8DgDSU+|w(u(xM=nV3$jcW)o zE~H!A0&7o=EXL^dB1Ms#!6AWsYvU5X3wcE*WBoA5D-z$CRL=ygeG+hvP_67RFO1Ie zjUY;KUieDY*9p|>qCHDX`=fTQc9HfOoz^-fI(>EQv}ft?bOqY?biV4g*Bz)kLU*F> zQk@051-eIcHt1f_d8G45_c`%j!g2Ed*nOGZe zyi-b0xVy@$FNmeH5pw&Y=2Fnfgs4gW981;SVI^qyhN%pakD6Gk6sEGp{&?G9DMMQi zqNZ4K+*KZNv}Bz$h^i!e3(|FXa1WH65n3-{s!|?~_sa|vX?KOF$z(UR5S^wvX@zDl*DRrj6)KC&tqB9vD zS0c&1Qmo^v%hEDTF(-NIB_Z7k#tm)Ikbe7>sWiMvA%i5148j>y3LnGL?SX zUNAL;7_}}8*ZK-kshP>N>AKIgI;Og?2pdkqfLbP}xrC}xW!NR?WW&#sBet!ywIE+X zYQ8oF)HKbFL%5%OIXb;ysy}g9aiLmoU@BWc&@;lLwAO-sX_%%6m51Y6jJa2LEv;s? z0ELBe+7iv91h5ISqM>&dWH$uA4HKrZWmrCK_2?Rk)M0QMcm)J;sbns)MUC)AZ?u*W z$j%6(V8*~D8kF27deW;;U5gvm8ejZR{U8y;F9F_c1Rx6M3;qQJ6=DWb4er;SBd}Dv zfbm?Almnd_^jmO%(ioJ@6A9%4BID4em{k!P1eO~WO9P2E(S*smL-{fRXbPoB2GA** zY*tP*5&t}Qrke+_*8%Mif;^!F#GpjF1~$WMhYM`}Y{a#1=3CF~StI)E~Ml)wCvb4Ddg;9-=nu>4DOcP@3pa zQ0>BT9N`ChW5N%{_Jki`0ag!WxqvuJNOpm~y8$yfJZkl+`PCmKxTP$N&vB7SlK~75 zTc=?#n_@VpObEIu;7^8A8bTTGT|kapWEN%o1XaNZpdE@*Nf^I|1&MYHtxNSASYsMk z=&|26*WWf*E>^4rJCRj3DQDpRkn9CY^&uu39Sz4gzCXZ0C6fwyK>{f#mnws>H^j|` zoVT1{a|Qb!;3mK0uE=>-)t^F`p%p!0upgnV{L67yzhy23MKJs=bFsym1fc&JnG5WH zo}OO)qKozNuDDtg^@#`^QKIBvq+y5yB?lvpyijs5;s_BX2P1B?rQ~45?NXE+j5roZ zBZsBoNJ=s}2cr}lr#CqVBi>I^94Xf#jzRjUawkH06BDt1qR3hpZg+_wt9LkpLrE8w zg7s4`w-<^Wg^~J+$L(h>WO@+Rj|Z8KBX03rqP7^1&kNO_XJ2mxBn5>Lg7{)wus8ku zNp}}B;*x z9_%0SuL@#=LWq!S6_9oSmIG?RD`N$)kEBKw8u1rjYax6Y@G6(uUP}&h!VolFMlAkkB?tw~)dzEVv)8Y15F(FT=Zla6{{~uuo9}dgl zqa_X1%b-vfc$s<^r9uIOcW(&>p`e^0ahI^T5VQb5BM5Q>AqHUo^TX?(^)z=^hUz3s zz;7qY>7nMyI2RYfld@kp$=qW@iBIA;=+ECNV8kb@Mk~X8b$EY*3+L3EZ{=z%WeFX&T zZanS*_Vahrd@P7{F_Rj9cc(;(2v16XLApsJ*Y5m&IC&wNc;nnxct1Hi!y&iTu z$*4^J{5*BcKr%T0Jn^BK$Ul<+zB(?wC+LnF2?|;luOkZbmDm40izZ+}BdrHKt zsxQr3Hf!1JbsODg=FxZCc4>9;UB;=FwciVJc^U2MjD`)&*imr$@WP^w9Zqgq8f$hc z^u==a9xacS^EVE={Z%KY!)El6*vEbmC=$O?2*g+ebO7YIJew*x!uQIqun0lzi(-h zRa4>HO7W)i;0-z}B2F{1e827cgSI4f*28_XLoTe8ynpjtU>#PJH=!yap(JleWl-^e zLtm#%47_>H=hdamiCdK|zX_gr+N?Y3!I_iwwD40?>s?t!Pv+g6-`lxlO?7dcjc(|j zLw3V+{4#drSCzkKuk>lbw0l>U+|u*n_Jqqz_oOHt-07azU7doh7p&nNdE3%y+T<2{ zC*4@V-EL;^;EGR1d`ow3s^IxUr{hLzU6=Q*5*2=>uk$<3Oet(75cK1 zYw_MQC+v-~o;|6m_&mU5y0~P=DcZ)>tFrf-lqK|EFl)kF@yhbt7k2lWOm3}p>#AkZ z{0;V>%wMOEaO|)w)IS_1)3t=ksq|~XtH!rjn3V0I)w{LR&2@T;HDfMFMiu2W7moDLf0uhQcgLjc zv!1ygIn6EyY?^cB{+PU*<%7-pT{?fWP~q~;bPe4}s9Z-s#3Et0CTqOgUL4QVZ)0Is(iQG=zZ zWVp^pj4pv5SCyV1$DvZ52jRD97ijHF$XgRGQVWL-0fk2x69YzVsAH<#JF;p2e4m5gR-_PoVUv zF^?a^R*DoFSVB;iVWTP%%&T(iKn4K&1T&PtX0AweVRPdVM_yL}pBt}<5roe1xL3%y zh44cItI-D`f04i#5P1c(XUH>xvNQ;~Fj4&`IC^Ld*hF7KKpH^ISOBQB8Vg6wk&SGW z39J7sG=*SQ0&WWvL_#iL6lw0%KM&zU1h#A9C8X^WhKfNc&{k5_U;+DIgmW6&p_;}3 z)-sad0JHVnf6ZcTx@2o2H7xWLqg)Ju1y!F46n<0KfnF|>s^#Yhej zo|Fs9gQEsg#Lq}m-CE#m4ZBIPMgDw+Ovr7}dkK`Xh3Y)=VF!gU0bc^KgTyMz%tGq` zsvZkWwvtA{Na(l0%K)D{;yOZ;u4sCx%FIkS)uA*au#*Y^AW7K;8t~%?M%`pU*_C?U z9K_R(Fo_|+0bd5a2dY-DVLwAmQJpDdW>ffwvg0S2MZ6`y;NJwD$DlH>97cXtwkkHj-vCVfQ05l2Gfh0C z_##2?l}Sa=&4uy`+zZhDFR$H~dQ>p^;1G;6prCX*NWtuL?X`bE8e48ZuQN**sy>#+>1NjnTzVj)R7~{E8x^;Fm_ zdYh#1QRL<&ZMCnqscNCM)k=S>VfM<~x4yWU#5k`lec8VkuS(z2v!&PWJ|`YD-L>7AX-SwGFMyT^idb*s8eSn=uGLc^TEb~k49?`rnv{!WV0odt=5dKk~2x_@lp_6ggDC@xRG zDP_KN-8X6cz11DK?ED^My_}c1WqS`|?pv62vfRg_U3Shg<3Fc)8SHfGrnki8kZ*^M zw~9*zS%snleSB|Sz2xpSW6_BuRzJ`6qB7gORy&J#UeZ4qwW9lhG4cBrp3GaEZF{`d zw(Q#~n{b~@c9*!?eYYH@UG6`fb?fH9PQ}evna;|ZarwlWO!m?;;aD^FjTMR8=e}4J zyMDl4+pzu@x)#T4UoGplsZXm*&Eo~zYD^<4(=1wT-hb#ytG7EV%wi9;S+ujC!TN%B zd%AVrc|3n-$7+^on>~Hajhyq?qpS_jp4eQ_&Uv> z?oE8Ub80A-c+%pjE4{M^6BzYYfxvuPy>-6V+cU=4o^tH`pdFg#vhD~xFH8uL; zBOFfSCYvk_dRJ*m8*pet$t%;H*H>o9nnb>^-!x%I`k_J9d)t+iG#l-*hx2OCpRd+m zroYHqs?b}|FKg6bofm=Hz6BFE8eD#`s5rGnfxmu%Fuo>fZrRB&SBLzCkh4BH;xXA? zLMEixdjxZc(IIp=JWIqBxQhL;0QpA8SBaU)tb`!ojR_zA39tt!t}Z~{PL!ZRN(*Wr zlO7stheRMIC-`$vx4WpA|3)w(8dhil`<}|dsg7qCq2zjqXC}MK{W(lDTUe4%F60Oy z@fnMr1Rj>q^Qf;Ee(@7T$zX!`tZ_k1rO?(E6K9?RywC=>V^U3%H- z#r=?soNlfQL;7p4Hs16uIM?@sU080V+14A)o9y|vz9oX5R<691KJI?bxd*SVDD0Pi z7jOFU{o}EWJzaWTvl(%2<)y_h(tOTn_b}~#MaQ$~#dM$1Yad3hYr#G?Yp8$Ou~*^G zU56)MU2wix*XjCiI!?}5(1AHQ$+<*)Mbf72JE?v0v5~!+m7d)BVIJ4A_w{D`#st05 zt{%K5+Ntyl+aN<=cU-IEABv7IwtF7DE^N(R*l(HkYMr1riN<>9(DI^d{bOriXE+zW zjh|3+t*%LF^Y@#z(zTnsZtmWF?ZXzjP6_Xv3wyr%nEX|=_uGKEvtIq^sFl8;gBLc4 zCN)y~+}AO=BhQWq@a#2X(yc;^vVHpwCnk=oxi{4={OGaC86REywYexO)K1y=G^NXP z{>Lq0rHi`nOfl?!#$Z!I>Gy#C-R=+4YTIi-zShh1q+6S-yZ6k>X+C{k)N`XF#RFQ5 zSQFN?U8~+H!v@}OKh(2}R(ejG^lvuZUdV!-7EX+uyiu#&_?1pNh@vU)ot+upHSb&G zoj;2vc&4{~9HQMsx5e$Yvu)kZ7&M!FH-BpMN5R=StL9g=Vz2G9TF2>4q7KH1DbbNJ zQwraTCe+-h8y9jM<>END!7_x4DIH%N9yV#?x+hb!1>mh6$FKB4-`XMDSm9!G8w;8fc3U8VHV%u~J&4$L}?^*VANbR)3=O_5{ z7kW!S^mklfE^?0Qb?_QftNn-oy7tYcFN;G;QcXs;X})nsyQ_zXo*go+?Y#)?7P{?j zx1A4|$!Ml^@1!mp-2cf6521)JM^etBXc|P3`~(ez5_36&ivVD}Y&nD&5#byH$Aq9CG_@pzO(Ehh8JATb{zpis2-Jjl02InGe2;`?QSc)b6c)?} zu3sQfOD+&51hG*=$2hiJF69d0UqnGXnV`#YX&3;E)FB*19oXc7G2W;oivmJIzQ)ib6i*{K0!G0RD5QL66dt-uAOzfB zC=rU4Vz@PNF;E(#)(#X@gG1eLZHBan8h{-!=RlS$)s+p>vyz^nybJMz43zLuFb(8o z16l7-iC4oI$?<={OAjQ-Q5Q->dEHN;StR09(}?jPR=I)+Les#;LcH}Tri@>GF&Y#| z&72MC|Ml`5E9HEa6kfHiU;_ag8O$h1`9!k@^9gV|{5u%(25D)LG^fZOhqpUQfmbXs zwR$~WGnQ0y#nu|e7@$`+B7-;>FEp@vBL-zVEUlr)#1uzMbNDaybJ5iEwTg9*B_xP<&+SBUYd>t265m*$16#2vg0O$!;si1C38?CWAg(l?yUMwg<_u5& ze@cmmVIlSY&d{PG8ZQ9Sh6QWGiw@}jT3Xh2HWpUqHWu(E)l*;JU2TDPx}}w+ovjVc z94YXwzM?xC8w||aAe|F@g!&4B<-{6Jims=CvVJ*Gldjow$Pi?by$zY52BeHqaGdef zHVj)PJWut@ssGQtN_DTm+0axa>0LR(SRS4bL+p%{q$ikWfhDQ;I4FQx1^PK!!<9?a zvot{r@?#4??o(qQ7n+r=herT1bs*a)l+RNb18i0-Avj`a))wd-Spe7ydP9O3jSK)5 zO1OzJ)W(9m1dzh?P#Cz}5sR+YON5O1pl6xmILZgP*c%($!?u3t*T|QE7pWg(<3{io z79kBjDY512&J!wBscf+T^?z=)MJRovLa;ey*l_*_SDX4*sa6~O-N81Z=@XU*N8azPR<6mR9pBw==7n%JhS|Z;mK8o;-8>Ixlkc#w)Hat@PfW zGmW2Ywx;D1odaUmy>jgcT^ewfQ(Oy}8PR{d{i^B#wKLvUmF~ShK)Afsxbv0Miz=6Q zU*%RY!*6iwt~A@bD=axD{N$HTucm#tHS)p9g+{N>>Nf{{fi7=ZD~5MoVbzXX810n2 z9xN9dH?w>OP*geY*t|OEU$U;wW%gu9TNS$cg1WU%;k+MeOcMmE88g3e5B&6`@~}R zv}g0A&)&7v-m!0=@Y>2wMlq}RuMWsJx_mRb5 z|MBlaMH8zIJiht1Wa7^d)#Z?c2#gP3 zeUl-r+7JKQ;o4ZXXp{l7YerIPapvkReQl1`$-5+m8z=I8m)WYAzwgO*^Wt^qY#-Qt>R7hRWXj6u>3u5v zs=5X)542yh>V(IfR<&2`HqplBoWEM|iq+~0FZ=P*0Z)tzbMtQmek;A{npIlmUpRNw zu2$iDheYh%eXJt0@2puncfG7xQkoRnWqoL7P^jmlNqS3t7N1+{cg4?-f1y^u51$;i z`C~zkKWlB{m_;25r+S#K-_qCbtYfr(-&QR-13DdlpDh-0o*s%X&9++B$2E0mb{# zMzHTZyqLB-*FyBAY8PP>1}59UyXIqJ(ew${7lyo_eemgtNsc{+%-&Z!`gkU1$-KM! z$B9I=P17B%?VnGte6X3nd`-n3htjEz)+?f>2Ce=$?&_fdmi<51ZkgQ8%?7&v?P$cD za5Vb_GQq;$+F3ztVFw>}zwLi`I#E45pJ@GLq^SK55$a)mrh%#yzwLi|PI9PB54gxt z3!Ge$3Jh|vGzBMiZAh!YHHCUg1r6|>Z2wbbY5i^g1M&dC{}N2zeOWlH=hNI?wT}t)Y(MF|(L}ZsG`@YM*@4G^l!7vOnb|Hl#A|*?OCy(8vZ=h6JJ2GO()AK41kHpJxRob)udtw7m@8d*TO7=^h-a4=i z5yY7+ja&$gsw#PBwCJ-qX*27#g?Bb8scE39+EjF6+tSn5o=3H!-w9_s_7AyC1xh+7 zT2)CzW%u``dp%Wt88XGMRlU&1ZeP{DOK*9=W`6@qfSfCGEN0KOWz=@|*x>5q1B|C$ zdJ6T?w+z=HPsx*g+(LfZ-$$I0znXekJ(A)wU2c6vZ&3SWyZ`BC-{}-4r8k`ouXeyM zO8J#0wQf|IAicVOCCxa4{!^zPdG)<=w1lnoCU{Fq+3t|(kG)?%KC_f&)w!kjoSd1% zzTk(C8^sS5e&gHBAx3yXbS+W{f8O6Q~77vAjdPmDK!v$RPT-a!kVEPNwptEeJ)7O}f)&uMBp z9fT#bUvvz$h{JgJbcTHVUH>GS5${M%me}1!v1o>evE2UWduLfGt@?(%2Q!l|2)ifk z;c+jB`S500*xgUgD5E{!ngXsPqHXjr3YJw_r=0b1n1fsM@*&N;-rFV~MjA-aL&OHaOBVLVVfJ1v}Yy5?aSuVtuo z=05#fSI??XR8cNct~4ivgsU1*Fic)UnZ>G(cO{>%OO=tFjM#D z*HVlXyGbMjj-)h5Ugin%6_{yh>P_N~z5V8`4{zc1rk3$Sg^GA#&BHWYr>JwhmEMNG zvTyH=+aEi*dHO?ZRq%rYKcuaBoo62=sejveAU4}CX*RE`_iJNTmF*Sz3+kaEUu^~Y zbj&YwO?>V+yYtZ3x$(K)-ui_R*9R3xn=g`S_OtF&()3Z%;JErDyD`f+}ei(Z$xAzoNUs`Knc3Gj#(Pja4iT19q&7TGh z)ta(?NMBxF5}4FhXelt0_%tfSI;gEsDXTR2b%myLrQm#J;GK$;9{rAY-ns>ku7)m( zmOl)9m>l|u$@)@k!;j{1RDk#q9$$y+%L78(%k4skmW?}}*U5eh%^tb)Ac?l|L#TXR zj_=1$sxkG+vJ;O#i7P3H9dIy{Di#{L9M|;XjL;>0neg(R9p7h#6cd#-&%fGui{Q~MVYIswz?Q{r6S z7zkw-qpF!qH@kboRGSqpf1N+#yS!M>bmp7P;#7m9)RL%xwtHyRg|E#GXPs@6 z3b^UN4Ud)OjV(-^b?(f|YiK?`Gvl=MotdhC3i1ky zwF@hQ!z;JWRQlg5PXV&(vKRWAZ8Cm$a3^-VV3DQ32!8=LGjGE{oYKL_OmTIO$oct6 z(%Zbb_gOjmRkl6yu6oQpr*Ykm=*(PE-;Q$`JieP$U(r~(X7l9wo_)(E&dn|6`q~V@ z{!9h!f5w24@SphKf8u}tiT|~8$AwDa4Y`^Ic8ZV7A?&o_e^Dd5ep9O&St9*ETw6+URsZiM__x&m6E1;2(*F~^kkEfd|4-P1 ze`OJhKn9NWEhbna)c;}qznB!U{(mP%|G(9T3p5NJwzEeA$CfwXj^PBv#(KbvJ?$I- z=K~;A$Q#U;A}5L0vfxVM;**F9u6kIzK!C?km@FEMgwwGLK>C3G?O2rzU{4}!YSoY1 z9f-XGa;IXuuu2C4>ky#T3R@8rV}{WTIU|tvb_fGI05i)Qmn|)LyCP;OlK8z*b z!3XV)bO&@Ddou3t0zR)80nPavEW`tGD5hjT@ z;G3v=LKA5RfQ(_%tN%2e>lo31RUjcQDJUf<3O(!scLY*)|2(faTPe7*AP*TpdkL*k zaM@sY3;I`g{i?xKQBhP)NK_DG)OEnYr-6|#T0ZWcXvn<^TESqotI>hj)dqVg+0Wu5DUa!jEZOnn1>_igZzuIa~vcAT$KkKy^ z9wtbPGdAXe!a9T!`F|2PSYuuez&U}CO(36XAE%$*j^F_xG#%hez$kLSz0(8<>Xz(* zko@0CjYP5J;Mm|us*cVub4&=_A4|J`5T*aSF5u)`>@=AGxj?YS!SzQLa_9pkay7U& zbY~K-4WhZm&M<*b7Wjr32hUY!&0n>%Iu?+}GMLp`855}}Ck8lINkXn}*oO(G!O#Z@ zz(g48XadXx03r&yR;_h&C+g(Sp24f@{*OnV=-sfMyukf`X3bV6=-)qBw6@^#!MU5` z094kTF+@|e{yJLUH*@{fM0oD~AH$;k{p$Y*hYAAjumh6n0-o;JBj?}7p#p7e7XL5( zc`&jQUI@Yo!~C|WKzxEZBU=<+sQX_M|1Si&-=cwe!@v4J@&BvdQdou1f8PHGz=*=u zjg2Z;)lU86|AX=W3C3$&6n*#!^5}XP;50Gj=rwe_rpHV~n4rA7Kd!u{rlRp`K3*?4 zj&3hyM?m*>!_iI9!US|59UR>k?m|HKHO0}<-g*RdKRp~>$10GJj(;y>BVPi#zbmf1 zkh?7*9gRP995Jp61h!X5ljP`4s7X@R`d2hbQX&$<;=+PLm}ratMVcf~mH=60MNk_G zevuOe74p7FxC0DSdg7F0j&QUm!Y%+*GlMEEv?AIFX%8q&fsbfK0B$1y4M#)ixFHo6 z5K#z4mF_4o3toCi}gdH{l0RJBZ;o4z`mpKR+2G3vTZGeIeY#|+^I*`u6lW<-k)?^jz z5i^d$Vq%yBc-;>;)h%8x2^k9r?X7r4C(hac#3RCxkRb<>dB7|6r)XjPLs)JaTL8nk zdjc4st63?4*c%`+8GzpdP#VBhi-EJmEG|$<3<{5s$I_|@iSH}{GS50Eh6fnrhg*7J zH*o-TKtQxT424zK+CmU9c7$_)v2I{(%2<#aoXzDQ>kMJw5~!D#08H1S;?PkH4)Rax z{D09pL%iE@cLwq1#?guQGLBBXb#Zj!orjZZYg=Zwho4Xwd>pV@2g%04+u}K5%>Uk>Av!~rEP#2SLwI|kS$S93DRVczYnP)r z_Sln`o;Ws4cW{QphOef`RXCQ>p7%{aYe>6b-ANZM}7zCsyH&u2YlB<_&1^hJ!CEjKnUnw_kcZK*6V3a8AfH z=xyd?2~t~PW`iTzz`j|CCiHM)9*mZKDs5YG$>?Vq($@?ZnsP5sGMw^Vxy~waYoYKO z)9KbQ@#s3CJ2$qu)4~mVw$;7;I4Yw{H7!T0zx)Grs;l;HSM8lB{bL@|v*pSiG-3<4 zhGaU9J6pdr3ih%1L1k8)bci5I3Q1B)T~13s6Pp@v`CdHi|IxM@&Gmrdgo59PZW(9x(4mhouCF7Sv2a)% zTx8o-BVn~t)DGJnPKo=QXgJuf9oc)qeX68+0(I|Rd*W^J^au8(jpGzqyl~Ekd*VpC zKewb-!%s-%3s=h4xZRnf_{h4gPUF$0M%()jZtW=4a((PG(c~h~OBFwAJL|dRH*@mx zLSHY}&2Xd8%3f_gc`nB$O{3iVCD)H%xm4mCcl++$o`F$ctB->8bv9{EiP;5V--d3z z(nq{Jurbm+udc24YYF+jBK~fkhpDbUnEWAr^kNTo|`|%l%9aaHwaejm!cp^V>>xa zPIi1BQ@0hV%hP5d*_rL7Lv%Jfu1PuFyBH$y!)?$Ec^dgud{f<#WCnk>o&ud4&$_9lcG)G**>-fzemzFQ$Zc6}-2lq;`bS(r?p!E?8mp z!It&H73o+y)>)+{&yL$eMqEi&S8LK5mEj7)*YiEf%*sx2&u*IAc%6^MaIUm-?s~jx zSlJjnfq`Z3IfnF}rw?fYqd5bUlI9C;=@*0YKM;)zS)>Iq7xmhIfNL9fNjm{C_67ic zHW;#t;RB$n6aZNk78l|blmTvf0EQ1biM^3)3a$?*@b7~L2q~@ss1t+~8NWdT5E6k@ zvHk#bFvKGK2{-^40Fo_<^|^HQ^F~T5+M?a?dE^a+U7E*ce<;$1*p~dE~5f{!^2h=Ff3?Pn`aF9hi%c@a)o<0|u*IIY6_gUpW zx^rIoRyQ*qzEqfPpOc_!Z@cqq>_euBZC=rVxH~fqBttE?@?*rVmmQL;TT;jxEdQE+ zJmg-)6yi=X)$W@nJOO9Lie};l z3N!44)N6C{j%t0>em-hw)ASV9h}=4Go5`>$?U8kM#KwXS&PS1kbIm>bm`YB6X9`P7 zii#{Q|DLWNG8@`trZK@ve>LdyLcSUO0a+HCXpQH?+wUFW^e^cjSG{HJlb?PN&42tu z6*aB3m;S|_QS|=9=t;dx#@orzDmm3!?CL4xDx4Q?vrUC;u)C{5!yM@A5?U9%bl)%fo&ou`rCqQy+?)X?fSi_X?htot@RS=tjE3_&b4_< z{+>EZ`r`$;mRpZE-Pq@UF6hLT`lg7B*Jl~IHpPU9b@XZRA5>NyFCVOjHMpC7n7_yO zs$oi3Y|5+O)!mdQX&3tYfiuBJ`OV%39nGX|dsq`#6vSb&Oqs!+Eo+g4kdD*$H&Ho9 zqY*vGTe_vWpK9`kRl;YOL*>0q@)l0vjgL8DH|weAfLJZNn_oz#%(&vzM;?9SRNT0L z=KDAdOMlZ(jofhP5S85dajr6}d8O)x?kEbe?-4QdviIF2Xx^)~r8SnHn*3%UFHObt z$tLBKkp1D^bq511{IW$!pZ&NIW+}#L)-xnFrah`yVKSS|a)3ThW|F>UhyQI^y2~{U zX+ziP9-QrIxlr;>bUR}w?-xVI@yE&Aq!JvbcR8}LY(++dYfzn+|YgTl^6AUXzCx+h^^ecFn*6lm9CFFrER0a{SS(cTd#1` z)=dWAP3hl~GO~!gIytz>asNL4k^S4ZiRm41OUM*V=iH_U@c-d=7Y4?@8*=L93@?%g z6}VJ${*W4C$~(=y;i$`U*RkaBrBU@9r?1~l;R5wL*b@zJFi_5%(eGpEH*ig}3RTH7 zkUzckXNRpgFu{rx4c&8{cSi)VDki-koq2J?3QR>au;R zypsViTM6s9WLaL9ctqMQtA-f4^7h0MsoAHu?kyz#&>DFCV4#4tII~gfQk-L4?wHg_ z+lgC6<9F}y$@Ei`Mi>YTHfs#+_3q}aG(Vu=r6N%l_vjo#*-eZ!U% zF6q>P?dsZDN3t`VXZ&49-irCB%?(jB#5;U+yFS@SR%*lk=t=K*@Tsp`qqmNVmF%Y^ z=Xv_Dq1CIS+fT&l*1H0?(tOHU>pZI@b{fusVPksr>FBWbtgY|59J)J1k8B}()K4is znL!Z}p?l`A4g9s^gN@>k%rDLd^=uM6!M>Q79e|Ruhvin}k4`IVo zaoLf_^wr1p_8a>uA4`8!OKP6ym7g{Fu5@3ASlhIDN5@qymR&CBTYKrdxlEN)+Y+NM ztjurIJI!^kEB^s}vg}^2rR4dvx%1PN4omFr)LBgpafRPy=gwbs({jof`Wo?-f1cWS z&z<(nK5<1i{oQaAL=W#cf2zuM*1)$OozCvnJ!i9dl2bRa_fpR6G5E9)##=Mf`_A6~ zfkdCT^{IPQd@lD-V>#Bdkgc2GUsHDgs|HKQEwFr zgJ|v}8`b4m_^Ct!3}?P;`FJ~t$#1-Hig*6tczZ@{T#|YB+xKDWOWo`*dzk&1_C&Ey zcr@{!FTJ2T8Fe-)G3ryLCk0EaWP;6bW*XTYGO617iBB5ol@FVqn_rT8^QOm2=+hym zCI9@lQD2Qte!ioy^M#Vhmn@XCGbitZr$5vMU!1C><_eOi6|Lk99oRd{og??Ia`7IQ z*N-E$aFdbeUtu^96|i7Z(>bBxN7fnK}ib>66U3b zd$Vgwp_K8Qn~^aF&&Spsz&C-RKxi5;TaDP-B-a*@?6K-di32-<$m?}qOjJrzOiV%u z14gtiHqPoBLbVW%48*tvnXT=ElQb*XJ637P!xH?+460Pg$pNh)5ZveiIC;nhtbKV8 zh$#ipe_(#UcqahB1}BA~!IZ#&!W*MS1TbFsF;1!A2+IW_*7n8`6KjCgo#JtZelbM9 zaDm1x4oo}iyXLPqy7-~PKp7FmN38Ft4|EadnhMzv{&=B4gSQH{L^K_+zvTcx((}JM z0REj9cQ8kilsc|EH01@38^4O5pwv{?Qt~%<@`V=IWR4hKCDAcKfzw zJ$cOJTUht9UE`rXUFfTAQ=GRwmBQ%`E|6{3StjX^54)hCBH36pyv+N#+~NLsD@&;} zi80q>5>L$yr6<)@S;^j)8c01BK5>^qJ<)pRY*{fnj$ew6KQ|VfbNZOQKjA;7nXXd&Ay%7lzbm0TZ@{+C~n1#ezka9 z^j>0-I5wFU=yegD79G|C`{(|^$D(=<52>q-WjZ{vnUiDP) z9)=Gy#)k)#KYwLS>flGF4qqLMV@ka}x75=7b-`UZeQBh-ey7L`@A&s?RW>6HI#z0^ zq40f)QvJ{3g7ybX#YM6gB)sX^{8&o-U3|`b!6sc1ZPwFm6_Fb4JfV-W6ww;>CogmI z&|SAfn|4nw^`8+tsd*E1j`twdV}{skwGpWSksX=3XX=+dKktg&^2M>uKlBhS#i=CK^e)ooyN*<~XT6#Kc(qervtTm(p7khd$bUw1 z@lbMSc7TIT3Z0yuTd^M{{npG8#@;11VahQp1%}P)D^W_(;@YjYdVM>^Hyg0Ol zk>ixu`-$DHbGbIfJ(f&@`^j{Y$tF(~D2yIh7`U^Nca18dLRsg)c)MY1`o^fmB=5Z! z+f5rYdxN_AjTgxe9eB?|3TIcMb=I8R2)uIRHmzMBZc#Bew!DnmIju#Pow{LJ(8scj zWM^*|b0a)p!=vor4ASq*n*l}$nR6#yK&c^Xl-QSvg>1*V* z-72vxr3a(Gza%-ZSgZnKGBp5=;`BlXahBaFrIu?*cMeY}$WChQj^-yKZ; zHlEq8D`2R{e4{1#+wD`ujvq?%BQ?yuWiv-QKAeib%F(r*Ha1l1ZGr^jMC|^=EgczS zW2coUnT~*$yL#%8QXUA>X8duH0rZ8?3l)Eo#%diXLv!#z2cTJI1;=`|!$0BmS zdHK)O+}Qb$+p*N>#@GHIQvE!7WhcQkA&Nuhj_&!)PVA?-6Wt+vKtAcdIusNOo06#bIvi^EaFW^_EDqHgN29g>XiuZxTN%> ztG50mxg^;c_~rUHuTPE&%!X`^*~*j^%SK|hSe*BPv$}bzj91b3N__O5g=%XPu17Q5 zx|j9HHgevW^mbA|5Iw&{N9N&NySTk)>3&U%Qw#5z9h9@ibC#1+MZA9B7CkbanJ{bD z1*6zb_6t`nd3Ncf>%UVR_!zo)C}B%zCufJX-HPv%r?UO~)v8urx1Na8)TfE{I9nk~ z-4w%5PNn8n=Ov<=ym)9+0#9T#9|K48bF0?}>B+=4eE&iwFJgbq;+U7=_ud~W)oEgp z*)WN@HZiJw?!iBXY|V2MZiQ!TO24?h?Z!E_g6?VI&jyAO6ji$V+G7!)uT=P@y9ygy zq`0>HIF+fG@H&Xu@Q9Q$shNwB?Az}{u~e$Ush41TcI|rHHiCTC(UYH|ueXPfB6xps z*^K35-`COy#eHuLp&FlSEEthcY@*lD)ucPWRCLlY>H$58bh9x z_>Q+`sf0_lC!4H;oo^onN7i-&p=&*`$5{=Jzp@>SDr$~MbFI4T<=)_ZwbZs~)K5}% zWCInvlh2GR$0f2$b#kZ!rX_Rk)AgDolaBANy4H+7>AMpZnN2fV^loRtKGiKSsuIS+ zLwj~!g_AGHUK5-l6>Y#glPNa z)@p|L;q}{XEu_CHZg%1J-xsm#qJgvFVD;W{>Mjv#Dk}8elV5LDOrN?J#~(vKpr@d) zqmReT@A57AV~f#r4DX-q(rAeI_|m}h(KA1)8Y9x8jqe6^$tQTu+|;rEcuWOebmX{{ zQ&zy~%6o^hm11|E^iznks`$!bbn<=81(Vb>h4eRiRH$~`T6#hfzYuGe-+ew z(tMqSvb`!sf#Zek!{Z;EZ#eo#hsq3zze`CyiH_RruYdOetyZPh9WibL1!VZFu5y@{ zyvQqt9K#F0s6=N z1Q50VY;Z7uuPgE22o83$Q}tVQHWM_#-8EiPy0&=NN<*|aj_$ga9*Cnmm>~>T>j~h; z(Ip($)Z>YxYa9AFW{wJWX%Cumm^@17%NX-7!M zyQIR_jsw1*%2sfKaWlfZ!s?5f6VmZP(jsfeAMgI7X|F1|I?j$3!2Umt>#M!9E5UD0 znm9UI--KWd1zP?gf3wz27Q>?aHU0(&xCgwtBqc=uMgE3Bius!bT>t;fO;$0$lwe(p zVNg{F-DIVV*A&o|CO)}vE~q~hBtFW1S&;Z_`(;7m!|s;_iBG>@79>6ne_4?DeEelW z;sf)S1&L44Ult@jQh!;H_>2V$g8n~PSAsuO(Gx2k*@f0>BJD8F6>C-WS}Ka#t7?2F zFG$$=h5mUxB{HDDloFAW5Cqag02j!*Dtf30kitMr@)s(4qCTMhT@^i1eVBLpt%@FV zj6q(@5LOaKS?{oRY>@u}T{Y2I{CQxV2z1rnNVy=Q0r31=3;-rE z#p(e3TgrL^OWy#3frI161~@l?oAp2bw>26Q?1IBE!)9j! z(?6L2pj|M>pTpjcn7S8Cn&qX9Nhjy zEUz)>RPODKg4+Y&NFXd6LBieN_6mY6a9U0OP88ls5NNFqf)=5GBvX)S6PNdws65f2 z{~T!yn9lUW?uh{MdI=E@E--SypaT~xaQClyOQLQ8fIq-c0&s*r z_P{7Q0JUKY0Ek82z)a)rqV?}E@cn;^zJ%UE(Zdl$5yDpXyD@}pg>dfxf_70zgc<~C z0mTF37TX#y8lu-wL;&T1E<3@U!9Hnv==*qsRVev*djsfQnjRpM5vEQk7WH>S0#@*s zaC!*}vw|_3&M*|0kRUhV-XK_87;b_PlSz(s;~$m9Gy!dbKZd8mF10mjT37R>U}tju zM7h9ZAW=Z@HIO0~y#I0fe+ZcZn~H8dPd*~&JC{HuGh0V-HFXPfK?6f644>|faUHLV`l#Kq6BhUE{BGAwUs9Ng*i-Nihj% zu{rzLp&aFcB*i4%6s53JWn`!=%?hOi#&D9rhQj8AWR+f*%nUg&{-U4js$`&^3IoX@ z1^=Q$DgHqr2s{9En#A@^jurR-vvX(mKzcJfA$@>#s;H;J3<8-k!yN@YfSUtDhTeN! z-#Uzw;LjeMu4@;v-=9zqA-xb*t1b(=n7}if2j{vQgFrp>v*3@JSy3@jQAmY_7yRuc z#A@v%OjCToZTesO+ZtaS^J>{TzJlJ9=DdxZKtoPwF?H_yh+?_Z(+=idR@9|3huCbc zz$qRbx^`QsuPd7FySatUOJ5(~zR|;H#|~~`%n56>*rRL9JlG-`e{IY77KPd%Qp-Ep zDwn={{CKT*-DG;W$vZdng#3@o^fL6>OYxhoNKz;tczu!T`nMHRSiDq8?lINs4+old zf0%@qI)9P3UM4%^532~h{Ni#=?v?Znj^Fw2FVhFPkAA$U6g)fi-O2LaO$LsVoUg2N z3E5PWs7FsH+w6{c|6Eq_iHA*W$)B^12fB5B z6ESEo-sMgv5dA&R_!jAd!@UAS_wRmPRyEY+EI))=+4hCWyTIX5RVCuc8RG?}>)elD zoM|@o%n%hU{c=_}Hd*h79W~kFl+@um3xn+gjTIk8^psglX+E>;-w4Z;7UaA8J<#t; z$M?{)YUWJ=KI1Yv!$qfO3%I`8+R>T^SCIAxwAuF041XlCxheKE`Pn_4s4b_8Z*wJ@ zss(T6Io&q?NOn5+$Pa^{GlnCk?3Wu<{I%ecqbHjt_%e3sE6wPViXwa)GMdhnr%vV5 z6g$2!ePZC)9+s%ka`vP3>z;j{Tnw7|)iqb&D)z`e({_5LKiwEU|vg_d;4S`qR2b-VY2!^+CFYcpG_NALEE=;d8{_(;0XTjQcoim`RasqrG3 z5u1g;l_E~I8$~4NtNE*p1{2@M&D=nRm*3U%zsH+QQ*Fgv+vD(XW54Pl^QhxZ)0s5~ zHBvi~BgwyidJs@iKqZ^ACvwAM&g6$X{DEQ3xgSAIyIycv>k4 zC0kac&u*M&5TPKamQ`arQ+X+VY^B#hMkli;T;P@bT$e zAmw$dJn-P{CB~4vtRzbI(X#AnnCf7qI=Ax7+tV~1Vg({?p^UrwD=g+uhRJ&6JexS$ z-q5Kz&m(`k;eiuR`Il>-Vmrlysp7){``>w6kY&<$QuPHmL4eH!fVVNc0St{(ORQnQ ziI_4a0~IVQ0$d~>7%X7|sO{e>Ge9m$1?hmzQ14{t0OlR{v4^CI->biXRzVI9%T@vz z?Lo~61RL`kBt#+vN!(YkB4a>V0`ma0j=y;vLbd^TG=R4P1P5=R(KV%iA(&tX2b)j= zDx`ySfbhdD<@g}wGL~8U>lIk6tzebgBlMyFE0YQ8wm_YhE)3-igK#X@ozpc;Isw-U z$RqA{fELpcl&A=$E}}K_+XeHh%7135*8_3Y1Q-T84{sv?fE9(t!p5R)v0n)lt-oJi z(BeX)kq&SGZ^RMXeULN9u2pFM{=_SS^{9FP3O*F{gbo}~@!BD9!p??YNCa3gBQ-bz zhLH&%DH_*fA9sWq@N%H)kyV)AAF9I#X5j#&dq?4ZAY4~OgAZa_f@29OVS)Y6DvJi? z24=`Qb(GZ`=I<@wfJPr>m8W2a{$yGB_^=rH!1rq?iHJAS5urNK^nb00V`9W40F>Q?IN5=4n|0 zY>hx8-tGvXMhvSM9E|Zz3GxQmOR#2XU<@#ySfz2Fm?0jR99&f3xI+pFAe%M@IvF@N za5z7B8n{*ns1AUb497w463`uSa5k74@qO`uLskoER_q5>Y3v7VM;viR8{p9La6)3` zEpqZ8IPgsx5Kx_vtkU5BvE|9hkxou%n70M4>*VC_cK&cENde{*1JrZm*jnXef5G39 zliP!wH@>x1jUIt=5BMkqDE2Ue3f3wnEDrkq0Dq9c1Af4C#!^lk6l8!uD7*>(JFs|w zDMWz5Q$R%1>KAENf-kJn1YfXkiB0pd8Z7nulNCBqe$p2G<_5rL1u}=(E1{5nXuxU% zf>54wR0a2Oz;Y z{}!1UTQPXh5UPe&rMk8HFQRUN4gecTRY*uxNK44hld*1K0{{Cji5h>U+70JOA}RQQ zKN1qYcpXC@ej&W{Y-p*RQf(BpBf4kS4lg_Y4?87(rulE|KxAGx=Z~P#M1B0}M^o(T$1AYmx6VD&PApIwn z09nNu&InrUx+jTK^IN+FJpX9j6P5QO`tB)Bs6_$t{}llD{{_?2zUPsBqwaNmUD~(Z z-nsUgUh}fc815F?;{>}_v@=OkQcd9K{ljmqK2c4*c>Gj9kW_ewTb1}NO$X;Z*1+YK z22JY5G@i)HIp*^UuSjzeTh1|X&ArIET(iH2mzyI)>CpcA~1tgO0F_i@na&ikKSy5ZKqpsMzH)qG}UP z{p#fmJf@MVT;*Jg_|yZ^vcT&s8?8^E)kzm}2TrlxZ7tC^I*FWo&*z>wQ?=Xk9Nm!- zjwUH~j(JL}yY^dbkG?wh>5O~Y*Oy0(zN9j3b1g1i?*5YB+OS{wzMaXyt|Xhwn<@GAFa&bNo`sY2iRY3vqFKV;J-x8SdDMI->A3aHvaD;c(<2Ex zq7-(L?;JIZa_hWOUM6i2G&>3lNFePM`1oq^{{8zVPuWACmkDuc(kh&-%W8XYWx

    vO&yy|JZ?lZeX=HzXJOK;Z@VV=~@DjI2YuvX-5teak6=6mk+4A(}FKF*y zppvQ*H!MugV?Z4@Jg2R2Ie(I$->h7Bd_G?yJwEB0)P%^dnp* z^yF_O8U)P7A}pp|90jjZ+*r(Lo)-14mWQJ;Ln<$V1QHNU4h5sK#|dLA15s6X-U zeg!4TS7piX70<6-NaKEPgNUOenf^NS_Twb!h0{B&X=rE`NtZ4pydl*au}Gs^_S$!L zch^FQ+l_6K{lW*TsjXta@4t~h%`!Bqd~&Li$t}Nw-@M`Y9^{Er%iBBXlqr&IZ~NsR z-W)HPxwI4c(z3+UH^F_m>)WeHd;gC-9TXA!_p!NrVd8O=t$jDU@?3dDB$Gv%gHO}< zZD3%gYl%Gvyg#f~sl!Oy%13we$f1|tzRZmomOtZsJsddiD=Fx)^wPBSeN}!=n_^Gu zj8;I|Jj>^!^l=;!Y-06A1%(@i`BW1p^^iS7F$-@d+(NY)?#&9eq`H0|t=Ql&s@M1d zxv77}|0BJK{_$ex-s~%e+TCLkDR+g2E3`_(!*&^_T&sD-Rb-NUcvo$wB8dNy0&=Rq zVR~?4GM00Iq-D4xV5jqdA%6E9VEoVoj86VogC59ifuaW>AcD*tB4Ew4tD@_=$_PM) zg`xF5G%#rsd1k>sNxeX094RAx#TmuZf;q1?yksL$T{Xj+GT;`hUwMgF^&%8}&O08!QC) zo1yH0!31hsPbEXtE9{_viwVl4g%haP?+c`k0UH6-3U>vC2kXW!&roeveCWVO=(x%*p_w2!?nfklu zzS}>av@UmcP2`<8CURLQ)=R5*pV2#YKFOG-`(!n5z3EoOJ07r{7MFGH407C}TV=It zc}lfeBsPPVQuC2v#F?CmQ+M+s3=h?w_>@uS*?w+r;#T2=L1NCwWJa`1gt?DE&cbt% z583_42g9~EZ74@>n0JIt7$|3cw6%FMUon#I>HkdNaCarIcc#w-t)P5^^b0i>($gkS zKfV>{`6geQv$?C9iuqZA8jXtC==uEdAkr6Zq5g^X(dM3ogS0)3D~S2;#U~h^S1zAO zIXqv;)7W}ldc%O)*9CeVWJ8bM3DS_dK9Uucmw^`p9vAwy)OPw6!5*Z;q~?}R zN6p0JzIkAEWcNldk@GDMO?uAV}i(nmUv1s3s*Q`49!bjd8V{LoFw z`*JtE5}nge#V9Lh&ByQBf3M!>IYWX;RNG$vq2vAW^0p^@Qfi-)@yO&K-nJ;u_B!`z zJZ&twvPggrv;67!$UV38rjw;eT}H|Z`kxCdHaZP*T(WqT8qFel@Q3~7mob$$FBrpL z^p?T33qAHSZ!3PHMssHbqu0z;)?|0zWu6V3W1B4=q#&M(i!T{=_sNF0Ye^j% z{HP;A*D<(~WIOtJTQ`fUh<%nx)QYC&g3*=&+jI@Ss~b+mYQ!{oWPQoRktJ}af=Me?Wi(MrRQB~X`b8S$!&2=LelNuX&5rBMk_E9<@x{Hq zU3vYu((9ucRb?w*w%2c~K^fD@*zi25`sR*EN6{{kSC!bkyrQ_dq?4i;Rf4kDd!lQ@ zvdDPE-|IuT*M5bvhsT5HdehZ{8XFs}5-(*|CmWV}@waVd6R)^!T`1kCvG+|d^8Jd7 zW$FBLrEYfH%q<-=nno^ko?lLcw7MhV+rsk^JBSkJH??ew~Fk&I-srhODr= zNAI0!J5LeVE+t7dcIDbeYA^1hm!2d)oDqRkW>V8m@GC*LhwDYkGJ> z>)G-9$CD*cmqP>s7ZL^o%kwrg-P!$Uo}Zc2)zp=FLHKZ)RpyFSs8c!2t;~&$VZr`# zNT2iceq>yo_)EL)(937(gQKOGT-J3v9_j!E;t$c z1&*bBo`2gltwmbn-pY;cb8^-ipc6YIQ&f9msE~58N>w2iv3ok#WAs_ukGcUv_r|uM zn>Tij-I{A%7CS#%+v65m+#4E5b@Aoh{(OthJ7U4Er+0Y;>tE*!vk-Yl8lZ=yzMgMI z#gTa~>_y3*iN}``BM#HS zPk9Q(c)zj}@oG-om6E6${KWo!TC<@4wzt)}&*{hzH?LfSiw_UpDwg3sldqv5lv{8j zM>9LkV4efk;4>nyd8D#Sb>`&UsLx_x!LxF%4Mb8(L22QJ3Bv@ zZM~-KqO35eF`Y=Ec}nayd4C1-%EX7uo_WX87rwp9%RWyd+ojO(?4Xk252}Sx-W$4! zse0C78gqhCM<*=#=w|jQB=KDlGTkvMoY6bgCM?|HMk_tH^l~S6!j>028yxSxW+;!& znfEarGaWMgvJy}d13xQUJ+`2st$f3{?p}J@WS274R&lMiw()wOmfJbzsa`%!eXs{N=mrD&X$8WRe6fs)rS%xK5n>={;D7YK$thi*u zbm@(^D)+D@U-K0n&(lwIyy2~>mmKSdl^V}wXtj-fn)ILuY9xIh6t7KnB8%g}{E%BC zvh9HE5!KvI4v%2}fz32MSDaY3k8B-fC=GKh9nqe7+?M@bo!@)8vzO&tz~~ugcHPPY z4KwV~ibobkTKqc$qICtNk7aWO)mPs;V)I&6PPp$hwaXXytF0EFZ>EK9zHd;o{Qibo zOQa~>3)b_EIt$^dqaW(N$>eqCk$l!ZqO4!J(c{3fEh)_Fm@y3ri37O*kvMm33?t!# z2CZ;2Nk`2%R_o4dPdyI6fE4@7@rj8*dgecY_Uo+bY$zlquCC~%WNKmPDJ+fjMmn$N z?nXG_JBG;(M?4Z>m)G;7CmcPeICH-|fVyU6hg z3yDgI0{QGkC2>#S3Sgy2!V)+qEL*@N2CjHH*`JoCg{?l}(nLZl;BSQ>41uwLTYymi zzq)G)sjJc$#wrjD?tcVqbxHnDY<{V=BkYFXWB=CX7lw8cu=}M%A!F#jvip_o;e@+i z3g5H;&h8f$gU%_;egHiApisR3J=;3w^|mf$aK>7#9Hioe_??4besdqoQg3u3NW zPhESgb=1}gjz9of86*mua0f`U=m-b+AB+l62)9-Nb(04g%1P)D0GQIT%NcT=z?X3V zs}aJ~1DL{vF@?zgLb#By6y)IxF+hM`3vwO#+oTHw+)O~_q#EGyfgLLkP=m}0Xr2Iz zJS%QYpi#gM0&(0A?)!f|{zq5}f6CxEEPSSdf8u|j_#YCIsD?sIL5F?vE!@$QuWRJa zv!_M|>m@8LWza`;4BI8#y@zfBBVH6>6p}mYw5A1aDWk z9)2(qmd{-wkRVp+yPvN770n*{iJV#vJaw&}bjPtLF}&{< zyArZCw6oauUDXcf-y3dot$(ORw)*H{{*FTWQiW|deOky^Mvb3Oi?>UO9Z`C2Z^@A< zwMTeI^+B)Hg`|t(*`_Dp2`oZaXIN)FdHyq#9pA=*$DmPKraDT`10*UgGD%+Dy%bIByY3sUu_MJT8Nn;x)TQfUypo8Q@ zu*WgIy^D(TWevw()0ZEeR5j{rj~N=QWTzigRDy3p1XR8hEz#T%c~_z?Z)$c ziAnMS)|t-9+7XB&sidzp2D=Kv52eXl-}I^3Y$E55B-=&#{cEK~kZLKv)QZC~0hd0n z!`axVpF?&&WG+nK(cDG;t2bcG;D;r`&cd#vP#mfIEek%wfN*NB< zF*4$pW`*n-(51sFz4{sQu_8*xs{wfH0d7h#4`)F10wg*>p8C7or2iV7fkR`ITTU04fC%h4Xn{ur(fCsrV0;UESX`y&iOod>KEwom!?Gp?X zF7%kd$__5HRU`XvOyvLu3K+I(40IWW2JG3Y9^QbJ83<@V0>cl6Vgtro6*ZOLhhc+5 z4*4#qx_f#D0A6CiQi0N${?_>uJ7my&;Sf#;wMA>z1<}~41N#a~0_O+vfW-vuVTb1z z(G0kuQi0j~IAf!Vf7cGtlF)!Vy8sD>UGTx_zgtcO@AoHENxxlI;243f8n{)A@x1`% z2NZ<>3=#y)81Ua!L_5F$CjlU0op8bl77E5=L|FxzD^TmUUI+uxNJDdrMEJOSVBhCw zKprA&9bgYQ!BOr&B#jOb1_5B{fQwND>Ahz8Vp@Up|A5N+f8GS3A{1bYw+R@ztZLf- z#MJ%T1i*(;{sG53bf^D(OZ^lr{F4cQUtNcwW3HQ8PVnUGa@?Ya+Z2#xD zfnV`X0J<1n1Bln2U|$vg{qLp!cUY_cGymVF|J!;A=C~&C8+-g<*?oX5#b2o=E&yNV z>;mQJ#%2ITgI1G&GJ)f?gaDJ~h>6Sr4t6k~4|uiPLi#aWcnBJ}N&d+MjsU^Vu$L*s zRAJe8fN3NM9);MwKL^DOCMgmY78J+e9)N?qD!F3gH2=j+;6xaP7C2a3iM8mLc<5u* zwVAB&(7wiN>3FDK^R;w5 + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 From b7e4ff16e8e4d5470c52cc6b3884b1bfe5a7dd32 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:52:25 +0200 Subject: [PATCH 02/59] fix: remove developers' pictures from pom.xml --- pom.xml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 97debe9..1b9a087 100644 --- a/pom.xml +++ b/pom.xml @@ -47,9 +47,6 @@ SPDX-License-Identifier: Apache-2.0 Testeur Europe/Paris - - ${team_pics_dir}/JB158FFN.png - Mathieu SABARTHES @@ -61,9 +58,6 @@ SPDX-License-Identifier: Apache-2.0 Testeur Europe/Paris - - ${team_pics_dir}/MS4D7E9N.png - @@ -341,4 +335,4 @@ SPDX-License-Identifier: Apache-2.0 - \ No newline at end of file + From 5733a19a87da92c1d9b671a0e8283e519bddf071 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:26:15 +0200 Subject: [PATCH 03/59] fix: improve README with explicit instructions --- README.fr.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.fr.md b/README.fr.md index f2e8934..f0b97f3 100644 --- a/README.fr.md +++ b/README.fr.md @@ -59,12 +59,12 @@ Décompressez le fichier .zip dans le dossier de votre choix. Extraire le zip ```bash -unzip target/TIC2WebSocket-*-bin -d /chemin/vers/votre/dossier +unzip target/TIC2WebSocket-VERSION-bin -d /chemin/vers/votre/dossier ``` 💻 **Windows** -Extrayez `target/TIC2WebSocket-*-bin` dans le dossier de votre choix. +Extrayez `target/TIC2WebSocket-VERSION-bin` dans le dossier de votre choix. ### Démarrage de l'Application From d060aa7101e6817cce471cedfbb16119c52b56d1 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:27:50 +0200 Subject: [PATCH 04/59] fix: improve README with explicit instructions --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fddcc9b..9e4ece3 100644 --- a/README.md +++ b/README.md @@ -59,12 +59,12 @@ Unzip the .zip file into the folder of your choice. Extract zip ```bash -unzip target/TIC2WebSocket-*-bin -d /path/to/your/folder +unzip target/TIC2WebSocket-VERSION-bin -d /path/to/your/folder ``` 💻 **Windows** -Extract `target/TIC2WebSocket-*-bin` in the folder of your choice. +Extract `target/TIC2WebSocket-VERSION-bin` in the folder of your choice. ### Starting the Application From 02da255100c2dc967025ea0314cfb88058fcaf4e Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:29:55 +0200 Subject: [PATCH 05/59] fix: remove getting started link --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 9e4ece3..297cad2 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,6 @@ Add `--help` option to get basic information on how to use the launcher. ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) -See the [Getting started](GETTING_STARTED.md), which document how to install and setup the required environment for developing - You don't need to be a developer to contribute, nor do much, you can simply: * Enhance documentation, * Correct a spelling, From f955ac26d8372dedb5d61b1626a82e8566258bf1 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:31:05 +0200 Subject: [PATCH 06/59] fix: remove getting started link --- README.fr.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.fr.md b/README.fr.md index f0b97f3..d261e98 100644 --- a/README.fr.md +++ b/README.fr.md @@ -97,8 +97,6 @@ Ajoutez l'option `--help` pour obtenir des informations de base sur l'utilisatio ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) -Consultez le guide [Getting started](GETTING_STARTED.md), qui documente comment installer et configurer l'environnement requis pour le développement - Vous n'avez pas besoin d'être développeur pour contribuer, ni de faire beaucoup, vous pouvez simplement : * Améliorer la documentation, * Corriger une faute d'orthographe, From e79892e4d81f46f516a65e89b398aef07037c3dc Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Mon, 15 Sep 2025 12:21:17 +0200 Subject: [PATCH 07/59] docs: add CHANGELOG and CONTRIBUTING --- CHANGELOG.fr.md | 20 ++++ CHANGELOG.md | 20 ++++ CONTRIBUTING.fr.md | 246 +++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 246 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 532 insertions(+) create mode 100644 CHANGELOG.fr.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.fr.md create mode 100644 CONTRIBUTING.md diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md new file mode 100644 index 0000000..90c0bfe --- /dev/null +++ b/CHANGELOG.fr.md @@ -0,0 +1,20 @@ + +# Journal des modifications + +[🇫🇷 Français](CHANGELOG.fr.md) | [🇺🇸 English](CHANGELOG.md) + +## [v1.0.0](https://github.com/Enedis-OSS/TIC2WebSocket/tree/v1.0.0) +### ✨ Nouvelles fonctionnalités: +- Connexion et lecture des trames TIC (Télé-Information Client) via port série +- Serveur WebSocket pour diffuser les données TIC en temps réel +- Support des différents modes TIC (historique et standard) +- Configuration flexible via fichier de propriétés +- Logging détaillé pour le diagnostic et le débogage +- Gestion robuste des erreurs et reconnexion automatique +- API WebSocket simple pour l'intégration avec d'autres applications \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ca39601 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ + +# Changelog + +[🇫🇷 Français](CHANGELOG.fr.md) | [🇺🇸 English](CHANGELOG.md) + +## [v1.0.0](https://github.com/Enedis-OSS/TIC2WebSocket/tree/v1.0.0) +### ✨ New features: +- Serial port connection and reading of TIC (Télé-Information Client) frames +- WebSocket server to broadcast TIC data in real-time +- Support for different TIC modes (historical and standard) +- Flexible configuration via properties file +- Detailed logging for diagnosis and debugging +- Robust error handling and automatic reconnection +- Simple WebSocket API for integration with other applications \ No newline at end of file diff --git a/CONTRIBUTING.fr.md b/CONTRIBUTING.fr.md new file mode 100644 index 0000000..f523d2a --- /dev/null +++ b/CONTRIBUTING.fr.md @@ -0,0 +1,246 @@ + +# Contribution + +[🇫🇷 Français](CONTRIBUTING.fr.md) | [🇺🇸 English](CONTRIBUTING.md) + +## Sommaire + +* [Comment contribuer à la documentation](#doc) +* [Comment faire une Pull Request](#pr) +* [Conventions de code](#code) +* [Conventions de test](#test) +* [Conventions de branche](#branch) +* [Validation des modifications](#commit) +* [Gestion des dépendances](#dep) +* [Processus de build](#build) +* [Gestion des releases](#release) +* [Publication](#releasing) +* [Licences](#oss) + + +## Comment contribuer à la documentation + +Pour contribuer à cette documentation (README, CONTRIBUTING, etc.), nous nous conformons à la [spécification CommonMark](http://spec.commonmark.org/0.27/) + +* [https://www.makeareadme.com/#suggestions-for-a-good-readme](https://www.makeareadme.com/#suggestions-for-a-good-readme) +* [https://help.github.com/en/articles/setting-guidelines-for-repository-contributors](https://help.github.com/en/articles/setting-guidelines-for-repository-contributors) + + +## Comment faire une Pull Request + +1. Copier le dépôt et maintenez une synchronisation active avec notre dépôt +2. Créez vos branches de travail en respectant les [branches conventionnelles](https://conventional-branch.github.io/). + * **ATTENTION** - Ne modifiez pas la branche main ni aucune de nos branches car cela casserait la synchronisation automatique +3. Quand vous avez terminé, récupérez tout et rebasez votre branche sur notre main ou toute autre de nos branches + * ex. sur votre branche, faites : + * `git fetch --all --prune` + * `git rebase --no-ff origin/main` +4. Testez vos modifications et assurez-vous que tout fonctionne +5. Soumettez votre Pull Request vers la branche dev (la branche main n'est autorisée que pour les propriétaires du projet) + * N'oubliez pas d'ajouter des relecteurs ! Vérifiez les derniers auteurs du code que vous avez modifié et ajoutez-les. + * En cas de doute, voici les contributeurs actifs : + * Jehan BOUSCH + + +## Conventions de code + +### Bonnes pratiques + +En règle générale, vous devez suivre le [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html). + +### Formatage + +Tous les fichiers source Go doivent se conformer à la [Section Formatting du Style Guide](https://google.github.io/styleguide/javaguide.html#s4-formatting) + +### Javadoc + +Tous les fichiers source Go doivent utiliser le [Javadoc section of Style Guide](https://google.github.io/styleguide/javaguide.html#s7-javadoc). + +## Conventions de test + +Pour exécuter tous les tests du projet, utilisez la commande suivante : + + ``` + mvn test + ``` + +### Nommage + +* Les tests sont situés dans `src/test` +* Quand vous écrivez un test, le nom de fichier doit se terminer par **Test.java**. +* Toutes les fonctions de test doivent commencer par le préfixe `test_` + +### Framework de test + +* Utilisez le framwork de test `org.junit`. + +### Simulations + +* Les interfaces Mock doivent avoir le même nom que l'original avec Mock à la fin. + +Exemple: `TICCoreStream` --> `TICCoreStreamMock` + +## Conventions de branche + +Nous nous conformons aux [branches conventionnelles](https://conventional-branch.github.io/). + +## Validation des modifications + +Nous nous conformons aux [commits conventionnels](https://www.conventionalcommits.org/fr/v1.0.0/). + +## Gestion des dépendances + +Les dépendances du projet sont listées dans le fichier go.mod. Le fichier go.sum, quant à lui, contient les sommes de contrôle cryptographiques du contenu de versions spécifiques de modules, incluant à la fois les dépendances directes et indirectes. + +Pour voir les modules réellement "utilisés" par l'application, utilisez la commande suivante : + ``` + mvn dependency:list + ``` + +Pour une documentation plus détaillée, veuillez vous référer à la [Apache Maven Dependency Plugin](https://maven.apache.org/plugins/maven-dependency-plugin/list-mojo.html). + +## Processus de build + +Pour compiler l'application TIC2WebSocket dans le répertoire du projet, utilisez la commande suivante : + + ``` + mvn clean package + ``` + +## Gestion des releases + +La gestion des releases se fait exclusivement sur GitHub + +## Publication + +Les releases de TIC2WebSocket ne sont disponibles que sur GitHub. + +### Mise à jour du fichier Changelog + +Mettez toujours à jour le changelog avant de créer une release. Cela garantit que les changements sont documentés pour la release en cours de création. +Référez-vous à cette page pour des conseils sur les notes de release générées automatiquement : [Notes de release générées automatiquement](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) + +N'hésitez pas à mettre à jour les notes de release générées, en particulier les titres des pull requests :) +Utilisez-les pour mettre à jour [CHANGELOG.md](https://github.com/Enedis-OSS/TIC2WebSocket/blob/main/CHANGELOG.md) + + +### Publication de version uniquement sur GitHub + +#### Informations générales + +- Les releases sur GitHub ne mettent à jour que le dernier chiffre de la version (ex., `2.7.1.1` ou `2.9.4.2`). +- La version snapshot suivante reste inchangée. +- Le fichier `CHANGELOG.md` est committé dans le tag. +- Les pull requests ne sont pas requises pour les releases sur GitHub. + +#### Étape par étape + +```shell +git switch -c release/ +git add CHANGELOG.md (voir la section Mise à jour du fichier Changelog) +git add . +git diff --staged +git commit -m "chore: Release " +``` + +- Ensuite tagger et pousser. +```shell +git tag +git push origin +``` + +- Mettre à jour les notes de release sur [github](https://github.com/Enedis-OSS/TIC2WebSocket/releases) + + +## Licences + +Nous avons choisi d'appliquer la licence Apache 2.0 (ALv2) : [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Comme pour tout projet, des problèmes de compatibilité de licences peuvent survenir et doivent être pris en compte. + +Des instructions concrètes et des outils pour maintenir TIC2WebSocket conforme à ALv2 et limiter les problèmes de licence sont détaillés ci-dessous. + +Cependant, nous reconnaissons la complexité du sujet, des erreurs peuvent être commises et nous pourrions ne pas avoir 100% raison. + +Néanmoins, nous nous efforçons d'être conformes et équitables, c'est-à-dire de faire de notre mieux en toute bonne foi. + +À ce titre, nous accueillons favorablement tout conseil et demande de modification. + + +À tout contributeur, nous recommandons vivement une lecture approfondie et une recherche personnelle : +* [http://www.apache.org/licenses/](http://www.apache.org/licenses/) +* [http://www.apache.org/legal/](http://www.apache.org/legal/) +* [http://apache.org/legal/resolved.html](http://apache.org/legal/resolved.html) +* [http://www.apache.org/dev/apply-license.html](http://www.apache.org/dev/apply-license.html) +* [http://www.apache.org/legal/src-headers.html](http://www.apache.org/legal/src-headers.html) +* [http://www.apache.org/legal/release-policy.html](http://www.apache.org/legal/release-policy.html) +* [http://www.apache.org/dev/licensing-howto.html](http://www.apache.org/dev/licensing-howto.html) + +* [Pourquoi la LGPL n'est pas autorisée](https://issues.apache.org/jira/browse/LEGAL-192) +* https://issues.apache.org/jira/projects/LEGAL/issues/ + +* Actualités générales : [https://opensource.com/tags/law](https://opensource.com/tags/law) + +### Comment gérer la compatibilité des licences + +Lors de l'ajout d'une nouvelle dépendance, **on doit vérifier sa licence ainsi que toutes les licences de ses dépendances transitives**. + +La compatibilité de la licence ALv2 telle que définie par l'ASF peut être trouvée ici : [http://apache.org/legal/resolved.html](http://apache.org/legal/resolved.html) + +3 catégories sont définies : +* [Catégorie A](https://www.apache.org/legal/resolved.html#category-a) : Contient toutes les licences compatibles. +* [Catégorie B](https://www.apache.org/legal/resolved.html#category-b) : Contient les licences compatibles sous certaines conditions. +* [Catégorie X](https://www.apache.org/legal/resolved.html#category-x) : Contient toutes les licences incompatibles qui doivent être évitées à tout prix. + +__D'après notre compréhension :__ + +Si, par quelque moyen que ce soit, votre contribution devait s'appuyer sur une dépendance de Catégorie X, alors vous devez fournir un moyen de la structurer +et de rendre son utilisation optionnelle pour TIC2WebSocket, sous forme de plugin. + +Vous pouvez distribuer votre plugin sous les termes de la licence de Catégorie X. + +Toute distribution de TIC2WebSocket accompagnée de votre plugin sera probablement faite sous les termes de la licence de Catégorie X. + +Mais _"vous pouvez fournir à l'utilisateur des instructions sur comment obtenir et installer le plugin non-inclus"_. + +__Références :__ +- [Optionnel](https://www.apache.org/legal/resolved.html#optional) +- [Prohibé](https://www.apache.org/legal/resolved.html#prohibited) + +### Comment se conformer aux clauses de redistribution et d'attribution + +De nombreuses licences imposent des conditions sur la redistribution et l'attribution, y compris ALv2. + +__Références :__ +* http://mail-archives.apache.org/mod_mbox/www-legal-discuss/201502.mbox/%3CCAAS6%3D7gzsAYZMT5mar_nfy9egXB1t3HendDQRMUpkA6dqvhr7w%40mail.gmail.com%3E +* http://mail-archives.apache.org/mod_mbox/www-legal-discuss/201501.mbox/%3CCAAS6%3D7jJoJMkzMRpSdJ6kAVSZCvSfC5aRD0eMyGzP_rzWyE73Q%40mail.gmail.com%3E + +#### Fichier LICENSE +##### Dans la distribution source + +Ce fichier contient : +* la licence ALv2 complète. +* la liste des dépendances et pointe vers leur fichier de licence respectif + * Exemple : + _Ce produit intègre SuperWidget 1.2.3, qui est disponible sous licence + "3-clause BSD". Pour plus de détails, voir deps/superwidget/_ +* ne pas lister les dépendances sous ALv2 + +#### Fichier NOTICE + +##### Dans la distribution source + +_Le fichier NOTICE n'est pas destiné à transmettre des informations aux consommateurs en aval -- il +est un moyen d'*obliger* les consommateurs en aval à *relayer* certains avis requis._ + +### Questions non résolues - AIDE RECHERCHÉE - +* Les dépendances de test doivent-elles être prises en compte pour la distribution source ? + * Il semblerait que OUI +* Les dépendances de temps de build doivent-elles être prises en compte ? + * Il semblerait que NON mais cela pourrait dépendre de ce que fait réellement cette dépendance \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7bc88a9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,246 @@ + +# Contributing + +[🇫🇷 Français](CONTRIBUTING.fr.md) | [🇺🇸 English](CONTRIBUTING.md) + +## Summary + +* [How to contribute to the documentation](#doc) +* [How to make a Pull Request](#pr) +* [Code convention](#code) +* [Test convention](#test) +* [Branch convention](#branch) +* [Commit changes](#commit) +* [Dependency management](#dep) +* [Build Process](#build) +* [Release Management](#release) +* [Releasing](#releasing) +* [Licensing](#oss) + + +## How to contribute to the documentation + +To contribute to this documentation (README, CONTRIBUTING, etc.), we conforms to the [CommonMark Spec](http://spec.commonmark.org/0.27/) + +* [https://www.makeareadme.com/#suggestions-for-a-good-readme](https://www.makeareadme.com/#suggestions-for-a-good-readme) +* [https://help.github.com/en/articles/setting-guidelines-for-repository-contributors](https://help.github.com/en/articles/setting-guidelines-for-repository-contributors) + + +## How to make a Pull Request + +1. Fork the repository and keep active sync on our repo +2. Create your working branches conforming to [conventional branch](https://conventional-branch.github.io/). + * **WARNING** - Do not modify the main branch nor any of our branches since it will break the automatic sync +3. When you are done, fetch all and rebase your branch onto our main or any other of ours + * ex. on your branch, do : + * `git fetch --all --prune` + * `git rebase --no-ff origin/main` +4. Test your changes and make sure everything is working +5. Submit your Pull Request to the dev branch (main branch is only allowed for project owners) + * Do not forget to add reviewers! Check out the last authors of the code you modified and add them. + * In case of doubts, here are active contributors : + * Jehan BOUSCH + + +## Code convention + +### Best Practices + +As a general rule you have to follow the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html). + +### Formatting + +All Java source files must conform to the [Formatting section of Style Guide](https://google.github.io/styleguide/javaguide.html#s4-formatting) + +### Javadoc + +All Java source files must use the [Javadoc section of Style Guide](https://google.github.io/styleguide/javaguide.html#s7-javadoc). + +## Test convention + +To run all the tests of the project use the following command : + + ``` + mvn test + ``` + +### Naming + +* Tests are located in `src/test` +* When you write a test file name should end with **Test.java**. +* All test functions must start with a `test_` prefix. + +### Test framework + +* Use `org.junit` as test framework. + +### Mocking + +* Mock interfaces should have the same name as the original ending with Mock + +Example: `TICCoreStream` --> `TICCoreStreamMock` + +## Branch convention + +We conform to [conventional branch](https://conventional-branch.github.io/). + +## Commit changes + +We conform to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/). + +## Dependency management + +Project dependencies are listed in the pom.xml file. + +To see modules really "used" by the application use the following command : + ``` + mvn dependency:list + ``` + +For more detailed documentation please refer to [Apache Maven Dependency Plugin](https://maven.apache.org/plugins/maven-dependency-plugin/list-mojo.html). + +## Build Process + +To build the application TIC2WebSocket in the project directory use the following command : + + ``` + mvn clean package + ``` + +## Release Management + +Release management is exclusively done on GitHub + +## Releasing + +TIC2WebSocket releases are available only on GitHub. + +### Update Changelog file + +Always update the changelog before creating a release. This ensures the changes are documented for the release being created. +Refer to this page for guidance on automatically generated release notes: [Automatically generated release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) + +Do not hesitate to update the release note generated especially the titles of pull request :) +Use it to update [CHANGELOG.md](https://github.com/Enedis-OSS/TIC2WebSocket/blob/main/CHANGELOG.md) + + +### Releasing version only on GitHub + +#### General information + +- Releases to GitHub only update the last digit of the version (e.g., `2.7.1.1` or `2.9.4.2`). +- The subsequent snapshot version remains unchanged. +- The `CHANGELOG.md` file is committed to the tag. +- Pull requests are not required for releases to GitHub. + +#### Step by step + +```shell +git switch -c release/ +git add CHANGELOG.md (see Update Changelog file section) +git add . +git diff --staged +git commit -m "chore: Release " +``` + +- Then tag and push. +```shell +git tag +git push origin +``` + +- Update the release note on [github](https://github.com/Enedis-OSS/TIC2WebSocket/releases) + + +## Licensing + +We choose to apply the Apache License 2.0 (ALv2) : [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +As for any project, license compatibility issues may arise and should be taken care of. + +Concrete instructions and tooling to keep TIC2WebSocket ALv2 compliant and limit licensing issues are to be found below. + +However, we acknowledge topic's complexity, mistakes might be done and we might not get it 100% right. + +Still, we strive to be compliant and be fair, meaning, we do our best in good faith. + +As such, we welcome any advice and change request. + + +To any contributor, we strongly recommend further reading and personal research : +* [http://www.apache.org/licenses/](http://www.apache.org/licenses/) +* [http://www.apache.org/legal/](http://www.apache.org/legal/) +* [http://apache.org/legal/resolved.html](http://apache.org/legal/resolved.html) +* [http://www.apache.org/dev/apply-license.html](http://www.apache.org/dev/apply-license.html) +* [http://www.apache.org/legal/src-headers.html](http://apache.org/legal/src-headers.html) +* [http://www.apache.org/legal/release-policy.html](http://www.apache.org/legal/release-policy.html) +* [http://www.apache.org/dev/licensing-howto.html](http://www.apache.org/dev/licensing-howto.html) + +* [Why is LGPL not allowed](https://issues.apache.org/jira/browse/LEGAL-192) +* https://issues.apache.org/jira/projects/LEGAL/issues/ + +* General news : [https://opensource.com/tags/law](https://opensource.com/tags/law) + +### How to manage license compatibility + +When adding a new dependency, **one should check its license and all its transitive dependencies** licenses. + +ALv2 license compatibility as defined by the ASF can be found here : [http://apache.org/legal/resolved.html](http://apache.org/legal/resolved.html) + +3 categories are defined : +* [Category A](https://www.apache.org/legal/resolved.html#category-a) : Contains all compatibles licenses. +* [Category B](https://www.apache.org/legal/resolved.html#category-b) : Contains compatibles licenses under certain conditions. +* [Category X](https://www.apache.org/legal/resolved.html#category-x) : Contains all incompatibles licenses which must be avoid at all cost. + +__As far as we understand :__ + +If, by any mean, your contribution should rely on a Category X dependency, then you must provide a way to modularize it +and make it's use optional to TIC2WebSocket, as a plugin. + +You may distribute your plugin under the terms of the Category X license. + +Any distribution of TIC2WebSocket bundled with your plugin will probably be done under the terms of the Category X license. + +But _"you can provide the user with instructions on how to obtain and install the non-included"_ plugin. + +__References :__ +- [Optional](https://www.apache.org/legal/resolved.html#optional) +- [Prohibited](https://www.apache.org/legal/resolved.html#prohibited) + +### How to comply with Redistribution and Attribution clauses + +Lots of licenses place conditions on redistribution and attribution, including ALv2. + +__References :__ +* http://mail-archives.apache.org/mod_mbox/www-legal-discuss/201502.mbox/%3CCAAS6%3D7gzsAYZMT5mar_nfy9egXB1t3HendDQRMUpkA6dqvhr7w%40mail.gmail.com%3E +* http://mail-archives.apache.org/mod_mbox/www-legal-discuss/201501.mbox/%3CCAAS6%3D7jJoJMkzMRpSdJ6kAVSZCvSfC5aRD0eMyGzP_rzWyE73Q%40mail.gmail.com%3E + +#### LICENSE file +##### In Source distribution + +This file contains : +* the complete ALv2 license. +* list dependencies and points to their respective license file + * Example : + _This product bundles SuperWidget 1.2.3, which is available under a + "3-clause BSD" license. For deails, see deps/superwidget/_ +* do not list dependencies under the ALv2 + +#### NOTICE file + +##### In source distribution + +_The NOTICE file is not for conveying information to downstream consumers -- it +is a way to *compel* downstream consumers to *relay* certain required notices._ + +### Unresolved questions - HELP WANTED - +* Should test dependencies be taken into account for source distribution ? + * It appears to be YES +* Should build time dependencies be taken into account ? + * It appears to be NO but might depend on the actual stuff done by this dependency \ No newline at end of file From ce71128128a3c7cb2539838e1550aa5fefeb7340 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Wed, 24 Sep 2025 17:11:38 +0200 Subject: [PATCH 08/59] chore: add target directory to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 29a63cf..64ae858 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ # SPDX-License-Identifier: Apache-2.0 src/main/resources/TIC2WebSocket.properties +target/ \ No newline at end of file From 98d4c30c6e4c6cfa7c91c6dc7084b858535facd9 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Wed, 24 Sep 2025 17:11:38 +0200 Subject: [PATCH 09/59] chore: add target directory to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 29a63cf..64ae858 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ # SPDX-License-Identifier: Apache-2.0 src/main/resources/TIC2WebSocket.properties +target/ \ No newline at end of file From e436b008639481d4e2cf1f2cbcfac8fedd449d05 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Wed, 24 Sep 2025 19:09:02 +0200 Subject: [PATCH 10/59] chore: format enedis.lab.codec package --- src/main/java/enedis/lab/codec/Codec.java | 5 +- .../java/enedis/lab/codec/CodecException.java | 143 ++++++------------ 2 files changed, 46 insertions(+), 102 deletions(-) diff --git a/src/main/java/enedis/lab/codec/Codec.java b/src/main/java/enedis/lab/codec/Codec.java index e7932f8..a529a14 100644 --- a/src/main/java/enedis/lab/codec/Codec.java +++ b/src/main/java/enedis/lab/codec/Codec.java @@ -14,8 +14,8 @@ * @param * */ -public interface Codec -{ +public interface Codec { + /** * Decode type K to type T * @@ -33,4 +33,5 @@ public interface Codec * @throws CodecException */ public K encode(T object) throws CodecException; + } diff --git a/src/main/java/enedis/lab/codec/CodecException.java b/src/main/java/enedis/lab/codec/CodecException.java index 678524d..ae2909a 100644 --- a/src/main/java/enedis/lab/codec/CodecException.java +++ b/src/main/java/enedis/lab/codec/CodecException.java @@ -8,162 +8,105 @@ package enedis.lab.codec; /** - * Codec exception + * Exception thrown during codec operations + * + * This exception is used to signal errors occurring + * during data encoding and decoding processes. */ -public class CodecException extends Exception -{ +public class CodecException extends Exception { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private static final long serialVersionUID = 6029680699870915485L; - private static final long serialVersionUID = 6029680699870915485L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Object data; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private Object data; /** - * Default constructor + * Creates a new CodecException with no detail message */ - public CodecException() - { + public CodecException() { super(); } /** - * Constructor + * Creates a new CodecException with the specified detail message and cause * - * @param message - * @param cause + * @param message the detail message (which is saved for later retrieval + * by the {@link Throwable#getMessage()} method) + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method) */ - public CodecException(String message, Throwable cause) - { + public CodecException(String message, Throwable cause) { super(message, cause); } /** - * Constructor + * Creates a new CodecException with the specified detail message * - * @param message + * @param message the detail message (which is saved for later retrieval + * by the {@link Throwable#getMessage()} method) */ - public CodecException(String message) - { + public CodecException(String message) { super(message); } /** - * Constructor + * Creates a new CodecException with the specified detail message and data * - * @param message - * @param data + * @param message the detail message (which is saved for later retrieval + * by the {@link Throwable#getMessage()} method) + * @param data additional data object associated with this exception */ - public CodecException(String message, Object data) - { + public CodecException(String message, Object data) { super(message); this.data = data; } /** - * Constructor + * Creates a new CodecException with the specified cause * - * @param cause + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method) */ - public CodecException(Throwable cause) - { + public CodecException(Throwable cause) { super(cause); } /** - * Raise CodecException invalid value + * Creates and throws a CodecException for invalid value scenarios * - * @param info - * @throws CodecException + * @param info additional information about the invalid value + * @throws CodecException always thrown with message "Invalid value : " + info */ - public static void raiseInvalidValue(String info) throws CodecException - { + public static void raiseInvalidValue(String info) throws CodecException { throw new CodecException("Invalid value : " + info); } /** - * Raise CodecException missing value + * Creates and throws a CodecException for missing value scenarios * - * @param value - * @throws CodecException + * @param value the name or identifier of the missing value + * @throws CodecException always thrown with message "Missing value : " + value */ - public static void raiseMissingValue(String value) throws CodecException - { + public static void raiseMissingValue(String value) throws CodecException { throw new CodecException("Missing value : " + value); } /** - * Raise CodecException inconsistency + * Creates and throws a CodecException for data inconsistency scenarios * - * @param info - * @throws CodecException + * @param info additional information about the inconsistency + * @throws CodecException always thrown with message "Inconsistency : " + info */ - public static void raiseInconsistency(String info) throws CodecException - { + public static void raiseInconsistency(String info) throws CodecException { throw new CodecException("Inconsistency : " + info); } /** - * Get data + * Returns the additional data object associated with this exception * - * @return data + * @return the data object, or null if no data was provided */ - public Object getData() - { + public Object getData() { return this.data; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - } From efa3825745bc9cc6ed9086ad7f1fe23549d109f6 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Thu, 25 Sep 2025 14:20:55 +0200 Subject: [PATCH 11/59] chore: add javadoc author to classes --- src/main/java/enedis/lab/codec/Codec.java | 2 +- .../java/enedis/lab/codec/CodecException.java | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main/java/enedis/lab/codec/Codec.java b/src/main/java/enedis/lab/codec/Codec.java index a529a14..3627dc0 100644 --- a/src/main/java/enedis/lab/codec/Codec.java +++ b/src/main/java/enedis/lab/codec/Codec.java @@ -12,7 +12,7 @@ * * @param * @param - * + * @author Enedis Smarties team */ public interface Codec { diff --git a/src/main/java/enedis/lab/codec/CodecException.java b/src/main/java/enedis/lab/codec/CodecException.java index ae2909a..a6a7094 100644 --- a/src/main/java/enedis/lab/codec/CodecException.java +++ b/src/main/java/enedis/lab/codec/CodecException.java @@ -8,10 +8,12 @@ package enedis.lab.codec; /** - * Exception thrown during codec operations + * Exception thrown during codec operations. * * This exception is used to signal errors occurring * during data encoding and decoding processes. + * + * @author Enedis Smarties team */ public class CodecException extends Exception { @@ -20,14 +22,14 @@ public class CodecException extends Exception { private Object data; /** - * Creates a new CodecException with no detail message + * Creates a new CodecException with no detail message. */ public CodecException() { super(); } /** - * Creates a new CodecException with the specified detail message and cause + * Creates a new CodecException with the specified detail message and cause. * * @param message the detail message (which is saved for later retrieval * by the {@link Throwable#getMessage()} method) @@ -39,7 +41,7 @@ public CodecException(String message, Throwable cause) { } /** - * Creates a new CodecException with the specified detail message + * Creates a new CodecException with the specified detail message. * * @param message the detail message (which is saved for later retrieval * by the {@link Throwable#getMessage()} method) @@ -49,7 +51,7 @@ public CodecException(String message) { } /** - * Creates a new CodecException with the specified detail message and data + * Creates a new CodecException with the specified detail message and data. * * @param message the detail message (which is saved for later retrieval * by the {@link Throwable#getMessage()} method) @@ -61,7 +63,7 @@ public CodecException(String message, Object data) { } /** - * Creates a new CodecException with the specified cause + * Creates a new CodecException with the specified cause. * * @param cause the cause (which is saved for later retrieval by the * {@link Throwable#getCause()} method) @@ -71,7 +73,7 @@ public CodecException(Throwable cause) { } /** - * Creates and throws a CodecException for invalid value scenarios + * Creates and throws a CodecException for invalid value scenarios. * * @param info additional information about the invalid value * @throws CodecException always thrown with message "Invalid value : " + info @@ -81,7 +83,7 @@ public static void raiseInvalidValue(String info) throws CodecException { } /** - * Creates and throws a CodecException for missing value scenarios + * Creates and throws a CodecException for missing value scenarios. * * @param value the name or identifier of the missing value * @throws CodecException always thrown with message "Missing value : " + value @@ -91,7 +93,7 @@ public static void raiseMissingValue(String value) throws CodecException { } /** - * Creates and throws a CodecException for data inconsistency scenarios + * Creates and throws a CodecException for data inconsistency scenarios. * * @param info additional information about the inconsistency * @throws CodecException always thrown with message "Inconsistency : " + info @@ -101,7 +103,7 @@ public static void raiseInconsistency(String info) throws CodecException { } /** - * Returns the additional data object associated with this exception + * Returns the additional data object associated with this exception. * * @return the data object, or null if no data was provided */ From b499c71f707490198e73e17364cb4fb48a9144f5 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Thu, 25 Sep 2025 14:28:08 +0200 Subject: [PATCH 12/59] chore: format enedis.lab.io.channels.serialport package --- .../ChannelSerialPortConfiguration.java | 499 ++++++++++-------- .../lab/io/channels/serialport/Parity.java | 56 +- 2 files changed, 334 insertions(+), 221 deletions(-) diff --git a/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java b/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java index de7e171..ced2c10 100644 --- a/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java +++ b/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java @@ -23,70 +23,85 @@ import enedis.lab.types.datadictionary.KeyDescriptorString; /** - * ChannelSerialPortConfiguration class + * Configuration class for serial port communication channels. + *

    + * This class extends {@link ChannelConfiguration} and provides specific configuration + * parameters for serial port communication, including port identification, baud rate, + * parity settings, data bits, stop bits, and timeout configurations. + *

    + * The configuration supports both port ID and port name identification methods, + * with validation for standard serial communication parameters. + *

    + * Supported baud rates: 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, + * 115200, 230400, 460800, 921600, 1843200, 3686400 + *

    + * Supported data bits: 5, 6, 7, 8 + *

    + * Supported stop bits: 1.0, 1.5, 2.0 + *

    + * Default sync read timeout: 10000 milliseconds * - * Generated + * @author Enedis Smarties team + * @see ChannelConfiguration + * @see ChannelProtocol#SERIAL_PORT + * @see ChannelDirection#RXTX */ -public class ChannelSerialPortConfiguration extends ChannelConfiguration -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_PORT_ID = "portId"; - protected static final String KEY_PORT_NAME = "portName"; - protected static final String KEY_BAUDRATE = "baudrate"; - protected static final String KEY_PARITY = "parity"; - protected static final String KEY_DATA_BITS = "dataBits"; - protected static final String KEY_STOP_BITS = "stopBits"; - protected static final String KEY_SYNC_READ_TIMEOUT = "syncReadTimeout"; - - private static final ChannelProtocol PROTOCOL_ACCEPTED_VALUE = ChannelProtocol.SERIAL_PORT; - private static final ChannelDirection DIRECTION_ACCEPTED_VALUE = ChannelDirection.RXTX; - private static final Number[] BAUDRATE_ACCEPTED_VALUES = { 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1843200, - 3686400 }; - private static final Number[] DATA_BITS_ACCEPTED_VALUES = { 5, 6, 7, 8 }; - private static final Number[] STOP_BITS_ACCEPTED_VALUES = { 1.0d, 1.5d, 2.0d }; - private static final Number SYNC_READ_TIMEOUT_DEFAULT_VALUE = 10000; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kPortId; - protected KeyDescriptorString kPortName; - protected KeyDescriptorNumber kBaudrate; - protected KeyDescriptorEnum kParity; - protected KeyDescriptorNumber kDataBits; - protected KeyDescriptorNumber kStopBits; - protected KeyDescriptorNumber kSyncReadTimeout; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ChannelSerialPortConfiguration() - { +public class ChannelSerialPortConfiguration extends ChannelConfiguration { + + /** Key for the serial port identifier configuration parameter. */ + protected static final String KEY_PORT_ID = "portId"; + /** Key for the serial port name configuration parameter. */ + protected static final String KEY_PORT_NAME = "portName"; + /** Key for the baud rate configuration parameter. */ + protected static final String KEY_BAUDRATE = "baudrate"; + /** Key for the parity configuration parameter. */ + protected static final String KEY_PARITY = "parity"; + /** Key for the data bits configuration parameter. */ + protected static final String KEY_DATA_BITS = "dataBits"; + /** Key for the stop bits configuration parameter. */ + protected static final String KEY_STOP_BITS = "stopBits"; + /** Key for the synchronous read timeout configuration parameter. */ + protected static final String KEY_SYNC_READ_TIMEOUT = "syncReadTimeout"; + + /** Accepted protocol value for serial port channels. */ + private static final ChannelProtocol PROTOCOL_ACCEPTED_VALUE = ChannelProtocol.SERIAL_PORT; + /** Accepted direction value for serial port channels (bidirectional). */ + private static final ChannelDirection DIRECTION_ACCEPTED_VALUE = ChannelDirection.RXTX; + /** Array of supported baud rate values for serial communication. */ + private static final Number[] BAUDRATE_ACCEPTED_VALUES = { 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, + 115200, 230400, 460800, 921600, 1843200, 3686400 }; + /** Array of supported data bits values (5, 6, 7, or 8 bits). */ + private static final Number[] DATA_BITS_ACCEPTED_VALUES = { 5, 6, 7, 8 }; + /** Array of supported stop bits values (1.0, 1.5, or 2.0). */ + private static final Number[] STOP_BITS_ACCEPTED_VALUES = { 1.0d, 1.5d, 2.0d }; + /** Default timeout value for synchronous read operations (10000 milliseconds). */ + private static final Number SYNC_READ_TIMEOUT_DEFAULT_VALUE = 10000; + + /** List of key descriptors for configuration validation. */ + private List> keys = new ArrayList>(); + + /** Key descriptor for port ID validation. */ + protected KeyDescriptorString kPortId; + /** Key descriptor for port name validation. */ + protected KeyDescriptorString kPortName; + /** Key descriptor for baud rate validation. */ + protected KeyDescriptorNumber kBaudrate; + /** Key descriptor for parity validation. */ + protected KeyDescriptorEnum kParity; + /** Key descriptor for data bits validation. */ + protected KeyDescriptorNumber kDataBits; + /** Key descriptor for stop bits validation. */ + protected KeyDescriptorNumber kStopBits; + /** Key descriptor for sync read timeout validation. */ + protected KeyDescriptorNumber kSyncReadTimeout; + + /** + * Default constructor for serial port configuration. + *

    + * Initializes the configuration with default values and sets up + * the key descriptors for parameter validation. + */ + protected ChannelSerialPortConfiguration() { super(); this.loadKeyDescriptors(); @@ -95,61 +110,69 @@ protected ChannelSerialPortConfiguration() } /** - * Constructor using map + * Constructs a new serial port configuration from a map of key-value pairs. + *

    + * This constructor initializes the configuration with values from the provided map, + * performing validation and setting default values for missing parameters. * - * @param map - * @throws DataDictionaryException + * @param map the map containing configuration parameters + * @throws DataDictionaryException if the map contains invalid values or required + * parameters are missing */ - public ChannelSerialPortConfiguration(Map map) throws DataDictionaryException - { + public ChannelSerialPortConfiguration(Map map) throws DataDictionaryException { this(); this.copy(fromMap(map)); } /** - * Constructor using datadictionary + * Constructs a new serial port configuration by copying from another data dictionary. + *

    + * This constructor creates a new configuration instance by copying all parameters + * from the provided data dictionary, ensuring consistency between configurations. * - * @param other - * @throws DataDictionaryException + * @param other the data dictionary to copy configuration from + * @throws DataDictionaryException if the source data dictionary contains invalid + * values or is incompatible with serial port configuration */ - public ChannelSerialPortConfiguration(DataDictionary other) throws DataDictionaryException - { + public ChannelSerialPortConfiguration(DataDictionary other) throws DataDictionaryException { this(); this.copy(other); } /** + * Constructs a new serial port configuration with a name and file reference, + * initializing all parameters to their default values. + *

    * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file + * @param name the configuration name + * @param file the configuration file associated with this configuration */ - public ChannelSerialPortConfiguration(String name, File file) - { + public ChannelSerialPortConfiguration(String name, File file) { this(); this.init(name, file); } /** - * Constructor setting parameters to specific values + * Constructs a new serial port configuration with all parameters set to specific values. + *

    + * This constructor provides a convenient way to create a fully configured serial port + * channel with all necessary parameters specified at construction time. * - * @param name - * @param alias - * @param portId - * @param portName - * @param baudrate - * @param parity - * @param dataBits - * @param stopBits - * @param syncReadTimeout - * @throws DataDictionaryException + * @param name the configuration name + * @param alias the configuration alias + * @param portId the serial port identifier (alternative to portName) + * @param portName the serial port name (alternative to portId) + * @param baudrate the communication baud rate + * @param parity the parity setting for data validation + * @param dataBits the number of data bits per frame + * @param stopBits the number of stop bits + * @param syncReadTimeout the timeout for synchronous read operations in milliseconds + * @throws DataDictionaryException if any of the provided values are invalid or + * incompatible with serial port communication requirements */ - public ChannelSerialPortConfiguration(String name, String alias, String portId, String portName, Number baudrate, Parity parity, Number dataBits, Number stopBits, - Number syncReadTimeout) throws DataDictionaryException - { + public ChannelSerialPortConfiguration(String name, String alias, String portId, String portName, Number baudrate, + Parity parity, Number dataBits, Number stopBits, + Number syncReadTimeout) throws DataDictionaryException { this(); this.setName(name); @@ -165,239 +188,291 @@ public ChannelSerialPortConfiguration(String name, String alias, String portId, this.checkAndUpdate(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ChannelConfiguration - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - + /** + * Updates optional parameters with default values if not already set. + *

    + * This method ensures that either port ID or port name is specified, + * and sets default values for protocol, direction, and sync read timeout + * if they are not already configured. + * + * @throws DataDictionaryException if neither port ID nor port name is defined, + * or if there are validation errors with the configuration + */ @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_PORT_ID) && !this.exists(KEY_PORT_NAME)) - { + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_PORT_ID) && !this.exists(KEY_PORT_NAME)) { throw new DataDictionaryException("Neither " + KEY_PORT_ID + " nor " + KEY_PORT_NAME + " is defined"); } - if (!this.exists(KEY_PROTOCOL)) - { + if (!this.exists(KEY_PROTOCOL)) { this.setProtocol(PROTOCOL_ACCEPTED_VALUE); } - if (!this.exists(KEY_DIRECTION)) - { + if (!this.exists(KEY_DIRECTION)) { this.setDirection(DIRECTION_ACCEPTED_VALUE); } - if (!this.exists(KEY_SYNC_READ_TIMEOUT)) - { + if (!this.exists(KEY_SYNC_READ_TIMEOUT)) { this.setSyncReadTimeout(SYNC_READ_TIMEOUT_DEFAULT_VALUE); } super.updateOptionalParameters(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** - * Get port id + * Retrieves the serial port identifier. + *

    + * The port ID is an alternative way to identify the serial port, + * typically used when the system provides a unique identifier for the port. * - * @return the port id + * @return the port identifier, or null if not set */ - public String getPortId() - { + public String getPortId() { return (String) this.data.get(KEY_PORT_ID); } /** - * Get port name + * Retrieves the serial port name. + *

    + * The port name is typically a system-specific identifier such as + * "/dev/ttyUSB0" on Unix systems or "COM1" on Windows systems. * - * @return the port name + * @return the port name, or null if not set */ - public String getPortName() - { + public String getPortName() { return (String) this.data.get(KEY_PORT_NAME); } /** - * Get baudrate + * Retrieves the communication baud rate. + *

    + * The baud rate determines the speed of data transmission over the serial port. * - * @return the baudrate + * @return the baud rate in bits per second, or null if not set */ - public Number getBaudrate() - { + public Number getBaudrate() { return (Number) this.data.get(KEY_BAUDRATE); } /** - * Get parity + * Retrieves the parity setting for data validation. + *

    + * Parity is used for error detection in serial communication. + * It can be NONE, ODD, or EVEN depending on the communication protocol. * - * @return the parity + * @return the parity setting, or null if not set */ - public Parity getParity() - { + public Parity getParity() { return (Parity) this.data.get(KEY_PARITY); } /** - * Get data bits + * Retrieves the number of data bits per frame. + *

    + * Data bits determine how many bits are used to represent each character + * in the serial communication. Common values are 7 or 8 bits. * - * @return the data bits + * @return the number of data bits, or null if not set */ - public Number getDataBits() - { + public Number getDataBits() { return (Number) this.data.get(KEY_DATA_BITS); } /** - * Get stop bits + * Retrieves the number of stop bits. + *

    + * Stop bits are used to signal the end of a data frame in serial communication. * - * @return the stop bits + * @return the number of stop bits, or null if not set */ - public Number getStopBits() - { + public Number getStopBits() { return (Number) this.data.get(KEY_STOP_BITS); } /** - * Get sync read timeout + * Retrieves the timeout for synchronous read operations. + *

    + * This timeout determines how long the system will wait for data to be + * available on the serial port before timing out the read operation. * - * @return the sync read timeout + * @return the timeout value in milliseconds, or null if not set */ - public Number getSyncReadTimeout() - { + public Number getSyncReadTimeout() { return (Number) this.data.get(KEY_SYNC_READ_TIMEOUT); } /** - * Set port id + * Sets the serial port identifier. + *

    + * The port ID provides an alternative way to identify the serial port, + * typically used when the system provides a unique identifier for the port. + * Either port ID or port name must be specified for a valid configuration. * - * @param portId - * @throws DataDictionaryException + * @param portId the port identifier to set + * @throws DataDictionaryException if the port ID is invalid or conflicts + * with other configuration parameters */ - public void setPortId(String portId) throws DataDictionaryException - { + public void setPortId(String portId) throws DataDictionaryException { this.setPortId((Object) portId); } /** - * Set port name + * Sets the serial port name. + *

    + * The port name is typically a system-specific identifier such as + * "/dev/ttyUSB0" on Unix systems or "COM1" on Windows systems. + * Either port ID or port name must be specified for a valid configuration. * - * @param portName - * @throws DataDictionaryException + * @param portName the port name to set + * @throws DataDictionaryException if the port name is invalid or conflicts + * with other configuration parameters */ - public void setPortName(String portName) throws DataDictionaryException - { + public void setPortName(String portName) throws DataDictionaryException { this.setPortName((Object) portName); } /** - * Set baudrate + * Sets the communication baud rate. + *

    + * The baud rate determines the speed of data transmission over the serial port. + * Must be one of the supported values: {@link #BAUDRATE_ACCEPTED_VALUES}. * - * @param baudrate - * @throws DataDictionaryException + * @param baudrate the baud rate in bits per second + * @throws DataDictionaryException if the baud rate is not supported or invalid */ - public void setBaudrate(Number baudrate) throws DataDictionaryException - { + public void setBaudrate(Number baudrate) throws DataDictionaryException { this.setBaudrate((Object) baudrate); } /** - * Set parity + * Sets the parity setting for data validation. + *

    + * Parity is used for error detection in serial communication. + * The parity setting must be one of the supported {@link Parity} enum values. * - * @param parity - * @throws DataDictionaryException + * @param parity the parity setting to use + * @throws DataDictionaryException if the parity value is invalid or not supported */ - public void setParity(Parity parity) throws DataDictionaryException - { + public void setParity(Parity parity) throws DataDictionaryException { this.setParity((Object) parity); } /** - * Set data bits + * Sets the number of data bits per frame. + *

    + * Data bits determine how many bits are used to represent each character + * in the serial communication. Must be one of: {@link #DATA_BITS_ACCEPTED_VALUES}. * - * @param dataBits - * @throws DataDictionaryException + * @param dataBits the number of data bits to use + * @throws DataDictionaryException if the data bits value is not supported or invalid */ - public void setDataBits(Number dataBits) throws DataDictionaryException - { + public void setDataBits(Number dataBits) throws DataDictionaryException { this.setDataBits((Object) dataBits); } /** - * Set stop bits + * Sets the number of stop bits. + *

    + * Stop bits are used to signal the end of a data frame in serial communication. + * Must be one of: {@link #STOP_BITS_ACCEPTED_VALUES}. * - * @param stopBits - * @throws DataDictionaryException + * @param stopBits the number of stop bits to use + * @throws DataDictionaryException if the stop bits value is not supported or invalid */ - public void setStopBits(Number stopBits) throws DataDictionaryException - { + public void setStopBits(Number stopBits) throws DataDictionaryException { this.setStopBits((Object) stopBits); } /** - * Set sync read timeout + * Sets the timeout for synchronous read operations. + *

    + * This timeout determines how long the system will wait for data to be + * available on the serial port before timing out the read operation. + * If not set, defaults to {@link #SYNC_READ_TIMEOUT_DEFAULT_VALUE}. * - * @param syncReadTimeout - * @throws DataDictionaryException + * @param syncReadTimeout the timeout value in milliseconds + * @throws DataDictionaryException if the timeout value is invalid or negative */ - public void setSyncReadTimeout(Number syncReadTimeout) throws DataDictionaryException - { + public void setSyncReadTimeout(Number syncReadTimeout) throws DataDictionaryException { this.setSyncReadTimeout((Object) syncReadTimeout); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setPortId(Object portId) throws DataDictionaryException - { + /** + * Internal method to set the port ID with object conversion. + * + * @param portId the port ID object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setPortId(Object portId) throws DataDictionaryException { this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); } - protected void setPortName(Object portName) throws DataDictionaryException - { + /** + * Internal method to set the port name with object conversion. + * + * @param portName the port name object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setPortName(Object portName) throws DataDictionaryException { this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); } - protected void setBaudrate(Object baudrate) throws DataDictionaryException - { + /** + * Internal method to set the baud rate with object conversion. + * + * @param baudrate the baud rate object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setBaudrate(Object baudrate) throws DataDictionaryException { this.data.put(KEY_BAUDRATE, this.kBaudrate.convert(baudrate)); } - protected void setParity(Object parity) throws DataDictionaryException - { + /** + * Internal method to set the parity with object conversion. + * + * @param parity the parity object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setParity(Object parity) throws DataDictionaryException { this.data.put(KEY_PARITY, this.kParity.convert(parity)); } - protected void setDataBits(Object dataBits) throws DataDictionaryException - { + /** + * Internal method to set the data bits with object conversion. + * + * @param dataBits the data bits object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setDataBits(Object dataBits) throws DataDictionaryException { this.data.put(KEY_DATA_BITS, this.kDataBits.convert(dataBits)); } - protected void setStopBits(Object stopBits) throws DataDictionaryException - { + /** + * Internal method to set the stop bits with object conversion. + * + * @param stopBits the stop bits object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setStopBits(Object stopBits) throws DataDictionaryException { this.data.put(KEY_STOP_BITS, this.kStopBits.convert(stopBits)); } - protected void setSyncReadTimeout(Object syncReadTimeout) throws DataDictionaryException - { + /** + * Internal method to set the sync read timeout with object conversion. + * + * @param syncReadTimeout the sync read timeout object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setSyncReadTimeout(Object syncReadTimeout) throws DataDictionaryException { this.data.put(KEY_SYNC_READ_TIMEOUT, this.kSyncReadTimeout.convert(syncReadTimeout)); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { + /** + * Initializes and configures all key descriptors for parameter validation. + *

    + * This method sets up the validation rules for all serial port configuration + * parameters, including accepted values and required/optional status. + * + * @throws RuntimeException if there is an error during key descriptor initialization + */ + private void loadKeyDescriptors() { + try { this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); this.keys.add(this.kPortId); @@ -423,9 +498,7 @@ private void loadKeyDescriptors() this.keys.add(this.kSyncReadTimeout); this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { + } catch (DataDictionaryException e) { throw new RuntimeException(e.getMessage(), e); } } diff --git a/src/main/java/enedis/lab/io/channels/serialport/Parity.java b/src/main/java/enedis/lab/io/channels/serialport/Parity.java index b5caaf4..4872cfe 100644 --- a/src/main/java/enedis/lab/io/channels/serialport/Parity.java +++ b/src/main/java/enedis/lab/io/channels/serialport/Parity.java @@ -8,18 +8,58 @@ package enedis.lab.io.channels.serialport; /** - * Parity enum + * Enumeration representing parity settings for serial communication. + *

    + * Parity is used for error detection in serial communication by adding an extra + * bit to each data frame. The parity bit is calculated based on the number of + * 1-bits in the data and the selected parity type. + *

    + * This enum is used in {@link ChannelSerialPortConfiguration} to specify + * the parity setting for serial port communication channels. + * + * @author Enedis Smarties team + * @see ChannelSerialPortConfiguration */ -public enum Parity -{ - /** None */ +public enum Parity { + + /** + * No parity bit is added to the data frame. + *

    + * This setting disables parity checking, which means no error detection + * is performed on the data bits. + */ NONE, - /** Even */ + + /** + * Even parity is used for error detection. + *

    + * The parity bit is set to 1 if the number of 1-bits in the data is odd, + * ensuring the total number of 1-bits (including parity) is even. + */ EVEN, - /** Odd */ + + /** + * Odd parity is used for error detection. + *

    + * The parity bit is set to 1 if the number of 1-bits in the data is even, + * ensuring the total number of 1-bits (including parity) is odd. + */ ODD, - /** Mark */ + + /** + * Mark parity is used for error detection. + *

    + * The parity bit is always set to 1, regardless of the data content. + * This is typically used for special communication protocols. + */ MARK, - /** Space */ + + /** + * Space parity is used for error detection. + *

    + * The parity bit is always set to 0, regardless of the data content. + * This is typically used for special communication protocols. + */ SPACE + } \ No newline at end of file From 6b6aa4d72ef70fa260f372da12fe58af41698846 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Thu, 25 Sep 2025 15:54:39 +0200 Subject: [PATCH 13/59] chore: use mvn spotless --- pom.xml | 10 + src/main/java/enedis/lab/codec/Codec.java | 35 ++- .../java/enedis/lab/codec/CodecException.java | 224 +++++++++--------- 3 files changed, 137 insertions(+), 132 deletions(-) diff --git a/pom.xml b/pom.xml index 1b9a087..5efdc3e 100644 --- a/pom.xml +++ b/pom.xml @@ -312,6 +312,16 @@ SPDX-License-Identifier: Apache-2.0 + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + + + diff --git a/src/main/java/enedis/lab/codec/Codec.java b/src/main/java/enedis/lab/codec/Codec.java index 3627dc0..6beb320 100644 --- a/src/main/java/enedis/lab/codec/Codec.java +++ b/src/main/java/enedis/lab/codec/Codec.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -16,22 +16,21 @@ */ public interface Codec { - /** - * Decode type K to type T - * - * @param object - * @return instance of T - * @throws CodecException - */ - public T decode(K object) throws CodecException; - - /** - * Encode type T to type K - * - * @param object - * @return instance of K - * @throws CodecException - */ - public K encode(T object) throws CodecException; + /** + * Decode type K to type T + * + * @param object + * @return instance of T + * @throws CodecException + */ + public T decode(K object) throws CodecException; + /** + * Encode type T to type K + * + * @param object + * @return instance of K + * @throws CodecException + */ + public K encode(T object) throws CodecException; } diff --git a/src/main/java/enedis/lab/codec/CodecException.java b/src/main/java/enedis/lab/codec/CodecException.java index a6a7094..5e31d75 100644 --- a/src/main/java/enedis/lab/codec/CodecException.java +++ b/src/main/java/enedis/lab/codec/CodecException.java @@ -1,114 +1,110 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.codec; - -/** - * Exception thrown during codec operations. - * - * This exception is used to signal errors occurring - * during data encoding and decoding processes. - * - * @author Enedis Smarties team - */ -public class CodecException extends Exception { - - private static final long serialVersionUID = 6029680699870915485L; - - private Object data; - - /** - * Creates a new CodecException with no detail message. - */ - public CodecException() { - super(); - } - - /** - * Creates a new CodecException with the specified detail message and cause. - * - * @param message the detail message (which is saved for later retrieval - * by the {@link Throwable#getMessage()} method) - * @param cause the cause (which is saved for later retrieval by the - * {@link Throwable#getCause()} method) - */ - public CodecException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Creates a new CodecException with the specified detail message. - * - * @param message the detail message (which is saved for later retrieval - * by the {@link Throwable#getMessage()} method) - */ - public CodecException(String message) { - super(message); - } - - /** - * Creates a new CodecException with the specified detail message and data. - * - * @param message the detail message (which is saved for later retrieval - * by the {@link Throwable#getMessage()} method) - * @param data additional data object associated with this exception - */ - public CodecException(String message, Object data) { - super(message); - this.data = data; - } - - /** - * Creates a new CodecException with the specified cause. - * - * @param cause the cause (which is saved for later retrieval by the - * {@link Throwable#getCause()} method) - */ - public CodecException(Throwable cause) { - super(cause); - } - - /** - * Creates and throws a CodecException for invalid value scenarios. - * - * @param info additional information about the invalid value - * @throws CodecException always thrown with message "Invalid value : " + info - */ - public static void raiseInvalidValue(String info) throws CodecException { - throw new CodecException("Invalid value : " + info); - } - - /** - * Creates and throws a CodecException for missing value scenarios. - * - * @param value the name or identifier of the missing value - * @throws CodecException always thrown with message "Missing value : " + value - */ - public static void raiseMissingValue(String value) throws CodecException { - throw new CodecException("Missing value : " + value); - } - - /** - * Creates and throws a CodecException for data inconsistency scenarios. - * - * @param info additional information about the inconsistency - * @throws CodecException always thrown with message "Inconsistency : " + info - */ - public static void raiseInconsistency(String info) throws CodecException { - throw new CodecException("Inconsistency : " + info); - } - - /** - * Returns the additional data object associated with this exception. - * - * @return the data object, or null if no data was provided - */ - public Object getData() { - return this.data; - } - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.codec; + +/** + * Exception thrown during codec operations. + * + *

    This exception is used to signal errors occurring during data encoding and decoding processes. + * + * @author Enedis Smarties team + */ +public class CodecException extends Exception { + + private static final long serialVersionUID = 6029680699870915485L; + + private Object data; + + /** Creates a new CodecException with no detail message. */ + public CodecException() { + super(); + } + + /** + * Creates a new CodecException with the specified detail message and cause. + * + * @param message the detail message (which is saved for later retrieval by the {@link + * Throwable#getMessage()} method) + * @param cause the cause (which is saved for later retrieval by the {@link Throwable#getCause()} + * method) + */ + public CodecException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new CodecException with the specified detail message. + * + * @param message the detail message (which is saved for later retrieval by the {@link + * Throwable#getMessage()} method) + */ + public CodecException(String message) { + super(message); + } + + /** + * Creates a new CodecException with the specified detail message and data. + * + * @param message the detail message (which is saved for later retrieval by the {@link + * Throwable#getMessage()} method) + * @param data additional data object associated with this exception + */ + public CodecException(String message, Object data) { + super(message); + this.data = data; + } + + /** + * Creates a new CodecException with the specified cause. + * + * @param cause the cause (which is saved for later retrieval by the {@link Throwable#getCause()} + * method) + */ + public CodecException(Throwable cause) { + super(cause); + } + + /** + * Creates and throws a CodecException for invalid value scenarios. + * + * @param info additional information about the invalid value + * @throws CodecException always thrown with message "Invalid value : " + info + */ + public static void raiseInvalidValue(String info) throws CodecException { + throw new CodecException("Invalid value : " + info); + } + + /** + * Creates and throws a CodecException for missing value scenarios. + * + * @param value the name or identifier of the missing value + * @throws CodecException always thrown with message "Missing value : " + value + */ + public static void raiseMissingValue(String value) throws CodecException { + throw new CodecException("Missing value : " + value); + } + + /** + * Creates and throws a CodecException for data inconsistency scenarios. + * + * @param info additional information about the inconsistency + * @throws CodecException always thrown with message "Inconsistency : " + info + */ + public static void raiseInconsistency(String info) throws CodecException { + throw new CodecException("Inconsistency : " + info); + } + + /** + * Returns the additional data object associated with this exception. + * + * @return the data object, or null if no data was provided + */ + public Object getData() { + return this.data; + } +} From 40d9445492f18b0493714d31a7d745fea915dfe1 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Mon, 29 Sep 2025 12:15:54 +0200 Subject: [PATCH 14/59] chore: format enedis.lab.io.channels package --- .../java/enedis/lab/io/channels/Channel.java | 281 ++++--- .../enedis/lab/io/channels/ChannelBase.java | 291 ++++---- .../lab/io/channels/ChannelConfiguration.java | 690 ++++++++++-------- .../lab/io/channels/ChannelDirection.java | 73 +- .../lab/io/channels/ChannelException.java | 438 ++++++----- .../lab/io/channels/ChannelListener.java | 178 +++-- .../lab/io/channels/ChannelPhysical.java | 446 ++++++----- .../lab/io/channels/ChannelProtocol.java | 95 ++- .../enedis/lab/io/channels/ChannelStatus.java | 63 +- 9 files changed, 1438 insertions(+), 1117 deletions(-) diff --git a/src/main/java/enedis/lab/io/channels/Channel.java b/src/main/java/enedis/lab/io/channels/Channel.java index 45ee2ec..1a8aa15 100644 --- a/src/main/java/enedis/lab/io/channels/Channel.java +++ b/src/main/java/enedis/lab/io/channels/Channel.java @@ -1,110 +1,171 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.util.task.Notifier; -import enedis.lab.util.task.Task; - -/** - * Channel interface - */ -public interface Channel extends Task, Notifier -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Setup the channel from a configuration - * - * @param configuration - * the channel configuration - * - * @throws ChannelException - */ - public void setup(ChannelConfiguration configuration) throws ChannelException; - - /** - * Read data on the channel - * - * @return bytes - * @throws ChannelException - */ - public byte[] read() throws ChannelException; - - /** - * Write data on the channel - * - * @param data - * @throws ChannelException - */ - public void write(byte[] data) throws ChannelException; - - /** - * Get the channel name - * - * @return the channel's name - */ - public String getName(); - - /** - * Get the channel protocol - * - * @return the protocol used by the channel - */ - public ChannelProtocol getProtocol(); - - /** - * Get the channel direction - * - * @return the direction of the channel - */ - public ChannelDirection getDirection(); - - /** - * Get channel status - * - * @return the channel's status - */ - public ChannelStatus getStatus(); - - /** - * Get channel configuration - * - * @return channel configuration - */ - public ChannelConfiguration getConfiguration(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // none - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // none -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import enedis.lab.util.task.Notifier; +import enedis.lab.util.task.Task; + +/** + * Interface defining the contract for communication channels. + * + *

    + * A channel represents a communication endpoint that can be used to send and + * receive data. This + * interface extends both {@link Task} and {@link Notifier} to provide task + * execution capabilities + * and event notification to registered {@link ChannelListener} instances. + * + *

    + * Channels support different protocols (e.g., serial port, TCP, UDP) and + * directions + * (receive-only, transmit-only, or bidirectional). Each channel must be + * configured before use and + * can be monitored for status changes. + * + *

    + * Implementations of this interface should handle the specific communication + * protocol details + * while providing a unified interface for data exchange. + * + * @author Enedis Smarties team + * @see Task + * @see Notifier + * @see ChannelListener + * @see ChannelConfiguration + * @see ChannelProtocol + * @see ChannelDirection + * @see ChannelStatus + */ +public interface Channel extends Task, Notifier { + + /** + * Initializes and configures the channel with the provided configuration. + * + *

    + * This method must be called before any read or write operations can be + * performed. The + * configuration contains all necessary parameters for the specific channel + * type, such as port + * settings for serial channels or network settings for TCP channels. + * + *

    + * After successful setup, the channel should be ready for communication + * operations. + * + * @param configuration the channel configuration containing all necessary + * parameters + * @throws ChannelException if the configuration is invalid, the channel cannot + * be initialized, or + * there are resource allocation issues + */ + public void setup(ChannelConfiguration configuration) throws ChannelException; + + /** + * Reads data from the channel. + * + *

    + * This method performs a synchronous read operation, blocking until data is + * available or a + * timeout occurs (depending on the channel implementation). The returned byte + * array contains the + * raw data received from the channel. + * + *

    + * The channel must be properly configured and opened before calling this + * method. + * + * @return a byte array containing the data read from the channel, or an empty + * array if no data is + * available + * @throws ChannelException if the read operation fails, the channel is not + * properly configured, + * or a communication error occurs + */ + public byte[] read() throws ChannelException; + + /** + * Writes data to the channel. + * + *

    + * This method performs a synchronous write operation, sending the provided data + * through the + * channel. The operation blocks until the data is successfully transmitted or + * an error occurs. + * + *

    + * The channel must be properly configured and opened before calling this + * method. + * + * @param data the byte array containing the data to be written to the channel + * @throws ChannelException if the write operation fails, the channel is not + * properly configured, + * or a communication error occurs + */ + public void write(byte[] data) throws ChannelException; + + /** + * Retrieves the name of this channel. + * + *

    + * The channel name is typically used for identification and logging purposes. + * It is usually + * set during channel creation or configuration. + * + * @return the channel name, or null if not set + */ + public String getName(); + + /** + * Retrieves the communication protocol used by this channel. + * + *

    + * The protocol determines the underlying communication mechanism, such as + * serial port, TCP, + * UDP, or other supported protocols. + * + * @return the channel protocol, or null if not set + */ + public ChannelProtocol getProtocol(); + + /** + * Retrieves the communication direction of this channel. + * + *

    + * The direction indicates whether the channel can receive data (RX), transmit + * data (TX), or + * both (RXTX). This affects which operations are available. + * + * @return the channel direction, or null if not set + */ + public ChannelDirection getDirection(); + + /** + * Retrieves the current status of this channel. + * + *

    + * The status indicates the operational state of the channel, such as whether it + * is open, + * closed, error state, or in the process of opening/closing. + * + * @return the current channel status + */ + public ChannelStatus getStatus(); + + /** + * Retrieves the configuration used by this channel. + * + *

    + * The configuration contains all the parameters that were used to set up the + * channel, + * including protocol-specific settings and operational parameters. + * + * @return the channel configuration, or null if the channel has not been + * configured + */ + public ChannelConfiguration getConfiguration(); +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelBase.java b/src/main/java/enedis/lab/io/channels/ChannelBase.java index 140687a..7f9476c 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelBase.java +++ b/src/main/java/enedis/lab/io/channels/ChannelBase.java @@ -1,143 +1,148 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.task.TaskPeriodic; - -/** - * Channel base - */ -public abstract class ChannelBase extends TaskPeriodic implements Channel -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Logger logger; - protected ChannelConfiguration configuration; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param configuration - * configuration used to set the channel - * @throws ChannelException - */ - protected ChannelBase(ChannelConfiguration configuration) throws ChannelException - { - this.logger = LogManager.getLogger(this.getClass()); - this.setup(configuration); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Channel - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException - { - /* 1. Check configuration */ - if (configuration == null) - { - ChannelException.raiseInvalidConfiguration("null"); - } - /* 2. Copy configuration */ - ChannelConfiguration configurationBackUp = null; - try - { - if (this.configuration == null) - { - this.configuration = (ChannelConfiguration) configuration.clone(); - } - else - { - configurationBackUp = (ChannelConfiguration) this.configuration.clone(); - this.configuration.copy(configuration); - } - } - catch (DataDictionaryException exception) - { - ChannelException.raiseInvalidConfiguration(exception.getMessage()); - } - /* 3. Set up from internal configuration */ - try - { - this.setup(); - } - catch (Exception exception) - { - /* 4. Restore internal configuration */ - if (configurationBackUp != null) - { - try - { - this.configuration.copy(configurationBackUp); - } - catch (DataDictionaryException dataDictionaryException) - { - this.logger.error("", dataDictionaryException); - } - } - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setup() throws ChannelException - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.task.TaskPeriodic; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Abstract base class providing common functionality for channel + * implementations. + * + *

    + * This class serves as a foundation for all channel implementations, providing + * common setup + * logic, configuration management, and logging capabilities. It extends + * {@link TaskPeriodic} to + * provide periodic task execution and implements the {@link Channel} interface. + * + *

    + * The class handles configuration validation, backup and restoration in case of + * setup failures, + * and provides a template method pattern for channel-specific setup operations + * through the abstract + * {@link #setup()} method. + * + *

    + * Subclasses should implement the abstract {@link #setup()} method to provide + * channel-specific + * initialization logic. + * + * @author Enedis Smarties team + * @see Channel + * @see TaskPeriodic + * @see ChannelConfiguration + */ +public abstract class ChannelBase extends TaskPeriodic implements Channel { + + /** Logger instance for this channel implementation. */ + protected Logger logger; + + /** Configuration used by this channel. */ + protected ChannelConfiguration configuration; + + /** + * Constructs a new channel with the specified configuration. + * + *

    + * This constructor initializes the logger for the specific channel + * implementation and + * immediately sets up the channel using the provided configuration. If the + * setup fails, a {@link + * ChannelException} is thrown. + * + * @param configuration the configuration to use for setting up the channel + * @throws ChannelException if the configuration is invalid or the channel + * cannot be properly + * initialized + */ + protected ChannelBase(ChannelConfiguration configuration) throws ChannelException { + this.logger = LogManager.getLogger(this.getClass()); + this.setup(configuration); + } + + /** + * Sets up the channel with the provided configuration. + * + *

    + * This method implements a robust setup process that includes: + * + *

      + *
    • Configuration validation + *
    • Configuration backup (if reconfiguring) + *
    • Channel-specific setup via {@link #setup()} + *
    • Automatic rollback on setup failure + *
    + * + *

    + * If the setup fails, the previous configuration is automatically restored to + * maintain the + * channel in a consistent state. + * + * @param configuration the configuration to apply to the channel + * @throws ChannelException if the configuration is null, invalid, or if the + * channel-specific + * setup fails + */ + @Override + public void setup(ChannelConfiguration configuration) throws ChannelException { + /* Check configuration */ + if (configuration == null) { + ChannelException.raiseInvalidConfiguration("null"); + } + /* Copy configuration */ + ChannelConfiguration configurationBackUp = null; + try { + if (this.configuration == null) { + this.configuration = (ChannelConfiguration) configuration.clone(); + } else { + configurationBackUp = (ChannelConfiguration) this.configuration.clone(); + this.configuration.copy(configuration); + } + } catch (DataDictionaryException exception) { + ChannelException.raiseInvalidConfiguration(exception.getMessage()); + } + /* Set up from internal configuration */ + try { + this.setup(); + } catch (Exception exception) { + /* Restore internal configuration */ + if (configurationBackUp != null) { + try { + this.configuration.copy(configurationBackUp); + } catch (DataDictionaryException dataDictionaryException) { + this.logger.error("", dataDictionaryException); + } + } + } + } + + /** + * Performs channel-specific setup operations. + * + *

    + * This method is called by the public {@link #setup(ChannelConfiguration)} + * method after the + * configuration has been validated and applied. Subclasses should override this + * method to + * implement channel-specific initialization logic, such as opening + * communication ports, + * establishing network connections, or initializing hardware. + * + *

    + * If this method throws an exception, the configuration will be automatically + * rolled back to + * its previous state. + * + * @throws ChannelException if the channel-specific setup fails + */ + protected void setup() throws ChannelException { + } +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java b/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java index 18fac19..610e1c6 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java +++ b/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java @@ -1,296 +1,394 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.configuration.ConfigurationBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * ChannelConfiguration class - * - * Generated - */ -public class ChannelConfiguration extends ConfigurationBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_NAME = "name"; - protected static final String KEY_PROTOCOL = "protocol"; - protected static final String KEY_DIRECTION = "direction"; - protected static final String KEY_ALIAS = "alias"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kName; - protected KeyDescriptorEnum kProtocol; - protected KeyDescriptorEnum kDirection; - protected KeyDescriptorString kAlias; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ChannelConfiguration() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ChannelConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ChannelConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public ChannelConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param protocol - * @param direction - * @param alias - * @throws DataDictionaryException - */ - public ChannelConfiguration(String name, ChannelProtocol protocol, ChannelDirection direction, String alias) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setProtocol(protocol); - this.setDirection(direction); - this.setAlias(alias); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ConfigurationBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get name - * - * @return the name - */ - public String getName() - { - return (String) this.data.get(KEY_NAME); - } - - /** - * Get protocol - * - * @return the protocol - */ - public ChannelProtocol getProtocol() - { - return (ChannelProtocol) this.data.get(KEY_PROTOCOL); - } - - /** - * Get direction - * - * @return the direction - */ - public ChannelDirection getDirection() - { - return (ChannelDirection) this.data.get(KEY_DIRECTION); - } - - /** - * Get alias - * - * @return the alias - */ - public String getAlias() - { - return (String) this.data.get(KEY_ALIAS); - } - - /** - * Set name - * - * @param name - * @throws DataDictionaryException - */ - public void setName(String name) throws DataDictionaryException - { - this.setName((Object) name); - } - - /** - * Set protocol - * - * @param protocol - * @throws DataDictionaryException - */ - public void setProtocol(ChannelProtocol protocol) throws DataDictionaryException - { - this.setProtocol((Object) protocol); - } - - /** - * Set direction - * - * @param direction - * @throws DataDictionaryException - */ - public void setDirection(ChannelDirection direction) throws DataDictionaryException - { - this.setDirection((Object) direction); - } - - /** - * Set alias - * - * @param alias - * @throws DataDictionaryException - */ - public void setAlias(String alias) throws DataDictionaryException - { - this.setAlias((Object) alias); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setName(Object name) throws DataDictionaryException - { - this.data.put(KEY_NAME, this.kName.convert(name)); - } - - protected void setProtocol(Object protocol) throws DataDictionaryException - { - this.data.put(KEY_PROTOCOL, this.kProtocol.convert(protocol)); - } - - protected void setDirection(Object direction) throws DataDictionaryException - { - this.data.put(KEY_DIRECTION, this.kDirection.convert(direction)); - } - - protected void setAlias(Object alias) throws DataDictionaryException - { - this.data.put(KEY_ALIAS, this.kAlias.convert(alias)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kName = new KeyDescriptorString(KEY_NAME, true, false); - this.keys.add(this.kName); - - this.kProtocol = new KeyDescriptorEnum(KEY_PROTOCOL, true, ChannelProtocol.class); - this.keys.add(this.kProtocol); - - this.kDirection = new KeyDescriptorEnum(KEY_DIRECTION, true, ChannelDirection.class); - this.keys.add(this.kDirection); - - this.kAlias = new KeyDescriptorString(KEY_ALIAS, false, false); - this.keys.add(this.kAlias); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.configuration.ConfigurationBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import enedis.lab.types.datadictionary.KeyDescriptorString; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Configuration class for communication channels. + * + *

    + * This class extends {@link ConfigurationBase} and provides the base + * configuration parameters + * required for all types of communication channels. It defines the essential + * properties that every + * channel must have: name, protocol, direction, and alias. + * + *

    + * The configuration supports validation through key descriptors and provides + * methods for setting + * and retrieving channel parameters. It can be instantiated from various + * sources including maps, + * data dictionaries, and direct parameter values. + * + *

    + * This class serves as the foundation for more specific channel configurations. + * + * @author Enedis Smarties team + * @see ConfigurationBase + * @see ChannelProtocol + * @see ChannelDirection + */ +public class ChannelConfiguration extends ConfigurationBase { + + /** Key for the channel name configuration parameter. */ + protected static final String KEY_NAME = "name"; + + /** Key for the channel protocol configuration parameter. */ + protected static final String KEY_PROTOCOL = "protocol"; + + /** Key for the channel direction configuration parameter. */ + protected static final String KEY_DIRECTION = "direction"; + + /** Key for the channel alias configuration parameter. */ + protected static final String KEY_ALIAS = "alias"; + + /** List of key descriptors for configuration validation. */ + private List> keys = new ArrayList>(); + + /** Key descriptor for channel name validation. */ + protected KeyDescriptorString kName; + + /** Key descriptor for channel protocol validation. */ + protected KeyDescriptorEnum kProtocol; + + /** Key descriptor for channel direction validation. */ + protected KeyDescriptorEnum kDirection; + + /** Key descriptor for channel alias validation. */ + protected KeyDescriptorString kAlias; + + /** + * Default constructor for channel configuration. + * + *

    + * Initializes the configuration with default values and sets up the key + * descriptors for + * parameter validation. + */ + protected ChannelConfiguration() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructs a new channel configuration from a map of key-value pairs. + * + *

    + * This constructor initializes the configuration with values from the provided + * map, performing + * validation and setting default values for missing parameters. + * + * @param map the map containing configuration parameters + * @throws DataDictionaryException if the map contains invalid values or + * required parameters are + * missing + */ + public ChannelConfiguration(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructs a new channel configuration by copying from another data + * dictionary. + * + *

    + * This constructor creates a new configuration instance by copying all + * parameters from the + * provided data dictionary, ensuring consistency between configurations. + * + * @param other the data dictionary to copy configuration from + * @throws DataDictionaryException if the source data dictionary contains + * invalid values or is + * incompatible with channel configuration + */ + public ChannelConfiguration(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructs a new channel configuration with a name and file reference, + * initializing all + * parameters to their default values. + * + *

    + * This constructor is typically used when creating a new configuration that + * will be populated + * later with specific channel parameters. + * + * @param name the configuration name + * @param file the configuration file associated with this configuration + */ + public ChannelConfiguration(String name, File file) { + this(); + this.init(name, file); + } + + /** + * Constructs a new channel configuration with all parameters set to specific + * values. + * + *

    + * This constructor provides a convenient way to create a fully configured + * channel with all + * necessary parameters specified at construction time. + * + * @param name the channel name + * @param protocol the communication protocol to use + * @param direction the communication direction (RX, TX, or RXTX) + * @param alias the channel alias (optional) + * @throws DataDictionaryException if any of the provided values are invalid or + * incompatible with + * channel configuration requirements + */ + public ChannelConfiguration( + String name, ChannelProtocol protocol, ChannelDirection direction, String alias) + throws DataDictionaryException { + this(); + + this.setName(name); + this.setProtocol(protocol); + this.setDirection(direction); + this.setAlias(alias); + + this.checkAndUpdate(); + } + + /** + * Updates optional parameters with default values if not already set. + * + *

    + * This method is called during configuration validation to ensure that all + * required parameters + * are properly set. For the base channel configuration, this method delegates + * to the parent class + * implementation. + * + * @throws DataDictionaryException if there are validation errors with the + * configuration + */ + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + super.updateOptionalParameters(); + } + + /** + * Retrieves the channel name. + * + *

    + * The channel name is used for identification and logging purposes. It is + * typically set during + * channel creation or configuration. + * + * @return the channel name, or null if not set + */ + public String getName() { + return (String) this.data.get(KEY_NAME); + } + + /** + * Retrieves the communication protocol used by this channel. + * + *

    + * The protocol determines the underlying communication mechanism, such as + * serial port, TCP, + * UDP, or other supported protocols. + * + * @return the channel protocol, or null if not set + */ + public ChannelProtocol getProtocol() { + return (ChannelProtocol) this.data.get(KEY_PROTOCOL); + } + + /** + * Retrieves the communication direction of this channel. + * + *

    + * The direction indicates whether the channel can receive data (RX), transmit + * data (TX), or + * both (RXTX). This affects which operations are available. + * + * @return the channel direction, or null if not set + */ + public ChannelDirection getDirection() { + return (ChannelDirection) this.data.get(KEY_DIRECTION); + } + + /** + * Retrieves the channel alias. + * + *

    + * The alias provides an alternative identifier for the channel, typically used + * for + * user-friendly display or alternative referencing. + * + * @return the channel alias, or null if not set + */ + public String getAlias() { + return (String) this.data.get(KEY_ALIAS); + } + + /** + * Sets the channel name. + * + *

    + * The channel name is used for identification and logging purposes. It must be + * a non-empty + * string and is required for a valid configuration. + * + * @param name the channel name to set + * @throws DataDictionaryException if the name is invalid, empty, or null + */ + public void setName(String name) throws DataDictionaryException { + this.setName((Object) name); + } + + /** + * Sets the communication protocol for this channel. + * + *

    + * The protocol determines the underlying communication mechanism. Must be one + * of the supported + * {@link ChannelProtocol} values. + * + * @param protocol the communication protocol to use + * @throws DataDictionaryException if the protocol is invalid or not supported + */ + public void setProtocol(ChannelProtocol protocol) throws DataDictionaryException { + this.setProtocol((Object) protocol); + } + + /** + * Sets the communication direction for this channel. + * + *

    + * The direction determines whether the channel can receive data (RX), transmit + * data (TX), or + * both (RXTX). Must be one of the supported {@link ChannelDirection} values. + * + * @param direction the communication direction to use + * @throws DataDictionaryException if the direction is invalid or not supported + */ + public void setDirection(ChannelDirection direction) throws DataDictionaryException { + this.setDirection((Object) direction); + } + + /** + * Sets the channel alias. + * + *

    + * The alias provides an alternative identifier for the channel, typically used + * for + * user-friendly display or alternative referencing. This parameter is optional + * and can be null. + * + * @param alias the channel alias to set (can be null) + * @throws DataDictionaryException if the alias format is invalid + */ + public void setAlias(String alias) throws DataDictionaryException { + this.setAlias((Object) alias); + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Internal method to set the channel name with object conversion. + * + * @param name the name object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setName(Object name) throws DataDictionaryException { + this.data.put(KEY_NAME, this.kName.convert(name)); + } + + /** + * Internal method to set the channel protocol with object conversion. + * + * @param protocol the protocol object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setProtocol(Object protocol) throws DataDictionaryException { + this.data.put(KEY_PROTOCOL, this.kProtocol.convert(protocol)); + } + + /** + * Internal method to set the channel direction with object conversion. + * + * @param direction the direction object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setDirection(Object direction) throws DataDictionaryException { + this.data.put(KEY_DIRECTION, this.kDirection.convert(direction)); + } + + /** + * Internal method to set the channel alias with object conversion. + * + * @param alias the alias object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setAlias(Object alias) throws DataDictionaryException { + this.data.put(KEY_ALIAS, this.kAlias.convert(alias)); + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Initializes and configures all key descriptors for parameter validation. + * + *

    + * This method sets up the validation rules for all channel configuration + * parameters, including + * required/optional status and type validation. + * + * @throws RuntimeException if there is an error during key descriptor + * initialization + */ + private void loadKeyDescriptors() { + try { + this.kName = new KeyDescriptorString(KEY_NAME, true, false); + this.keys.add(this.kName); + + this.kProtocol = new KeyDescriptorEnum(KEY_PROTOCOL, true, ChannelProtocol.class); + this.keys.add(this.kProtocol); + + this.kDirection = new KeyDescriptorEnum(KEY_DIRECTION, true, ChannelDirection.class); + this.keys.add(this.kDirection); + + this.kAlias = new KeyDescriptorString(KEY_ALIAS, false, false); + this.keys.add(this.kAlias); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelDirection.java b/src/main/java/enedis/lab/io/channels/ChannelDirection.java index 95398f3..47add1a 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelDirection.java +++ b/src/main/java/enedis/lab/io/channels/ChannelDirection.java @@ -1,22 +1,51 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -/** - * Enumeration of channel direction - * - */ -public enum ChannelDirection -{ - /** Reception */ - RX, - /** Sending */ - TX, - /** Reception and sending */ - RXTX -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +/** + * Enumeration representing the communication direction for channels. + *

    + * This enum defines the possible communication directions that a channel + * can support. The direction determines which operations are available + * on the channel (read, write, or both). + *

    + * This enum is used in {@link ChannelConfiguration} to specify + * the communication direction for channel instances. + * + * @author Enedis Smarties team + * @see ChannelConfiguration + * @see Channel + */ +public enum ChannelDirection { + + /** + * Receive-only direction. + *

    + * Channels with this direction can only receive data from the communication + * endpoint. Read operations are supported, but write operations will fail. + */ + RX, + + /** + * Transmit-only direction. + *

    + * Channels with this direction can only send data to the communication + * endpoint. Write operations are supported, but read operations will fail. + */ + TX, + + /** + * Bidirectional communication. + *

    + * Channels with this direction can both receive and send data. + * Both read and write operations are supported, enabling full + * bidirectional communication. This is the most common direction + * for interactive communication channels. + */ + RXTX +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelException.java b/src/main/java/enedis/lab/io/channels/ChannelException.java index 18ff7c0..6bc1466 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelException.java +++ b/src/main/java/enedis/lab/io/channels/ChannelException.java @@ -1,194 +1,244 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.types.ExceptionBase; - -/** - * Class used for the channel exceptions - */ -public class ChannelException extends ExceptionBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -3507876436535833098L; - - /** Invalid channel error code */ - public static final int ERRCODE_INVALID_CHANNEL = -5; - /** Already exists error code */ - public static final int ERRCODE_ALREADY_EXISTS = -6; - /** Internal error error code */ - public static final int ERRCODE_INTERNAL_ERROR = -7; - /** Channel not ready error code */ - public static final int ERRCODE_CHANNEL_NOT_READY = -8; - /** Invalid configuration type error code */ - public static final int ERRCODE_INVALID_CONFIGURATION_TYPE = -9; - /** Invalid configuration error code */ - public static final int ERRCODE_INVALID_CONFIGURATION = -10; - /** Operation denied error code */ - public static final int ERRCODE_OPERATION_DENIED = -11; - /** Channel doesn't exist error code */ - public static final int ERRCODE_CHANNEL_DOESNT_EXIST = -12; - /** Unexpected error code */ - public static final int ERRCODE_UNEXPECTED = -99; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * @param channelType - * @throws ChannelException - */ - public static void raiseUnhandledChannelType(String channelType) throws ChannelException - { - throw new ChannelException(ERRCODE_INVALID_CHANNEL, channelType + " channel is not handled"); - } - - /** - * @param channelType - * @param expectedChannelType - * @throws ChannelException - */ - public static void raiseConflictingChannelType(String channelType, String expectedChannelType) throws ChannelException - { - throw new ChannelException(ERRCODE_INVALID_CHANNEL, channelType + " channel is conflicting with " + expectedChannelType); - } - - /** - * @param info - * @throws ChannelException - */ - public static void raiseInternalError(String info) throws ChannelException - { - throw new ChannelException(ERRCODE_INTERNAL_ERROR, info); - } - - /** - * @param info - * @throws ChannelException - */ - public static void raiseChannelNotReady(String info) throws ChannelException - { - throw new ChannelException(ERRCODE_CHANNEL_NOT_READY, info); - } - - /** - * @param configuration - * @param expected_configuration_name - * @throws ChannelException - */ - public static void raiseInvalidConfigurationType(ChannelConfiguration configuration, String expected_configuration_name) throws ChannelException - { - throw new ChannelException(ERRCODE_INVALID_CONFIGURATION_TYPE, - "Configuration " + configuration.getClass().getSimpleName() + " is not a valid type (" + expected_configuration_name + " expected)"); - } - - /** - * @param info - * @throws ChannelException - */ - public static void raiseInvalidConfiguration(String info) throws ChannelException - { - throw new ChannelException(ERRCODE_INVALID_CONFIGURATION, "Configuration is invalid (" + info + ")"); - } - - /** - * @param operation - * @throws ChannelException - */ - public static void raiseOperationDenied(String operation) throws ChannelException - { - throw new ChannelException(ERRCODE_OPERATION_DENIED, "Operation \'" + operation + "\' is not allowed"); - } - - /** - * @param info - * @throws ChannelException - */ - public static void raiseUnexpectedError(String info) throws ChannelException - { - throw new ChannelException(ERRCODE_UNEXPECTED, "Unexpected error occurs : " + info); - } - - /** - * @param channelName - * @throws ChannelException - */ - public static void raiseAlreadyExists(String channelName) throws ChannelException - { - throw new ChannelException(ERRCODE_ALREADY_EXISTS, "Channel " + channelName + " already exists"); - } - - /** - * @param channelName - * @throws ChannelException - */ - public static void raiseDoesntExist(String channelName) throws ChannelException - { - throw new ChannelException(ERRCODE_CHANNEL_DOESNT_EXIST, "Channel " + channelName + " doesn't exist"); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * @param code - * @param info - */ - public ChannelException(int code, String info) - { - super(code, info); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import enedis.lab.types.ExceptionBase; + +/** + * Exception class for channel-related errors. + *

    + * This exception is thrown when errors occur during channel operations such as + * setup, configuration, read/write operations, or channel management. It + * extends + * {@link ExceptionBase} to provide structured error handling with error codes + * and detailed error messages. + *

    + * The class provides static factory methods for creating specific types of + * channel exceptions, each with appropriate error codes and descriptive + * messages. + *

    + * Common scenarios that trigger this exception include: + *

      + *
    • Invalid channel configuration
    • + *
    • Channel not ready for operations
    • + *
    • Conflicting channel types
    • + *
    • Operation denied due to channel state
    • + *
    • Internal channel errors
    • + *
    + * + * @author Enedis Smarties team + * @see ExceptionBase + * @see Channel + * @see ChannelConfiguration + */ +public class ChannelException extends ExceptionBase { + + private static final long serialVersionUID = -3507876436535833098L; + + /** Error code for invalid or unsupported channel types. */ + public static final int ERRCODE_INVALID_CHANNEL = -5; + + /** Error code for attempting to create a channel that already exists. */ + public static final int ERRCODE_ALREADY_EXISTS = -6; + + /** Error code for internal channel processing errors. */ + public static final int ERRCODE_INTERNAL_ERROR = -7; + + /** Error code for operations attempted on channels that are not ready. */ + public static final int ERRCODE_CHANNEL_NOT_READY = -8; + + /** Error code for invalid or incompatible configuration types. */ + public static final int ERRCODE_INVALID_CONFIGURATION_TYPE = -9; + + /** Error code for invalid configuration parameters or values. */ + public static final int ERRCODE_INVALID_CONFIGURATION = -10; + + /** + * Error code for operations that are not allowed in the current channel state. + */ + public static final int ERRCODE_OPERATION_DENIED = -11; + + /** Error code for operations attempted on non-existent channels. */ + public static final int ERRCODE_CHANNEL_DOESNT_EXIST = -12; + + /** Error code for unexpected or unhandled errors. */ + public static final int ERRCODE_UNEXPECTED = -99; + + /** + * Creates and throws a ChannelException for unhandled channel types. + *

    + * This method is used when an attempt is made to create or use a channel + * type that is not supported by the system. + * + * @param channelType the unsupported channel type that was requested + * @throws ChannelException always thrown with error code + * {@link #ERRCODE_INVALID_CHANNEL} + */ + public static void raiseUnhandledChannelType(String channelType) throws ChannelException { + throw new ChannelException(ERRCODE_INVALID_CHANNEL, channelType + " channel is not handled"); + } + + /** + * Creates and throws a ChannelException for conflicting channel types. + *

    + * This method is used when a channel type conflicts with an expected type, + * typically during channel configuration or type validation. + * + * @param channelType the actual channel type that was provided + * @param expectedChannelType the expected channel type + * @throws ChannelException always thrown with error code + * {@link #ERRCODE_INVALID_CHANNEL} + */ + public static void raiseConflictingChannelType(String channelType, String expectedChannelType) + throws ChannelException { + throw new ChannelException( + ERRCODE_INVALID_CHANNEL, + channelType + " channel is conflicting with " + expectedChannelType); + } + + /** + * Creates and throws a ChannelException for internal errors. + *

    + * This method is used when an internal error occurs during channel + * processing that cannot be attributed to user input or configuration. + * + * @param info additional information about the internal error + * @throws ChannelException always thrown with error code + * {@link #ERRCODE_INTERNAL_ERROR} + */ + public static void raiseInternalError(String info) throws ChannelException { + throw new ChannelException(ERRCODE_INTERNAL_ERROR, info); + } + + /** + * Creates and throws a ChannelException for channels that are not ready. + *

    + * This method is used when an operation is attempted on a channel that + * is not in a ready state (e.g., not properly configured or initialized). + * + * @param info additional information about why the channel is not ready + * @throws ChannelException always thrown with error code + * {@link #ERRCODE_CHANNEL_NOT_READY} + */ + public static void raiseChannelNotReady(String info) throws ChannelException { + throw new ChannelException(ERRCODE_CHANNEL_NOT_READY, info); + } + + /** + * Creates and throws a ChannelException for invalid configuration types. + *

    + * This method is used when a channel configuration is of the wrong type + * for the expected channel implementation. + * + * @param configuration the invalid configuration that was + * provided + * @param expected_configuration_name the name of the expected configuration + * type + * @throws ChannelException always thrown with error code + * {@link #ERRCODE_INVALID_CONFIGURATION_TYPE} + */ + public static void raiseInvalidConfigurationType( + ChannelConfiguration configuration, String expected_configuration_name) + throws ChannelException { + throw new ChannelException( + ERRCODE_INVALID_CONFIGURATION_TYPE, + "Configuration " + + configuration.getClass().getSimpleName() + + " is not a valid type (" + + expected_configuration_name + + " expected)"); + } + + /** + * Creates and throws a ChannelException for invalid configuration parameters. + *

    + * This method is used when channel configuration parameters are invalid, + * missing, or incompatible. + * + * @param info additional information about the configuration error + * @throws ChannelException always thrown with error code + * {@link #ERRCODE_INVALID_CONFIGURATION} + */ + public static void raiseInvalidConfiguration(String info) throws ChannelException { + throw new ChannelException( + ERRCODE_INVALID_CONFIGURATION, "Configuration is invalid (" + info + ")"); + } + + /** + * Creates and throws a ChannelException for denied operations. + *

    + * This method is used when an operation is attempted that is not allowed + * in the current channel state or configuration. + * + * @param operation the operation that was denied + * @throws ChannelException always thrown with error code + * {@link #ERRCODE_OPERATION_DENIED} + */ + public static void raiseOperationDenied(String operation) throws ChannelException { + throw new ChannelException( + ERRCODE_OPERATION_DENIED, "Operation \'" + operation + "\' is not allowed"); + } + + /** + * Creates and throws a ChannelException for unexpected errors. + *

    + * This method is used when an unexpected or unhandled error occurs + * during channel operations. + * + * @param info additional information about the unexpected error + * @throws ChannelException always thrown with error code + * {@link #ERRCODE_UNEXPECTED} + */ + public static void raiseUnexpectedError(String info) throws ChannelException { + throw new ChannelException(ERRCODE_UNEXPECTED, "Unexpected error occurs : " + info); + } + + /** + * Creates and throws a ChannelException for duplicate channel names. + *

    + * This method is used when attempting to create a channel with a name + * that already exists in the system. + * + * @param channelName the name of the channel that already exists + * @throws ChannelException always thrown with error code + * {@link #ERRCODE_ALREADY_EXISTS} + */ + public static void raiseAlreadyExists(String channelName) throws ChannelException { + throw new ChannelException( + ERRCODE_ALREADY_EXISTS, "Channel " + channelName + " already exists"); + } + + /** + * Creates and throws a ChannelException for non-existent channels. + *

    + * This method is used when attempting to perform operations on a channel + * that does not exist in the system. + * + * @param channelName the name of the channel that does not exist + * @throws ChannelException always thrown with error code + * {@link #ERRCODE_CHANNEL_DOESNT_EXIST} + */ + public static void raiseDoesntExist(String channelName) throws ChannelException { + throw new ChannelException( + ERRCODE_CHANNEL_DOESNT_EXIST, "Channel " + channelName + " doesn't exist"); + } + + /** + * Constructs a new ChannelException with the specified error code and message. + *

    + * This constructor creates a channel exception with a specific error code + * and descriptive message. The error code should be one of the predefined + * constants in this class. + * + * @param code the error code for this exception + * @param info the detailed error message + */ + public ChannelException(int code, String info) { + super(code, info); + } +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelListener.java b/src/main/java/enedis/lab/io/channels/ChannelListener.java index e31a37d..adee055 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelListener.java +++ b/src/main/java/enedis/lab/io/channels/ChannelListener.java @@ -1,73 +1,105 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.Subscriber; - -/** - * Channel listener - */ -public interface ChannelListener extends Subscriber -{ - /** - * On data read - * - * @param channelName - * the channel name - * @param data - * the bytes read - */ - public void onDataRead(String channelName, byte[] data); - - /** - * On data written - * - * @param channelName - * the channel name - * @param data - * the bytes written - */ - public void onDataWritten(String channelName, byte[] data); - - /** - * On status changed - * - * @param channelName - * the channel name - * @param status - * the new status - */ - public void onStatusChanged(String channelName, ChannelStatus status); - - /** - * On error detected - * - * @param channelName - * the channel name - * @param errorCode - * the error code - * @param errorMessage - * the error message - */ - public void onErrorDetected(String channelName, int errorCode, String errorMessage); - - /** - * On error detected - * - * @param channelName - * the channel name - * @param errorCode - * the error code - * @param errorMessage - * the error message - * @param data - * the data associated with error message - */ - public void onErrorDetected(String channelName, int errorCode, String errorMessage, DataDictionary data); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import enedis.lab.types.DataDictionary; +import enedis.lab.util.task.Subscriber; + +/** + * Interface for receiving channel events and notifications. + *

    + * This interface extends {@link Subscriber} and provides methods for handling + * various channel events such as data read/write operations, status changes, + * and error conditions. Implementations of this interface can be registered + * with channels to receive real-time notifications about channel activities. + *

    + * The listener pattern allows for decoupled event handling, enabling multiple + * listeners to be registered with a single channel and receive notifications + * about all channel activities. + *

    + * Common use cases include: + *

      + *
    • Logging channel activities
    • + *
    • Monitoring channel status
    • + *
    • Processing received data
    • + *
    • Error handling and reporting
    • + *
    + * + * @author Enedis Smarties team + * @see Subscriber + * @see Channel + * @see ChannelStatus + */ +public interface ChannelListener extends Subscriber { + + /** + * Called when data is successfully read from a channel. + *

    + * This method is invoked whenever a channel performs a successful read + * operation. + * The received data is provided as a byte array containing the raw data + * that was read from the channel. + * + * @param channelName the name of the channel that read the data + * @param data the raw bytes that were read from the channel + */ + public void onDataRead(String channelName, byte[] data); + + /** + * Called when data is successfully written to a channel. + *

    + * This method is invoked whenever a channel performs a successful write + * operation. + * The transmitted data is provided as a byte array containing the raw data + * that was written to the channel. + * + * @param channelName the name of the channel that wrote the data + * @param data the raw bytes that were written to the channel + */ + public void onDataWritten(String channelName, byte[] data); + + /** + * Called when the status of a channel changes. + *

    + * This method is invoked whenever a channel's operational status changes, + * such as when it goes from closed to open, or from ready to error state. + * This allows listeners to monitor channel state transitions. + * + * @param channelName the name of the channel whose status changed + * @param status the new status of the channel + */ + public void onStatusChanged(String channelName, ChannelStatus status); + + /** + * Called when an error is detected on a channel. + *

    + * This method is invoked whenever an error occurs during channel operations. + * It provides basic error information including the error code and message. + * + * @param channelName the name of the channel where the error occurred + * @param errorCode the numeric error code identifying the type of error + * @param errorMessage a descriptive message explaining the error + */ + public void onErrorDetected(String channelName, int errorCode, String errorMessage); + + /** + * Called when an error is detected on a channel with additional context data. + *

    + * This method is invoked whenever an error occurs during channel operations + * and additional context data is available. It provides comprehensive error + * information including the error code, message, and associated data + * dictionary. + * + * @param channelName the name of the channel where the error occurred + * @param errorCode the numeric error code identifying the type of error + * @param errorMessage a descriptive message explaining the error + * @param data additional context data associated with the error + */ + public void onErrorDetected( + String channelName, int errorCode, String errorMessage, DataDictionary data); +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelPhysical.java b/src/main/java/enedis/lab/io/channels/ChannelPhysical.java index 5295653..0edd50c 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelPhysical.java +++ b/src/main/java/enedis/lab/io/channels/ChannelPhysical.java @@ -1,227 +1,219 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import java.util.Collection; - -import enedis.lab.util.task.NotifierBase; - -/** - * Channel physical - */ -public abstract class ChannelPhysical extends ChannelBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private NotifierBase notifier; - protected ChannelStatus status; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param configuration - * configuration used to set the channel - * @throws ChannelException - */ - protected ChannelPhysical(ChannelConfiguration configuration) throws ChannelException - { - super(configuration); - this.init(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Channel - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void subscribe(ChannelListener listener) - { - this.notifier.subscribe(listener); - } - - @Override - public void unsubscribe(ChannelListener listener) - { - this.notifier.unsubscribe(listener); - } - - @Override - public boolean hasSubscriber(ChannelListener subscriber) - { - return this.notifier.hasSubscriber(subscriber); - } - - @Override - public Collection getSubscribers() - { - return this.notifier.getSubscribers(); - } - - @Override - public String getName() - { - return this.configuration.getName(); - } - - @Override - public ChannelProtocol getProtocol() - { - return this.configuration.getProtocol(); - } - - @Override - public ChannelDirection getDirection() - { - return this.configuration.getDirection(); - } - - @Override - public ChannelStatus getStatus() - { - return this.status; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public ChannelConfiguration getConfiguration() - { - return this.configuration; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Set the channel status - * - * @param status - * channel status - */ - protected void setStatus(ChannelStatus status) - { - this.setStatus(status, true); - } - - /** - * Set the channel status. If 'notify' argument is false, no channel listener will be notified about a new status, - * otherwise, the will - * - * @param status - * channel status - * @param notify - * if 'true', the channel's listener will be notified, else it will not - */ - protected void setStatus(ChannelStatus status, boolean notify) - { - if (status != this.status) - { - this.status = status; - if (status == ChannelStatus.ERROR) - { - this.logger.error("Channel " + this.getName() + " new status : " + status.name()); - } - else - { - this.logger.info("Channel " + this.getName() + " new status : " + status.name()); - } - if (true == notify) - { - this.notifyOnStatusChanged(); - } - } - } - - protected void notifyOnDataRead(byte[] data) - { - if (data != null) - { - for (ChannelListener subscriber : this.notifier.getSubscribers()) - { - subscriber.onDataRead(this.getName(), data); - } - } - } - - protected void notifyOnDataWritten(byte[] data) - { - if (data != null) - { - for (ChannelListener subscriber : this.notifier.getSubscribers()) - { - subscriber.onDataWritten(this.getName(), data); - } - } - } - - protected void notifyOnStatusChanged() - { - for (ChannelListener subscriber : this.notifier.getSubscribers()) - { - subscriber.onStatusChanged(this.getName(), this.status); - } - } - - protected void notifyOnErrorDetected(int errorCode, String errorMessage) - { - for (ChannelListener subscriber : this.notifier.getSubscribers()) - { - subscriber.onErrorDetected(this.getName(), errorCode, errorMessage); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void init() - { - this.status = ChannelStatus.STOPPED; - this.notifier = new NotifierBase(); - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +import enedis.lab.util.task.NotifierBase; +import java.util.Collection; + +/** + * Abstract base class for physical communication channels. + *

    + * This class extends {@link ChannelBase} and provides the foundation for all + * physical communication channel implementations. It handles the common functionality + * for physical channels including status management, listener notification, and + * event propagation. + *

    + * Physical channels represent actual communication endpoints such as serial ports, + * network sockets, or other hardware interfaces. This class provides the + * infrastructure for managing channel state and notifying registered listeners + * about channel events. + *

    + * Key features provided by this class: + *

      + *
    • Status management with automatic listener notification
    • + *
    • Event notification system for data read/write operations
    • + *
    • Error detection and reporting
    • + *
    • Listener subscription management
    • + *
    + * + * @author Enedis Smarties team + * @see ChannelBase + * @see ChannelListener + * @see ChannelStatus + * @see NotifierBase + */ +public abstract class ChannelPhysical extends ChannelBase { + + /** Notifier for managing channel event listeners. */ + private NotifierBase notifier; + /** Current status of the physical channel. */ + protected ChannelStatus status; + + /** + * Constructs a new physical channel with the specified configuration. + *

    + * This constructor initializes the physical channel with the provided + * configuration and sets up the internal notification system. The channel + * starts in a STOPPED status and is ready for setup operations. + * + * @param configuration the configuration to use for setting up the channel + * @throws ChannelException if the configuration is invalid or the channel + * cannot be properly initialized + */ + protected ChannelPhysical(ChannelConfiguration configuration) throws ChannelException { + super(configuration); + this.init(); + } + + @Override + public void subscribe(ChannelListener listener) { + this.notifier.subscribe(listener); + } + + @Override + public void unsubscribe(ChannelListener listener) { + this.notifier.unsubscribe(listener); + } + + @Override + public boolean hasSubscriber(ChannelListener subscriber) { + return this.notifier.hasSubscriber(subscriber); + } + + @Override + public Collection getSubscribers() { + return this.notifier.getSubscribers(); + } + + @Override + public String getName() { + return this.configuration.getName(); + } + + @Override + public ChannelProtocol getProtocol() { + return this.configuration.getProtocol(); + } + + @Override + public ChannelDirection getDirection() { + return this.configuration.getDirection(); + } + + @Override + public ChannelStatus getStatus() { + return this.status; + } + + @Override + public ChannelConfiguration getConfiguration() { + return this.configuration; + } + + /** + * Sets the channel status and notifies all registered listeners. + *

    + * This method updates the channel status and automatically notifies + * all registered listeners about the status change. + * + * @param status the new status for the channel + */ + protected void setStatus(ChannelStatus status) { + this.setStatus(status, true); + } + + /** + * Sets the channel status with optional listener notification. + *

    + * This method updates the channel status and optionally notifies + * registered listeners about the status change. This allows for + * controlled status updates without triggering notifications. + * + * @param status the new status for the channel + * @param notify if true, listeners will be notified of the status change; + * if false, no notifications will be sent + */ + protected void setStatus(ChannelStatus status, boolean notify) { + if (status != this.status) { + this.status = status; + if (status == ChannelStatus.ERROR) { + this.logger.error("Channel " + this.getName() + " new status : " + status.name()); + } else { + this.logger.info("Channel " + this.getName() + " new status : " + status.name()); + } + if (true == notify) { + this.notifyOnStatusChanged(); + } + } + } + + /** + * Notifies all registered listeners about data that was read from the channel. + *

    + * This method is called internally when data is successfully read from + * the physical channel. It notifies all registered listeners with the + * channel name and the raw data that was read. + * + * @param data the raw bytes that were read from the channel + */ + protected void notifyOnDataRead(byte[] data) { + if (data != null) { + for (ChannelListener subscriber : this.notifier.getSubscribers()) { + subscriber.onDataRead(this.getName(), data); + } + } + } + + /** + * Notifies all registered listeners about data that was written to the channel. + *

    + * This method is called internally when data is successfully written to + * the physical channel. It notifies all registered listeners with the + * channel name and the raw data that was written. + * + * @param data the raw bytes that were written to the channel + */ + protected void notifyOnDataWritten(byte[] data) { + if (data != null) { + for (ChannelListener subscriber : this.notifier.getSubscribers()) { + subscriber.onDataWritten(this.getName(), data); + } + } + } + + /** + * Notifies all registered listeners about a status change. + *

    + * This method is called internally when the channel status changes. + * It notifies all registered listeners with the channel name and + * the new status. + */ + protected void notifyOnStatusChanged() { + for (ChannelListener subscriber : this.notifier.getSubscribers()) { + subscriber.onStatusChanged(this.getName(), this.status); + } + } + + /** + * Notifies all registered listeners about an error that was detected. + *

    + * This method is called internally when an error occurs during channel + * operations. It notifies all registered listeners with the channel name, + * error code, and error message. + * + * @param errorCode the numeric error code identifying the type of error + * @param errorMessage a descriptive message explaining the error + */ + protected void notifyOnErrorDetected(int errorCode, String errorMessage) { + for (ChannelListener subscriber : this.notifier.getSubscribers()) { + subscriber.onErrorDetected(this.getName(), errorCode, errorMessage); + } + } + + /** + * Initializes the physical channel with default values. + *

    + * This method sets up the initial state of the physical channel, + * including setting the status to STOPPED and initializing the + * notification system for managing listeners. + */ + private void init() { + this.status = ChannelStatus.STOPPED; + this.notifier = new NotifierBase(); + } +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java b/src/main/java/enedis/lab/io/channels/ChannelProtocol.java index 82206e5..f6cda97 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java +++ b/src/main/java/enedis/lab/io/channels/ChannelProtocol.java @@ -1,30 +1,65 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -/** - * Enumeration of channel protocol - * - */ -public enum ChannelProtocol -{ - /** File */ - FILE, - /** Serial port */ - SERIAL_PORT, - /** RTU modbus */ - MODBUS_RTU, - /** TCP modbus */ - MODBUS_TCP, - /** modbus stub */ - MODBUS_STUB, - /** TCP */ - TCP, - /** UDP */ - UDP, -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +/** + * Enumeration representing different types of communication channel protocols + * that can be used for data transmission in the TIC (Télé Information Client) system. + * + *

    This enum defines the various communication protocols supported by the system, + * ranging from file-based operations to network protocols and industrial communication + * standards like Modbus. + * + * @author Jehan BOUSCH + * @author Mathieu SABARTHES + * @since 1.0 + */ +public enum ChannelProtocol { + + /** + * File-based communication protocol. + * Used for reading from or writing to local files on the filesystem. + */ + FILE, + + /** + * Serial port communication protocol. + * Used for direct communication through serial interfaces (RS-232, RS-485, etc.). + */ + SERIAL_PORT, + + /** + * Modbus RTU (Remote Terminal Unit) protocol. + * A serial communication protocol commonly used in industrial automation systems. + */ + MODBUS_RTU, + + /** + * Modbus TCP protocol. + * A network-based implementation of the Modbus protocol over TCP/IP. + */ + MODBUS_TCP, + + /** + * Modbus stub protocol. + * A testing/mock implementation of Modbus for development and testing purposes. + */ + MODBUS_STUB, + + /** + * Transmission Control Protocol (TCP). + * A reliable, connection-oriented network protocol for data transmission. + */ + TCP, + + /** + * User Datagram Protocol (UDP). + * A connectionless network protocol for fast, lightweight data transmission. + */ + UDP, +} diff --git a/src/main/java/enedis/lab/io/channels/ChannelStatus.java b/src/main/java/enedis/lab/io/channels/ChannelStatus.java index acae4c5..8fb5bd2 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelStatus.java +++ b/src/main/java/enedis/lab/io/channels/ChannelStatus.java @@ -1,22 +1,41 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -/** - * Enumeration of channel status - * - */ -public enum ChannelStatus -{ - /** Stopped */ - STOPPED, - /** Started */ - STARTED, - /** Error */ - ERROR -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels; + +/** + * Enumeration representing the operational status of a communication channel + * in the TIC (Télé Information Client) system. + * + *

    This enum defines the possible states that a channel can be in during its lifecycle, + * from initialization through operation to error conditions. The status is used to monitor + * and manage the health and availability of communication channels. + * + * @author Jehan BOUSCH + * @author Mathieu SABARTHES + * @since 1.0 + */ +public enum ChannelStatus { + /** + * Channel is stopped and not actively processing data. + * This is the initial state when a channel is created but not yet started, + * or when it has been explicitly stopped. + */ + STOPPED, + + /** + * Channel is started and actively processing data. + * The channel is operational and ready to handle communication requests. + */ + STARTED, + + /** + * Channel has encountered an error and is not functioning properly. + * This state indicates that the channel needs attention or recovery actions. + */ + ERROR +} From 54a02265750289b9541a051893e81539c3dcc005 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Mon, 29 Sep 2025 15:50:47 +0200 Subject: [PATCH 15/59] chore: format with spotless --- .../java/enedis/lab/io/channels/Channel.java | 97 ++++-------- .../enedis/lab/io/channels/ChannelBase.java | 70 +++------ .../lab/io/channels/ChannelConfiguration.java | 125 +++++---------- .../lab/io/channels/ChannelDirection.java | 34 ++--- .../lab/io/channels/ChannelException.java | 143 ++++++++---------- .../lab/io/channels/ChannelListener.java | 86 +++++------ .../lab/io/channels/ChannelPhysical.java | 96 ++++++------ .../lab/io/channels/ChannelProtocol.java | 54 +++---- .../enedis/lab/io/channels/ChannelStatus.java | 31 ++-- 9 files changed, 297 insertions(+), 439 deletions(-) diff --git a/src/main/java/enedis/lab/io/channels/Channel.java b/src/main/java/enedis/lab/io/channels/Channel.java index 1a8aa15..24d52ad 100644 --- a/src/main/java/enedis/lab/io/channels/Channel.java +++ b/src/main/java/enedis/lab/io/channels/Channel.java @@ -13,23 +13,15 @@ /** * Interface defining the contract for communication channels. * - *

    - * A channel represents a communication endpoint that can be used to send and - * receive data. This - * interface extends both {@link Task} and {@link Notifier} to provide task - * execution capabilities + *

    A channel represents a communication endpoint that can be used to send and receive data. This + * interface extends both {@link Task} and {@link Notifier} to provide task execution capabilities * and event notification to registered {@link ChannelListener} instances. * - *

    - * Channels support different protocols (e.g., serial port, TCP, UDP) and - * directions - * (receive-only, transmit-only, or bidirectional). Each channel must be - * configured before use and + *

    Channels support different protocols (e.g., serial port, TCP, UDP) and directions + * (receive-only, transmit-only, or bidirectional). Each channel must be configured before use and * can be monitored for status changes. * - *

    - * Implementations of this interface should handle the specific communication - * protocol details + *

    Implementations of this interface should handle the specific communication protocol details * while providing a unified interface for data exchange. * * @author Enedis Smarties team @@ -46,74 +38,52 @@ public interface Channel extends Task, Notifier { /** * Initializes and configures the channel with the provided configuration. * - *

    - * This method must be called before any read or write operations can be - * performed. The - * configuration contains all necessary parameters for the specific channel - * type, such as port + *

    This method must be called before any read or write operations can be performed. The + * configuration contains all necessary parameters for the specific channel type, such as port * settings for serial channels or network settings for TCP channels. * - *

    - * After successful setup, the channel should be ready for communication - * operations. + *

    After successful setup, the channel should be ready for communication operations. * - * @param configuration the channel configuration containing all necessary - * parameters - * @throws ChannelException if the configuration is invalid, the channel cannot - * be initialized, or - * there are resource allocation issues + * @param configuration the channel configuration containing all necessary parameters + * @throws ChannelException if the configuration is invalid, the channel cannot be initialized, or + * there are resource allocation issues */ public void setup(ChannelConfiguration configuration) throws ChannelException; /** * Reads data from the channel. * - *

    - * This method performs a synchronous read operation, blocking until data is - * available or a - * timeout occurs (depending on the channel implementation). The returned byte - * array contains the + *

    This method performs a synchronous read operation, blocking until data is available or a + * timeout occurs (depending on the channel implementation). The returned byte array contains the * raw data received from the channel. * - *

    - * The channel must be properly configured and opened before calling this - * method. + *

    The channel must be properly configured and opened before calling this method. * - * @return a byte array containing the data read from the channel, or an empty - * array if no data is - * available - * @throws ChannelException if the read operation fails, the channel is not - * properly configured, - * or a communication error occurs + * @return a byte array containing the data read from the channel, or an empty array if no data is + * available + * @throws ChannelException if the read operation fails, the channel is not properly configured, + * or a communication error occurs */ public byte[] read() throws ChannelException; /** * Writes data to the channel. * - *

    - * This method performs a synchronous write operation, sending the provided data - * through the - * channel. The operation blocks until the data is successfully transmitted or - * an error occurs. + *

    This method performs a synchronous write operation, sending the provided data through the + * channel. The operation blocks until the data is successfully transmitted or an error occurs. * - *

    - * The channel must be properly configured and opened before calling this - * method. + *

    The channel must be properly configured and opened before calling this method. * * @param data the byte array containing the data to be written to the channel - * @throws ChannelException if the write operation fails, the channel is not - * properly configured, - * or a communication error occurs + * @throws ChannelException if the write operation fails, the channel is not properly configured, + * or a communication error occurs */ public void write(byte[] data) throws ChannelException; /** * Retrieves the name of this channel. * - *

    - * The channel name is typically used for identification and logging purposes. - * It is usually + *

    The channel name is typically used for identification and logging purposes. It is usually * set during channel creation or configuration. * * @return the channel name, or null if not set @@ -123,9 +93,7 @@ public interface Channel extends Task, Notifier { /** * Retrieves the communication protocol used by this channel. * - *

    - * The protocol determines the underlying communication mechanism, such as - * serial port, TCP, + *

    The protocol determines the underlying communication mechanism, such as serial port, TCP, * UDP, or other supported protocols. * * @return the channel protocol, or null if not set @@ -135,9 +103,7 @@ public interface Channel extends Task, Notifier { /** * Retrieves the communication direction of this channel. * - *

    - * The direction indicates whether the channel can receive data (RX), transmit - * data (TX), or + *

    The direction indicates whether the channel can receive data (RX), transmit data (TX), or * both (RXTX). This affects which operations are available. * * @return the channel direction, or null if not set @@ -147,9 +113,7 @@ public interface Channel extends Task, Notifier { /** * Retrieves the current status of this channel. * - *

    - * The status indicates the operational state of the channel, such as whether it - * is open, + *

    The status indicates the operational state of the channel, such as whether it is open, * closed, error state, or in the process of opening/closing. * * @return the current channel status @@ -159,13 +123,10 @@ public interface Channel extends Task, Notifier { /** * Retrieves the configuration used by this channel. * - *

    - * The configuration contains all the parameters that were used to set up the - * channel, + *

    The configuration contains all the parameters that were used to set up the channel, * including protocol-specific settings and operational parameters. * - * @return the channel configuration, or null if the channel has not been - * configured + * @return the channel configuration, or null if the channel has not been configured */ public ChannelConfiguration getConfiguration(); } diff --git a/src/main/java/enedis/lab/io/channels/ChannelBase.java b/src/main/java/enedis/lab/io/channels/ChannelBase.java index 7f9476c..6244631 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelBase.java +++ b/src/main/java/enedis/lab/io/channels/ChannelBase.java @@ -13,26 +13,17 @@ import org.apache.logging.log4j.Logger; /** - * Abstract base class providing common functionality for channel - * implementations. + * Abstract base class providing common functionality for channel implementations. * - *

    - * This class serves as a foundation for all channel implementations, providing - * common setup - * logic, configuration management, and logging capabilities. It extends - * {@link TaskPeriodic} to + *

    This class serves as a foundation for all channel implementations, providing common setup + * logic, configuration management, and logging capabilities. It extends {@link TaskPeriodic} to * provide periodic task execution and implements the {@link Channel} interface. * - *

    - * The class handles configuration validation, backup and restoration in case of - * setup failures, - * and provides a template method pattern for channel-specific setup operations - * through the abstract + *

    The class handles configuration validation, backup and restoration in case of setup failures, + * and provides a template method pattern for channel-specific setup operations through the abstract * {@link #setup()} method. * - *

    - * Subclasses should implement the abstract {@link #setup()} method to provide - * channel-specific + *

    Subclasses should implement the abstract {@link #setup()} method to provide channel-specific * initialization logic. * * @author Enedis Smarties team @@ -51,17 +42,13 @@ public abstract class ChannelBase extends TaskPeriodic implements Channel { /** * Constructs a new channel with the specified configuration. * - *

    - * This constructor initializes the logger for the specific channel - * implementation and - * immediately sets up the channel using the provided configuration. If the - * setup fails, a {@link + *

    This constructor initializes the logger for the specific channel implementation and + * immediately sets up the channel using the provided configuration. If the setup fails, a {@link * ChannelException} is thrown. * * @param configuration the configuration to use for setting up the channel - * @throws ChannelException if the configuration is invalid or the channel - * cannot be properly - * initialized + * @throws ChannelException if the configuration is invalid or the channel cannot be properly + * initialized */ protected ChannelBase(ChannelConfiguration configuration) throws ChannelException { this.logger = LogManager.getLogger(this.getClass()); @@ -71,25 +58,21 @@ protected ChannelBase(ChannelConfiguration configuration) throws ChannelExceptio /** * Sets up the channel with the provided configuration. * - *

    - * This method implements a robust setup process that includes: + *

    This method implements a robust setup process that includes: * *

      - *
    • Configuration validation - *
    • Configuration backup (if reconfiguring) - *
    • Channel-specific setup via {@link #setup()} - *
    • Automatic rollback on setup failure + *
    • Configuration validation + *
    • Configuration backup (if reconfiguring) + *
    • Channel-specific setup via {@link #setup()} + *
    • Automatic rollback on setup failure *
    * - *

    - * If the setup fails, the previous configuration is automatically restored to - * maintain the + *

    If the setup fails, the previous configuration is automatically restored to maintain the * channel in a consistent state. * * @param configuration the configuration to apply to the channel - * @throws ChannelException if the configuration is null, invalid, or if the - * channel-specific - * setup fails + * @throws ChannelException if the configuration is null, invalid, or if the channel-specific + * setup fails */ @Override public void setup(ChannelConfiguration configuration) throws ChannelException { @@ -127,22 +110,15 @@ public void setup(ChannelConfiguration configuration) throws ChannelException { /** * Performs channel-specific setup operations. * - *

    - * This method is called by the public {@link #setup(ChannelConfiguration)} - * method after the - * configuration has been validated and applied. Subclasses should override this - * method to - * implement channel-specific initialization logic, such as opening - * communication ports, + *

    This method is called by the public {@link #setup(ChannelConfiguration)} method after the + * configuration has been validated and applied. Subclasses should override this method to + * implement channel-specific initialization logic, such as opening communication ports, * establishing network connections, or initializing hardware. * - *

    - * If this method throws an exception, the configuration will be automatically - * rolled back to + *

    If this method throws an exception, the configuration will be automatically rolled back to * its previous state. * * @throws ChannelException if the channel-specific setup fails */ - protected void setup() throws ChannelException { - } + protected void setup() throws ChannelException {} } diff --git a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java b/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java index 610e1c6..6f916be 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java +++ b/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java @@ -21,22 +21,15 @@ /** * Configuration class for communication channels. * - *

    - * This class extends {@link ConfigurationBase} and provides the base - * configuration parameters - * required for all types of communication channels. It defines the essential - * properties that every + *

    This class extends {@link ConfigurationBase} and provides the base configuration parameters + * required for all types of communication channels. It defines the essential properties that every * channel must have: name, protocol, direction, and alias. * - *

    - * The configuration supports validation through key descriptors and provides - * methods for setting - * and retrieving channel parameters. It can be instantiated from various - * sources including maps, + *

    The configuration supports validation through key descriptors and provides methods for setting + * and retrieving channel parameters. It can be instantiated from various sources including maps, * data dictionaries, and direct parameter values. * - *

    - * This class serves as the foundation for more specific channel configurations. + *

    This class serves as the foundation for more specific channel configurations. * * @author Enedis Smarties team * @see ConfigurationBase @@ -75,9 +68,7 @@ public class ChannelConfiguration extends ConfigurationBase { /** * Default constructor for channel configuration. * - *

    - * Initializes the configuration with default values and sets up the key - * descriptors for + *

    Initializes the configuration with default values and sets up the key descriptors for * parameter validation. */ protected ChannelConfiguration() { @@ -88,15 +79,12 @@ protected ChannelConfiguration() { /** * Constructs a new channel configuration from a map of key-value pairs. * - *

    - * This constructor initializes the configuration with values from the provided - * map, performing + *

    This constructor initializes the configuration with values from the provided map, performing * validation and setting default values for missing parameters. * * @param map the map containing configuration parameters - * @throws DataDictionaryException if the map contains invalid values or - * required parameters are - * missing + * @throws DataDictionaryException if the map contains invalid values or required parameters are + * missing */ public ChannelConfiguration(Map map) throws DataDictionaryException { this(); @@ -104,18 +92,14 @@ public ChannelConfiguration(Map map) throws DataDictionaryExcept } /** - * Constructs a new channel configuration by copying from another data - * dictionary. + * Constructs a new channel configuration by copying from another data dictionary. * - *

    - * This constructor creates a new configuration instance by copying all - * parameters from the + *

    This constructor creates a new configuration instance by copying all parameters from the * provided data dictionary, ensuring consistency between configurations. * * @param other the data dictionary to copy configuration from - * @throws DataDictionaryException if the source data dictionary contains - * invalid values or is - * incompatible with channel configuration + * @throws DataDictionaryException if the source data dictionary contains invalid values or is + * incompatible with channel configuration */ public ChannelConfiguration(DataDictionary other) throws DataDictionaryException { this(); @@ -123,13 +107,10 @@ public ChannelConfiguration(DataDictionary other) throws DataDictionaryException } /** - * Constructs a new channel configuration with a name and file reference, - * initializing all + * Constructs a new channel configuration with a name and file reference, initializing all * parameters to their default values. * - *

    - * This constructor is typically used when creating a new configuration that - * will be populated + *

    This constructor is typically used when creating a new configuration that will be populated * later with specific channel parameters. * * @param name the configuration name @@ -141,21 +122,17 @@ public ChannelConfiguration(String name, File file) { } /** - * Constructs a new channel configuration with all parameters set to specific - * values. + * Constructs a new channel configuration with all parameters set to specific values. * - *

    - * This constructor provides a convenient way to create a fully configured - * channel with all + *

    This constructor provides a convenient way to create a fully configured channel with all * necessary parameters specified at construction time. * - * @param name the channel name - * @param protocol the communication protocol to use + * @param name the channel name + * @param protocol the communication protocol to use * @param direction the communication direction (RX, TX, or RXTX) - * @param alias the channel alias (optional) - * @throws DataDictionaryException if any of the provided values are invalid or - * incompatible with - * channel configuration requirements + * @param alias the channel alias (optional) + * @throws DataDictionaryException if any of the provided values are invalid or incompatible with + * channel configuration requirements */ public ChannelConfiguration( String name, ChannelProtocol protocol, ChannelDirection direction, String alias) @@ -173,15 +150,11 @@ public ChannelConfiguration( /** * Updates optional parameters with default values if not already set. * - *

    - * This method is called during configuration validation to ensure that all - * required parameters - * are properly set. For the base channel configuration, this method delegates - * to the parent class + *

    This method is called during configuration validation to ensure that all required parameters + * are properly set. For the base channel configuration, this method delegates to the parent class * implementation. * - * @throws DataDictionaryException if there are validation errors with the - * configuration + * @throws DataDictionaryException if there are validation errors with the configuration */ @Override protected void updateOptionalParameters() throws DataDictionaryException { @@ -191,9 +164,7 @@ protected void updateOptionalParameters() throws DataDictionaryException { /** * Retrieves the channel name. * - *

    - * The channel name is used for identification and logging purposes. It is - * typically set during + *

    The channel name is used for identification and logging purposes. It is typically set during * channel creation or configuration. * * @return the channel name, or null if not set @@ -205,9 +176,7 @@ public String getName() { /** * Retrieves the communication protocol used by this channel. * - *

    - * The protocol determines the underlying communication mechanism, such as - * serial port, TCP, + *

    The protocol determines the underlying communication mechanism, such as serial port, TCP, * UDP, or other supported protocols. * * @return the channel protocol, or null if not set @@ -219,9 +188,7 @@ public ChannelProtocol getProtocol() { /** * Retrieves the communication direction of this channel. * - *

    - * The direction indicates whether the channel can receive data (RX), transmit - * data (TX), or + *

    The direction indicates whether the channel can receive data (RX), transmit data (TX), or * both (RXTX). This affects which operations are available. * * @return the channel direction, or null if not set @@ -233,9 +200,7 @@ public ChannelDirection getDirection() { /** * Retrieves the channel alias. * - *

    - * The alias provides an alternative identifier for the channel, typically used - * for + *

    The alias provides an alternative identifier for the channel, typically used for * user-friendly display or alternative referencing. * * @return the channel alias, or null if not set @@ -247,9 +212,7 @@ public String getAlias() { /** * Sets the channel name. * - *

    - * The channel name is used for identification and logging purposes. It must be - * a non-empty + *

    The channel name is used for identification and logging purposes. It must be a non-empty * string and is required for a valid configuration. * * @param name the channel name to set @@ -262,9 +225,7 @@ public void setName(String name) throws DataDictionaryException { /** * Sets the communication protocol for this channel. * - *

    - * The protocol determines the underlying communication mechanism. Must be one - * of the supported + *

    The protocol determines the underlying communication mechanism. Must be one of the supported * {@link ChannelProtocol} values. * * @param protocol the communication protocol to use @@ -277,9 +238,7 @@ public void setProtocol(ChannelProtocol protocol) throws DataDictionaryException /** * Sets the communication direction for this channel. * - *

    - * The direction determines whether the channel can receive data (RX), transmit - * data (TX), or + *

    The direction determines whether the channel can receive data (RX), transmit data (TX), or * both (RXTX). Must be one of the supported {@link ChannelDirection} values. * * @param direction the communication direction to use @@ -292,11 +251,8 @@ public void setDirection(ChannelDirection direction) throws DataDictionaryExcept /** * Sets the channel alias. * - *

    - * The alias provides an alternative identifier for the channel, typically used - * for - * user-friendly display or alternative referencing. This parameter is optional - * and can be null. + *

    The alias provides an alternative identifier for the channel, typically used for + * user-friendly display or alternative referencing. This parameter is optional and can be null. * * @param alias the channel alias to set (can be null) * @throws DataDictionaryException if the alias format is invalid @@ -364,23 +320,22 @@ protected void setAlias(Object alias) throws DataDictionaryException { /** * Initializes and configures all key descriptors for parameter validation. * - *

    - * This method sets up the validation rules for all channel configuration - * parameters, including + *

    This method sets up the validation rules for all channel configuration parameters, including * required/optional status and type validation. * - * @throws RuntimeException if there is an error during key descriptor - * initialization + * @throws RuntimeException if there is an error during key descriptor initialization */ private void loadKeyDescriptors() { try { this.kName = new KeyDescriptorString(KEY_NAME, true, false); this.keys.add(this.kName); - this.kProtocol = new KeyDescriptorEnum(KEY_PROTOCOL, true, ChannelProtocol.class); + this.kProtocol = + new KeyDescriptorEnum(KEY_PROTOCOL, true, ChannelProtocol.class); this.keys.add(this.kProtocol); - this.kDirection = new KeyDescriptorEnum(KEY_DIRECTION, true, ChannelDirection.class); + this.kDirection = + new KeyDescriptorEnum(KEY_DIRECTION, true, ChannelDirection.class); this.keys.add(this.kDirection); this.kAlias = new KeyDescriptorString(KEY_ALIAS, false, false); diff --git a/src/main/java/enedis/lab/io/channels/ChannelDirection.java b/src/main/java/enedis/lab/io/channels/ChannelDirection.java index 47add1a..ed4c3f1 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelDirection.java +++ b/src/main/java/enedis/lab/io/channels/ChannelDirection.java @@ -9,13 +9,12 @@ /** * Enumeration representing the communication direction for channels. - *

    - * This enum defines the possible communication directions that a channel - * can support. The direction determines which operations are available - * on the channel (read, write, or both). - *

    - * This enum is used in {@link ChannelConfiguration} to specify - * the communication direction for channel instances. + * + *

    This enum defines the possible communication directions that a channel can support. The + * direction determines which operations are available on the channel (read, write, or both). + * + *

    This enum is used in {@link ChannelConfiguration} to specify the communication direction for + * channel instances. * * @author Enedis Smarties team * @see ChannelConfiguration @@ -25,27 +24,26 @@ public enum ChannelDirection { /** * Receive-only direction. - *

    - * Channels with this direction can only receive data from the communication - * endpoint. Read operations are supported, but write operations will fail. + * + *

    Channels with this direction can only receive data from the communication endpoint. Read + * operations are supported, but write operations will fail. */ RX, /** * Transmit-only direction. - *

    - * Channels with this direction can only send data to the communication - * endpoint. Write operations are supported, but read operations will fail. + * + *

    Channels with this direction can only send data to the communication endpoint. Write + * operations are supported, but read operations will fail. */ TX, /** * Bidirectional communication. - *

    - * Channels with this direction can both receive and send data. - * Both read and write operations are supported, enabling full - * bidirectional communication. This is the most common direction - * for interactive communication channels. + * + *

    Channels with this direction can both receive and send data. Both read and write operations + * are supported, enabling full bidirectional communication. This is the most common direction for + * interactive communication channels. */ RXTX } diff --git a/src/main/java/enedis/lab/io/channels/ChannelException.java b/src/main/java/enedis/lab/io/channels/ChannelException.java index 6bc1466..bb2d100 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelException.java +++ b/src/main/java/enedis/lab/io/channels/ChannelException.java @@ -11,24 +11,22 @@ /** * Exception class for channel-related errors. - *

    - * This exception is thrown when errors occur during channel operations such as - * setup, configuration, read/write operations, or channel management. It - * extends - * {@link ExceptionBase} to provide structured error handling with error codes - * and detailed error messages. - *

    - * The class provides static factory methods for creating specific types of - * channel exceptions, each with appropriate error codes and descriptive - * messages. - *

    - * Common scenarios that trigger this exception include: + * + *

    This exception is thrown when errors occur during channel operations such as setup, + * configuration, read/write operations, or channel management. It extends {@link ExceptionBase} to + * provide structured error handling with error codes and detailed error messages. + * + *

    The class provides static factory methods for creating specific types of channel exceptions, + * each with appropriate error codes and descriptive messages. + * + *

    Common scenarios that trigger this exception include: + * *

      - *
    • Invalid channel configuration
    • - *
    • Channel not ready for operations
    • - *
    • Conflicting channel types
    • - *
    • Operation denied due to channel state
    • - *
    • Internal channel errors
    • + *
    • Invalid channel configuration + *
    • Channel not ready for operations + *
    • Conflicting channel types + *
    • Operation denied due to channel state + *
    • Internal channel errors *
    * * @author Enedis Smarties team @@ -58,9 +56,7 @@ public class ChannelException extends ExceptionBase { /** Error code for invalid configuration parameters or values. */ public static final int ERRCODE_INVALID_CONFIGURATION = -10; - /** - * Error code for operations that are not allowed in the current channel state. - */ + /** Error code for operations that are not allowed in the current channel state. */ public static final int ERRCODE_OPERATION_DENIED = -11; /** Error code for operations attempted on non-existent channels. */ @@ -71,13 +67,12 @@ public class ChannelException extends ExceptionBase { /** * Creates and throws a ChannelException for unhandled channel types. - *

    - * This method is used when an attempt is made to create or use a channel - * type that is not supported by the system. + * + *

    This method is used when an attempt is made to create or use a channel type that is not + * supported by the system. * * @param channelType the unsupported channel type that was requested - * @throws ChannelException always thrown with error code - * {@link #ERRCODE_INVALID_CHANNEL} + * @throws ChannelException always thrown with error code {@link #ERRCODE_INVALID_CHANNEL} */ public static void raiseUnhandledChannelType(String channelType) throws ChannelException { throw new ChannelException(ERRCODE_INVALID_CHANNEL, channelType + " channel is not handled"); @@ -85,14 +80,13 @@ public static void raiseUnhandledChannelType(String channelType) throws ChannelE /** * Creates and throws a ChannelException for conflicting channel types. - *

    - * This method is used when a channel type conflicts with an expected type, - * typically during channel configuration or type validation. * - * @param channelType the actual channel type that was provided + *

    This method is used when a channel type conflicts with an expected type, typically during + * channel configuration or type validation. + * + * @param channelType the actual channel type that was provided * @param expectedChannelType the expected channel type - * @throws ChannelException always thrown with error code - * {@link #ERRCODE_INVALID_CHANNEL} + * @throws ChannelException always thrown with error code {@link #ERRCODE_INVALID_CHANNEL} */ public static void raiseConflictingChannelType(String channelType, String expectedChannelType) throws ChannelException { @@ -103,13 +97,12 @@ public static void raiseConflictingChannelType(String channelType, String expect /** * Creates and throws a ChannelException for internal errors. - *

    - * This method is used when an internal error occurs during channel - * processing that cannot be attributed to user input or configuration. + * + *

    This method is used when an internal error occurs during channel processing that cannot be + * attributed to user input or configuration. * * @param info additional information about the internal error - * @throws ChannelException always thrown with error code - * {@link #ERRCODE_INTERNAL_ERROR} + * @throws ChannelException always thrown with error code {@link #ERRCODE_INTERNAL_ERROR} */ public static void raiseInternalError(String info) throws ChannelException { throw new ChannelException(ERRCODE_INTERNAL_ERROR, info); @@ -117,13 +110,12 @@ public static void raiseInternalError(String info) throws ChannelException { /** * Creates and throws a ChannelException for channels that are not ready. - *

    - * This method is used when an operation is attempted on a channel that - * is not in a ready state (e.g., not properly configured or initialized). + * + *

    This method is used when an operation is attempted on a channel that is not in a ready state + * (e.g., not properly configured or initialized). * * @param info additional information about why the channel is not ready - * @throws ChannelException always thrown with error code - * {@link #ERRCODE_CHANNEL_NOT_READY} + * @throws ChannelException always thrown with error code {@link #ERRCODE_CHANNEL_NOT_READY} */ public static void raiseChannelNotReady(String info) throws ChannelException { throw new ChannelException(ERRCODE_CHANNEL_NOT_READY, info); @@ -131,16 +123,14 @@ public static void raiseChannelNotReady(String info) throws ChannelException { /** * Creates and throws a ChannelException for invalid configuration types. - *

    - * This method is used when a channel configuration is of the wrong type - * for the expected channel implementation. - * - * @param configuration the invalid configuration that was - * provided - * @param expected_configuration_name the name of the expected configuration - * type - * @throws ChannelException always thrown with error code - * {@link #ERRCODE_INVALID_CONFIGURATION_TYPE} + * + *

    This method is used when a channel configuration is of the wrong type for the expected + * channel implementation. + * + * @param configuration the invalid configuration that was provided + * @param expected_configuration_name the name of the expected configuration type + * @throws ChannelException always thrown with error code {@link + * #ERRCODE_INVALID_CONFIGURATION_TYPE} */ public static void raiseInvalidConfigurationType( ChannelConfiguration configuration, String expected_configuration_name) @@ -156,13 +146,12 @@ public static void raiseInvalidConfigurationType( /** * Creates and throws a ChannelException for invalid configuration parameters. - *

    - * This method is used when channel configuration parameters are invalid, - * missing, or incompatible. + * + *

    This method is used when channel configuration parameters are invalid, missing, or + * incompatible. * * @param info additional information about the configuration error - * @throws ChannelException always thrown with error code - * {@link #ERRCODE_INVALID_CONFIGURATION} + * @throws ChannelException always thrown with error code {@link #ERRCODE_INVALID_CONFIGURATION} */ public static void raiseInvalidConfiguration(String info) throws ChannelException { throw new ChannelException( @@ -171,13 +160,12 @@ public static void raiseInvalidConfiguration(String info) throws ChannelExceptio /** * Creates and throws a ChannelException for denied operations. - *

    - * This method is used when an operation is attempted that is not allowed - * in the current channel state or configuration. + * + *

    This method is used when an operation is attempted that is not allowed in the current + * channel state or configuration. * * @param operation the operation that was denied - * @throws ChannelException always thrown with error code - * {@link #ERRCODE_OPERATION_DENIED} + * @throws ChannelException always thrown with error code {@link #ERRCODE_OPERATION_DENIED} */ public static void raiseOperationDenied(String operation) throws ChannelException { throw new ChannelException( @@ -186,13 +174,11 @@ public static void raiseOperationDenied(String operation) throws ChannelExceptio /** * Creates and throws a ChannelException for unexpected errors. - *

    - * This method is used when an unexpected or unhandled error occurs - * during channel operations. + * + *

    This method is used when an unexpected or unhandled error occurs during channel operations. * * @param info additional information about the unexpected error - * @throws ChannelException always thrown with error code - * {@link #ERRCODE_UNEXPECTED} + * @throws ChannelException always thrown with error code {@link #ERRCODE_UNEXPECTED} */ public static void raiseUnexpectedError(String info) throws ChannelException { throw new ChannelException(ERRCODE_UNEXPECTED, "Unexpected error occurs : " + info); @@ -200,13 +186,12 @@ public static void raiseUnexpectedError(String info) throws ChannelException { /** * Creates and throws a ChannelException for duplicate channel names. - *

    - * This method is used when attempting to create a channel with a name - * that already exists in the system. + * + *

    This method is used when attempting to create a channel with a name that already exists in + * the system. * * @param channelName the name of the channel that already exists - * @throws ChannelException always thrown with error code - * {@link #ERRCODE_ALREADY_EXISTS} + * @throws ChannelException always thrown with error code {@link #ERRCODE_ALREADY_EXISTS} */ public static void raiseAlreadyExists(String channelName) throws ChannelException { throw new ChannelException( @@ -215,13 +200,12 @@ public static void raiseAlreadyExists(String channelName) throws ChannelExceptio /** * Creates and throws a ChannelException for non-existent channels. - *

    - * This method is used when attempting to perform operations on a channel - * that does not exist in the system. + * + *

    This method is used when attempting to perform operations on a channel that does not exist + * in the system. * * @param channelName the name of the channel that does not exist - * @throws ChannelException always thrown with error code - * {@link #ERRCODE_CHANNEL_DOESNT_EXIST} + * @throws ChannelException always thrown with error code {@link #ERRCODE_CHANNEL_DOESNT_EXIST} */ public static void raiseDoesntExist(String channelName) throws ChannelException { throw new ChannelException( @@ -230,10 +214,9 @@ public static void raiseDoesntExist(String channelName) throws ChannelException /** * Constructs a new ChannelException with the specified error code and message. - *

    - * This constructor creates a channel exception with a specific error code - * and descriptive message. The error code should be one of the predefined - * constants in this class. + * + *

    This constructor creates a channel exception with a specific error code and descriptive + * message. The error code should be one of the predefined constants in this class. * * @param code the error code for this exception * @param info the detailed error message diff --git a/src/main/java/enedis/lab/io/channels/ChannelListener.java b/src/main/java/enedis/lab/io/channels/ChannelListener.java index adee055..d4c45b1 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelListener.java +++ b/src/main/java/enedis/lab/io/channels/ChannelListener.java @@ -12,22 +12,22 @@ /** * Interface for receiving channel events and notifications. - *

    - * This interface extends {@link Subscriber} and provides methods for handling - * various channel events such as data read/write operations, status changes, - * and error conditions. Implementations of this interface can be registered - * with channels to receive real-time notifications about channel activities. - *

    - * The listener pattern allows for decoupled event handling, enabling multiple - * listeners to be registered with a single channel and receive notifications - * about all channel activities. - *

    - * Common use cases include: + * + *

    This interface extends {@link Subscriber} and provides methods for handling various channel + * events such as data read/write operations, status changes, and error conditions. Implementations + * of this interface can be registered with channels to receive real-time notifications about + * channel activities. + * + *

    The listener pattern allows for decoupled event handling, enabling multiple listeners to be + * registered with a single channel and receive notifications about all channel activities. + * + *

    Common use cases include: + * *

      - *
    • Logging channel activities
    • - *
    • Monitoring channel status
    • - *
    • Processing received data
    • - *
    • Error handling and reporting
    • + *
    • Logging channel activities + *
    • Monitoring channel status + *
    • Processing received data + *
    • Error handling and reporting *
    * * @author Enedis Smarties team @@ -39,66 +39,62 @@ public interface ChannelListener extends Subscriber { /** * Called when data is successfully read from a channel. - *

    - * This method is invoked whenever a channel performs a successful read - * operation. - * The received data is provided as a byte array containing the raw data - * that was read from the channel. + * + *

    This method is invoked whenever a channel performs a successful read operation. The received + * data is provided as a byte array containing the raw data that was read from the channel. * * @param channelName the name of the channel that read the data - * @param data the raw bytes that were read from the channel + * @param data the raw bytes that were read from the channel */ public void onDataRead(String channelName, byte[] data); /** * Called when data is successfully written to a channel. - *

    - * This method is invoked whenever a channel performs a successful write - * operation. - * The transmitted data is provided as a byte array containing the raw data - * that was written to the channel. + * + *

    This method is invoked whenever a channel performs a successful write operation. The + * transmitted data is provided as a byte array containing the raw data that was written to the + * channel. * * @param channelName the name of the channel that wrote the data - * @param data the raw bytes that were written to the channel + * @param data the raw bytes that were written to the channel */ public void onDataWritten(String channelName, byte[] data); /** * Called when the status of a channel changes. - *

    - * This method is invoked whenever a channel's operational status changes, - * such as when it goes from closed to open, or from ready to error state. - * This allows listeners to monitor channel state transitions. + * + *

    This method is invoked whenever a channel's operational status changes, such as when it goes + * from closed to open, or from ready to error state. This allows listeners to monitor channel + * state transitions. * * @param channelName the name of the channel whose status changed - * @param status the new status of the channel + * @param status the new status of the channel */ public void onStatusChanged(String channelName, ChannelStatus status); /** * Called when an error is detected on a channel. - *

    - * This method is invoked whenever an error occurs during channel operations. - * It provides basic error information including the error code and message. * - * @param channelName the name of the channel where the error occurred - * @param errorCode the numeric error code identifying the type of error + *

    This method is invoked whenever an error occurs during channel operations. It provides basic + * error information including the error code and message. + * + * @param channelName the name of the channel where the error occurred + * @param errorCode the numeric error code identifying the type of error * @param errorMessage a descriptive message explaining the error */ public void onErrorDetected(String channelName, int errorCode, String errorMessage); /** * Called when an error is detected on a channel with additional context data. - *

    - * This method is invoked whenever an error occurs during channel operations - * and additional context data is available. It provides comprehensive error - * information including the error code, message, and associated data - * dictionary. * - * @param channelName the name of the channel where the error occurred - * @param errorCode the numeric error code identifying the type of error + *

    This method is invoked whenever an error occurs during channel operations and additional + * context data is available. It provides comprehensive error information including the error + * code, message, and associated data dictionary. + * + * @param channelName the name of the channel where the error occurred + * @param errorCode the numeric error code identifying the type of error * @param errorMessage a descriptive message explaining the error - * @param data additional context data associated with the error + * @param data additional context data associated with the error */ public void onErrorDetected( String channelName, int errorCode, String errorMessage, DataDictionary data); diff --git a/src/main/java/enedis/lab/io/channels/ChannelPhysical.java b/src/main/java/enedis/lab/io/channels/ChannelPhysical.java index 0edd50c..136a01e 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelPhysical.java +++ b/src/main/java/enedis/lab/io/channels/ChannelPhysical.java @@ -12,23 +12,22 @@ /** * Abstract base class for physical communication channels. - *

    - * This class extends {@link ChannelBase} and provides the foundation for all - * physical communication channel implementations. It handles the common functionality - * for physical channels including status management, listener notification, and - * event propagation. - *

    - * Physical channels represent actual communication endpoints such as serial ports, - * network sockets, or other hardware interfaces. This class provides the - * infrastructure for managing channel state and notifying registered listeners - * about channel events. - *

    - * Key features provided by this class: + * + *

    This class extends {@link ChannelBase} and provides the foundation for all physical + * communication channel implementations. It handles the common functionality for physical channels + * including status management, listener notification, and event propagation. + * + *

    Physical channels represent actual communication endpoints such as serial ports, network + * sockets, or other hardware interfaces. This class provides the infrastructure for managing + * channel state and notifying registered listeners about channel events. + * + *

    Key features provided by this class: + * *

      - *
    • Status management with automatic listener notification
    • - *
    • Event notification system for data read/write operations
    • - *
    • Error detection and reporting
    • - *
    • Listener subscription management
    • + *
    • Status management with automatic listener notification + *
    • Event notification system for data read/write operations + *
    • Error detection and reporting + *
    • Listener subscription management *
    * * @author Enedis Smarties team @@ -41,19 +40,20 @@ public abstract class ChannelPhysical extends ChannelBase { /** Notifier for managing channel event listeners. */ private NotifierBase notifier; + /** Current status of the physical channel. */ protected ChannelStatus status; /** * Constructs a new physical channel with the specified configuration. - *

    - * This constructor initializes the physical channel with the provided - * configuration and sets up the internal notification system. The channel - * starts in a STOPPED status and is ready for setup operations. + * + *

    This constructor initializes the physical channel with the provided configuration and sets + * up the internal notification system. The channel starts in a STOPPED status and is ready for + * setup operations. * * @param configuration the configuration to use for setting up the channel - * @throws ChannelException if the configuration is invalid or the channel - * cannot be properly initialized + * @throws ChannelException if the configuration is invalid or the channel cannot be properly + * initialized */ protected ChannelPhysical(ChannelConfiguration configuration) throws ChannelException { super(configuration); @@ -107,9 +107,9 @@ public ChannelConfiguration getConfiguration() { /** * Sets the channel status and notifies all registered listeners. - *

    - * This method updates the channel status and automatically notifies - * all registered listeners about the status change. + * + *

    This method updates the channel status and automatically notifies all registered listeners + * about the status change. * * @param status the new status for the channel */ @@ -119,14 +119,13 @@ protected void setStatus(ChannelStatus status) { /** * Sets the channel status with optional listener notification. - *

    - * This method updates the channel status and optionally notifies - * registered listeners about the status change. This allows for - * controlled status updates without triggering notifications. + * + *

    This method updates the channel status and optionally notifies registered listeners about + * the status change. This allows for controlled status updates without triggering notifications. * * @param status the new status for the channel - * @param notify if true, listeners will be notified of the status change; - * if false, no notifications will be sent + * @param notify if true, listeners will be notified of the status change; if false, no + * notifications will be sent */ protected void setStatus(ChannelStatus status, boolean notify) { if (status != this.status) { @@ -144,10 +143,9 @@ protected void setStatus(ChannelStatus status, boolean notify) { /** * Notifies all registered listeners about data that was read from the channel. - *

    - * This method is called internally when data is successfully read from - * the physical channel. It notifies all registered listeners with the - * channel name and the raw data that was read. + * + *

    This method is called internally when data is successfully read from the physical channel. + * It notifies all registered listeners with the channel name and the raw data that was read. * * @param data the raw bytes that were read from the channel */ @@ -161,10 +159,9 @@ protected void notifyOnDataRead(byte[] data) { /** * Notifies all registered listeners about data that was written to the channel. - *

    - * This method is called internally when data is successfully written to - * the physical channel. It notifies all registered listeners with the - * channel name and the raw data that was written. + * + *

    This method is called internally when data is successfully written to the physical channel. + * It notifies all registered listeners with the channel name and the raw data that was written. * * @param data the raw bytes that were written to the channel */ @@ -178,10 +175,9 @@ protected void notifyOnDataWritten(byte[] data) { /** * Notifies all registered listeners about a status change. - *

    - * This method is called internally when the channel status changes. - * It notifies all registered listeners with the channel name and - * the new status. + * + *

    This method is called internally when the channel status changes. It notifies all registered + * listeners with the channel name and the new status. */ protected void notifyOnStatusChanged() { for (ChannelListener subscriber : this.notifier.getSubscribers()) { @@ -191,10 +187,9 @@ protected void notifyOnStatusChanged() { /** * Notifies all registered listeners about an error that was detected. - *

    - * This method is called internally when an error occurs during channel - * operations. It notifies all registered listeners with the channel name, - * error code, and error message. + * + *

    This method is called internally when an error occurs during channel operations. It notifies + * all registered listeners with the channel name, error code, and error message. * * @param errorCode the numeric error code identifying the type of error * @param errorMessage a descriptive message explaining the error @@ -207,10 +202,9 @@ protected void notifyOnErrorDetected(int errorCode, String errorMessage) { /** * Initializes the physical channel with default values. - *

    - * This method sets up the initial state of the physical channel, - * including setting the status to STOPPED and initializing the - * notification system for managing listeners. + * + *

    This method sets up the initial state of the physical channel, including setting the status + * to STOPPED and initializing the notification system for managing listeners. */ private void init() { this.status = ChannelStatus.STOPPED; diff --git a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java b/src/main/java/enedis/lab/io/channels/ChannelProtocol.java index f6cda97..cda1586 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java +++ b/src/main/java/enedis/lab/io/channels/ChannelProtocol.java @@ -8,13 +8,12 @@ package enedis.lab.io.channels; /** - * Enumeration representing different types of communication channel protocols - * that can be used for data transmission in the TIC (Télé Information Client) system. - * - *

    This enum defines the various communication protocols supported by the system, - * ranging from file-based operations to network protocols and industrial communication - * standards like Modbus. - * + * Enumeration representing different types of communication channel protocols that can be used for + * data transmission in the TIC (Télé Information Client) system. + * + *

    This enum defines the various communication protocols supported by the system, ranging from + * file-based operations to network protocols and industrial communication standards like Modbus. + * * @author Jehan BOUSCH * @author Mathieu SABARTHES * @since 1.0 @@ -22,44 +21,41 @@ public enum ChannelProtocol { /** - * File-based communication protocol. - * Used for reading from or writing to local files on the filesystem. + * File-based communication protocol. Used for reading from or writing to local files on the + * filesystem. */ FILE, - + /** - * Serial port communication protocol. - * Used for direct communication through serial interfaces (RS-232, RS-485, etc.). + * Serial port communication protocol. Used for direct communication through serial interfaces + * (RS-232, RS-485, etc.). */ SERIAL_PORT, - + /** - * Modbus RTU (Remote Terminal Unit) protocol. - * A serial communication protocol commonly used in industrial automation systems. + * Modbus RTU (Remote Terminal Unit) protocol. A serial communication protocol commonly used in + * industrial automation systems. */ MODBUS_RTU, - - /** - * Modbus TCP protocol. - * A network-based implementation of the Modbus protocol over TCP/IP. - */ + + /** Modbus TCP protocol. A network-based implementation of the Modbus protocol over TCP/IP. */ MODBUS_TCP, - + /** - * Modbus stub protocol. - * A testing/mock implementation of Modbus for development and testing purposes. + * Modbus stub protocol. A testing/mock implementation of Modbus for development and testing + * purposes. */ MODBUS_STUB, - + /** - * Transmission Control Protocol (TCP). - * A reliable, connection-oriented network protocol for data transmission. + * Transmission Control Protocol (TCP). A reliable, connection-oriented network protocol for data + * transmission. */ TCP, - + /** - * User Datagram Protocol (UDP). - * A connectionless network protocol for fast, lightweight data transmission. + * User Datagram Protocol (UDP). A connectionless network protocol for fast, lightweight data + * transmission. */ UDP, } diff --git a/src/main/java/enedis/lab/io/channels/ChannelStatus.java b/src/main/java/enedis/lab/io/channels/ChannelStatus.java index 8fb5bd2..b052693 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelStatus.java +++ b/src/main/java/enedis/lab/io/channels/ChannelStatus.java @@ -8,34 +8,33 @@ package enedis.lab.io.channels; /** - * Enumeration representing the operational status of a communication channel - * in the TIC (Télé Information Client) system. - * - *

    This enum defines the possible states that a channel can be in during its lifecycle, - * from initialization through operation to error conditions. The status is used to monitor - * and manage the health and availability of communication channels. - * + * Enumeration representing the operational status of a communication channel in the TIC (Télé + * Information Client) system. + * + *

    This enum defines the possible states that a channel can be in during its lifecycle, from + * initialization through operation to error conditions. The status is used to monitor and manage + * the health and availability of communication channels. + * * @author Jehan BOUSCH * @author Mathieu SABARTHES * @since 1.0 */ public enum ChannelStatus { /** - * Channel is stopped and not actively processing data. - * This is the initial state when a channel is created but not yet started, - * or when it has been explicitly stopped. + * Channel is stopped and not actively processing data. This is the initial state when a channel + * is created but not yet started, or when it has been explicitly stopped. */ STOPPED, - + /** - * Channel is started and actively processing data. - * The channel is operational and ready to handle communication requests. + * Channel is started and actively processing data. The channel is operational and ready to handle + * communication requests. */ STARTED, - + /** - * Channel has encountered an error and is not functioning properly. - * This state indicates that the channel needs attention or recovery actions. + * Channel has encountered an error and is not functioning properly. This state indicates that the + * channel needs attention or recovery actions. */ ERROR } From 0f87f88b0dfb8b6b3f02e49ed8b7b5ab639e0a9d Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Mon, 29 Sep 2025 17:03:59 +0200 Subject: [PATCH 16/59] fix: author --- src/main/java/enedis/lab/io/channels/ChannelProtocol.java | 6 ++---- src/main/java/enedis/lab/io/channels/ChannelStatus.java | 7 ++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java b/src/main/java/enedis/lab/io/channels/ChannelProtocol.java index cda1586..b1f2c4e 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java +++ b/src/main/java/enedis/lab/io/channels/ChannelProtocol.java @@ -9,14 +9,12 @@ /** * Enumeration representing different types of communication channel protocols that can be used for - * data transmission in the TIC (Télé Information Client) system. + * data transmission. * *

    This enum defines the various communication protocols supported by the system, ranging from * file-based operations to network protocols and industrial communication standards like Modbus. * - * @author Jehan BOUSCH - * @author Mathieu SABARTHES - * @since 1.0 + * @author Enedis Smarties team */ public enum ChannelProtocol { diff --git a/src/main/java/enedis/lab/io/channels/ChannelStatus.java b/src/main/java/enedis/lab/io/channels/ChannelStatus.java index b052693..7204170 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelStatus.java +++ b/src/main/java/enedis/lab/io/channels/ChannelStatus.java @@ -8,16 +8,13 @@ package enedis.lab.io.channels; /** - * Enumeration representing the operational status of a communication channel in the TIC (Télé - * Information Client) system. + * Enumeration representing the operational status of a communication channel. * *

    This enum defines the possible states that a channel can be in during its lifecycle, from * initialization through operation to error conditions. The status is used to monitor and manage * the health and availability of communication channels. * - * @author Jehan BOUSCH - * @author Mathieu SABARTHES - * @since 1.0 + * @author Enedis Smarties team */ public enum ChannelStatus { /** From c5f5d139594c3d220755366ba7391e5aac0b9599 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Wed, 1 Oct 2025 11:46:55 +0200 Subject: [PATCH 17/59] chore: format enedis.lab.io package --- .../java/enedis/lab/io/PlugSubscriber.java | 81 +- src/main/java/enedis/lab/io/PortFinder.java | 62 +- .../java/enedis/lab/io/PortPlugNotifier.java | 431 ++--- .../ChannelSerialPortConfiguration.java | 1037 ++++++------ .../lab/io/channels/serialport/Parity.java | 129 +- .../lab/io/datastreams/DataInputStream.java | 174 +- .../enedis/lab/io/datastreams/DataStream.java | 102 +- .../lab/io/datastreams/DataStreamBase.java | 759 ++++----- .../datastreams/DataStreamConfiguration.java | 599 +++---- .../io/datastreams/DataStreamDirection.java | 60 +- .../io/datastreams/DataStreamException.java | 388 +++-- .../io/datastreams/DataStreamListener.java | 148 +- .../lab/io/datastreams/DataStreamStatus.java | 72 +- .../lab/io/datastreams/DataStreamType.java | 87 +- .../lab/io/datastreams/DataStreamUser.java | 236 ++- .../io/serialport/SerialPortDescriptor.java | 860 +++++----- .../lab/io/serialport/SerialPortFinder.java | 257 +-- .../io/serialport/SerialPortFinderBase.java | 230 ++- .../serialport/SerialPortFinderForLinux.java | 1351 ++++++++-------- .../SerialPortFinderForWindows.java | 892 ++++++----- .../java/enedis/lab/io/tic/TICModemType.java | 90 +- .../enedis/lab/io/tic/TICPortDescriptor.java | 583 +++---- .../java/enedis/lab/io/tic/TICPortFinder.java | 139 +- .../enedis/lab/io/tic/TICPortFinderBase.java | 402 +++-- .../lab/io/tic/TICPortPlugNotifier.java | 271 ++-- .../enedis/lab/io/usb/USBPortDescriptor.java | 1393 ++++++++--------- .../java/enedis/lab/io/usb/USBPortFinder.java | 144 +- .../enedis/lab/io/usb/USBPortFinderBase.java | 401 +++-- 28 files changed, 5698 insertions(+), 5680 deletions(-) diff --git a/src/main/java/enedis/lab/io/PlugSubscriber.java b/src/main/java/enedis/lab/io/PlugSubscriber.java index 41f007a..86f0b18 100644 --- a/src/main/java/enedis/lab/io/PlugSubscriber.java +++ b/src/main/java/enedis/lab/io/PlugSubscriber.java @@ -1,31 +1,50 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io; - -import enedis.lab.util.task.Subscriber; - -/** - * Interface used to be notified when something has been plugged or unplugged - * - * @param the information class associated with plugged or unplugged notification - */ -public interface PlugSubscriber extends Subscriber -{ - /** - * Notification when something has been plugged - * - * @param info the information on what has been plugged - */ - public void onPlugged(T info); - /** - * Notification when something has been unplugged - * - * @param info the information on what has been unplugged - */ - public void onUnplugged(T info); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io; + +import enedis.lab.util.task.Subscriber; + +/** + * Listener interface for receiving plug and unplug events from devices or hardware. + * + *

    This interface extends {@link Subscriber} and defines callback methods that are invoked when + * hardware devices are connected (plugged) or disconnected (unplugged) from the system. + * Implementations of this interface can be registered with device monitors or port finders to + * receive notifications about hardware state changes. + * + *

    The interface uses generics to allow different types of information to be passed with the + * plug/unplug events, such as device descriptors, port information, or other device-specific + * details. + * + * @param the type of information associated with plugged or unplugged notifications + * @author Enedis Smarties team + * @see Subscriber + */ +public interface PlugSubscriber extends Subscriber { + /** + * Called when a device has been plugged into the system. + * + *

    This method is invoked when the system detects that a new device has been connected. + * Implementations should handle the new device information according to their specific + * requirements. + * + * @param info the information about the device that has been plugged in + */ + public void onPlugged(T info); + + /** + * Called when a device has been unplugged from the system. + * + *

    This method is invoked when the system detects that a device has been disconnected. + * Implementations should handle the removal appropriately, such as closing connections or + * releasing resources associated with the device. + * + * @param info the information about the device that has been unplugged + */ + public void onUnplugged(T info); +} diff --git a/src/main/java/enedis/lab/io/PortFinder.java b/src/main/java/enedis/lab/io/PortFinder.java index eda1e69..9b21b6c 100644 --- a/src/main/java/enedis/lab/io/PortFinder.java +++ b/src/main/java/enedis/lab/io/PortFinder.java @@ -1,24 +1,38 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io; - -import enedis.lab.types.DataList; - -/** - * Interface used to find all port descriptor - * @param the port descriptor class - */ -public interface PortFinder -{ - /** - * Find all port descriptor - * - * @return The port descriptor list found - */ - public DataList findAll(); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io; + +import enedis.lab.types.DataList; + +/** + * Generic interface for discovering and enumerating ports on the system. + * + *

    This interface defines a contract for port discovery implementations that can find and return + * information about available ports. It uses generics to support different types of port + * descriptors, such as serial ports, USB ports, or other communication ports. + * + *

    Implementations of this interface are responsible for querying the underlying system (via + * OS-specific APIs, device managers, or file systems) to enumerate all available ports and return + * their descriptors. + * + * @param the type of port descriptor returned by this finder + * @author Enedis Smarties team + * @see DataList + */ +public interface PortFinder { + /** + * Discovers and returns all available ports on the system. + * + *

    This method queries the system to find all ports that are currently available. The specific + * discovery mechanism depends on the implementation and may involve platform-specific APIs or + * system queries. + * + * @return a list of port descriptors representing all discovered ports + */ + public DataList findAll(); +} diff --git a/src/main/java/enedis/lab/io/PortPlugNotifier.java b/src/main/java/enedis/lab/io/PortPlugNotifier.java index 9b4a8fd..2c0e251 100644 --- a/src/main/java/enedis/lab/io/PortPlugNotifier.java +++ b/src/main/java/enedis/lab/io/PortPlugNotifier.java @@ -1,214 +1,217 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io; - -import java.util.Iterator; -import java.util.concurrent.atomic.AtomicReference; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; -import enedis.lab.util.task.TaskPeriodicWithSubscribers; -import enedis.lab.util.time.Time; - -/** - * Class used to notify when a port has been plugged or unplugged - * - * @param - * the port finder interface - * @param - * the port descriptor class - */ -public class PortPlugNotifier, T> extends TaskPeriodicWithSubscribers> -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program writing on the output stream when a port has been plugged or unplugged - * - * @param - * the port finder interface - * @param - * the port descriptor class - * @param notifier - * the port plug notifier instance - * @param subscriber - * the port plus subscriber instance - */ - public static , T> void main(PortPlugNotifier notifier, PlugSubscriber subscriber) - { - /* 1. Add program shutdown hook when CTRL+C is pressed to exit program properly */ - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() - { - @Override - public void run() - { - notifier.unsubscribe(subscriber); - notifier.stop(); - } - })); - /* 4. Add a subscriber to notification service */ - notifier.subscribe(subscriber); - /* 5. Start the notification service */ - notifier.start(); - /* 6. Execute forever loop until CTRL+C is pressed */ - while (notifier.isRunning()) - { - Time.sleep(1000); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private AtomicReference finder = new AtomicReference(); - private DataList descriptors = new DataArrayList(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor with all parameters - * - * @param period - * the period (in milliseconds) used to look for plugged or unplugged serial port - * @param finder - * the serial port finder interface used to find all serial port descriptors - */ - public PortPlugNotifier(long period, F finder) - { - super(period); - this.setFinder(finder); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Set the finder - * - * @param finder - * the serial port finder interface used to find all serial port descriptors - * @throws IllegalArgumentException - * if finder is null - */ - public void setFinder(F finder) - { - if (finder == null) - { - throw new IllegalArgumentException("Cannot set null finder"); - } - this.finder.set(finder); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void process() - { - DataList newDescriptors = this.finder.get().findAll(); - - this.checkAndNotifyIfUnplugged(newDescriptors); - this.checkAndNotifyIfPlugged(newDescriptors); - this.updateDescriptors(newDescriptors); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void checkAndNotifyIfUnplugged(DataList newDescriptors) - { - Iterator it = this.descriptors.iterator(); - - while (it.hasNext()) - { - T descriptor = it.next(); - if (!newDescriptors.contains(descriptor)) - { - this.notifyOnUnplugged(descriptor); - } - } - } - - private void checkAndNotifyIfPlugged(DataList newDescriptors) - { - Iterator it = newDescriptors.iterator(); - - while (it.hasNext()) - { - T newDescriptor = it.next(); - if (!this.descriptors.contains(newDescriptor)) - { - this.notifyOnPlugged(newDescriptor); - } - } - } - - private void updateDescriptors(DataList newDescriptors) - { - synchronized (this.descriptors) - { - this.descriptors.clear(); - this.descriptors.addAll(newDescriptors); - } - } - - private void notifyOnPlugged(T info) - { - for (PlugSubscriber subscriber : this.notifier.getSubscribers()) - { - subscriber.onPlugged(info); - } - } - - private void notifyOnUnplugged(T info) - { - for (PlugSubscriber subscriber : this.notifier.getSubscribers()) - { - subscriber.onUnplugged(info); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io; + +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataList; +import enedis.lab.util.task.TaskPeriodicWithSubscribers; +import enedis.lab.util.time.Time; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Periodic monitor that detects and notifies when ports are plugged or unplugged. + * + *

    This class extends {@link TaskPeriodicWithSubscribers} to provide automatic detection of port + * connection and disconnection events. It periodically polls the system using a {@link PortFinder} + * to discover available ports, compares the results with the previous state, and notifies + * registered {@link PlugSubscriber} instances of any changes. + * + *

    The notifier maintains a cached list of known port descriptors and performs delta detection to + * identify: + * + *

      + *
    • Newly plugged ports (present in current scan but not in previous) + *
    • Unplugged ports (present in previous scan but not in current) + *
    + * + *

    The class is thread-safe and uses atomic references for the finder and synchronized access for + * the descriptor list to ensure consistency during concurrent access. + * + * @param the port finder type that extends PortFinder + * @param the port descriptor type + * @author Enedis Smarties team + * @see PortFinder + * @see PlugSubscriber + * @see TaskPeriodicWithSubscribers + */ +public class PortPlugNotifier, T> + extends TaskPeriodicWithSubscribers> { + private AtomicReference finder = new AtomicReference(); + private DataList descriptors = new DataArrayList(); + + /** + * Main utility method that runs a port plug monitoring loop. + * + *

    This method sets up a shutdown hook to gracefully stop the notifier when CTRL+C is pressed, + * subscribes the provided subscriber, starts the notifier, and runs an infinite loop until the + * notifier is stopped. + * + *

    This is typically used for testing or standalone monitoring applications. + * + * @param the port finder type that extends PortFinder + * @param the port descriptor type + * @param notifier the port plug notifier instance to run + * @param subscriber the subscriber to receive plug/unplug notifications + */ + public static , T> void main( + PortPlugNotifier notifier, PlugSubscriber subscriber) { + /* 1. Add program shutdown hook when CTRL+C is pressed to exit program properly */ + Runtime.getRuntime() + .addShutdownHook( + new Thread( + new Runnable() { + @Override + public void run() { + notifier.unsubscribe(subscriber); + notifier.stop(); + } + })); + /* 4. Add a subscriber to notification service */ + notifier.subscribe(subscriber); + /* 5. Start the notification service */ + notifier.start(); + /* 6. Execute forever loop until CTRL+C is pressed */ + while (notifier.isRunning()) { + Time.sleep(1000); + } + } + + /** + * Constructs a PortPlugNotifier with the specified polling period and port finder. + * + *

    The notifier will periodically scan for ports using the provided finder at the specified + * interval. The period determines the responsiveness of plug/unplug detection. + * + * @param period the polling period in milliseconds + * @param finder the port finder used to discover available ports + */ + public PortPlugNotifier(long period, F finder) { + super(period); + this.setFinder(finder); + } + + /** + * Sets the port finder used to discover available ports. + * + *

    This method updates the finder used for port discovery. The finder is stored in an atomic + * reference to ensure thread-safe access during periodic polling. + * + * @param finder the port finder to use for discovering ports + * @throws IllegalArgumentException if finder is null + */ + public void setFinder(F finder) { + if (finder == null) { + throw new IllegalArgumentException("Cannot set null finder"); + } + this.finder.set(finder); + } + + /** + * Periodically executed process that detects port changes and notifies subscribers. + * + *

    This method is called at each polling interval and performs the following steps: + * + *

      + *
    1. Discovers currently available ports using the finder + *
    2. Checks for unplugged ports (in old list but not in new list) + *
    3. Checks for newly plugged ports (in new list but not in old list) + *
    4. Updates the internal descriptor cache with the new state + *
    + */ + @Override + protected void process() { + DataList newDescriptors = this.finder.get().findAll(); + + this.checkAndNotifyIfUnplugged(newDescriptors); + this.checkAndNotifyIfPlugged(newDescriptors); + this.updateDescriptors(newDescriptors); + } + + /** + * Checks for unplugged ports and notifies subscribers. + * + *

    This method compares the current descriptor list with the new list to identify ports that + * were present before but are no longer available. For each unplugged port, it notifies all + * subscribers. + * + * @param newDescriptors the newly discovered list of port descriptors + */ + private void checkAndNotifyIfUnplugged(DataList newDescriptors) { + Iterator it = this.descriptors.iterator(); + + while (it.hasNext()) { + T descriptor = it.next(); + if (!newDescriptors.contains(descriptor)) { + this.notifyOnUnplugged(descriptor); + } + } + } + + /** + * Checks for newly plugged ports and notifies subscribers. + * + *

    This method compares the new descriptor list with the current list to identify ports that + * are newly available. For each newly plugged port, it notifies all subscribers. + * + * @param newDescriptors the newly discovered list of port descriptors + */ + private void checkAndNotifyIfPlugged(DataList newDescriptors) { + Iterator it = newDescriptors.iterator(); + + while (it.hasNext()) { + T newDescriptor = it.next(); + if (!this.descriptors.contains(newDescriptor)) { + this.notifyOnPlugged(newDescriptor); + } + } + } + + /** + * Updates the internal descriptor cache with the new list of ports. + * + *

    This method clears the current descriptor list and replaces it with the newly discovered + * ports. The operation is synchronized to ensure thread-safe access. + * + * @param newDescriptors the new list of port descriptors to cache + */ + private void updateDescriptors(DataList newDescriptors) { + synchronized (this.descriptors) { + this.descriptors.clear(); + this.descriptors.addAll(newDescriptors); + } + } + + /** + * Notifies all subscribers that a port has been plugged in. + * + *

    This method iterates through all subscribed listeners and calls their onPlugged method with + * the port information. + * + * @param info the information about the port that was plugged in + */ + private void notifyOnPlugged(T info) { + for (PlugSubscriber subscriber : this.notifier.getSubscribers()) { + subscriber.onPlugged(info); + } + } + + /** + * Notifies all subscribers that a port has been unplugged. + * + *

    This method iterates through all subscribed listeners and calls their onUnplugged method + * with the port information. + * + * @param info the information about the port that was unplugged + */ + private void notifyOnUnplugged(T info) { + for (PlugSubscriber subscriber : this.notifier.getSubscribers()) { + subscriber.onUnplugged(info); + } + } +} diff --git a/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java b/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java index ced2c10..451f869 100644 --- a/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java +++ b/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java @@ -1,505 +1,532 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.io.channels.ChannelConfiguration; -import enedis.lab.io.channels.ChannelDirection; -import enedis.lab.io.channels.ChannelProtocol; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * Configuration class for serial port communication channels. - *

    - * This class extends {@link ChannelConfiguration} and provides specific configuration - * parameters for serial port communication, including port identification, baud rate, - * parity settings, data bits, stop bits, and timeout configurations. - *

    - * The configuration supports both port ID and port name identification methods, - * with validation for standard serial communication parameters. - *

    - * Supported baud rates: 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, - * 115200, 230400, 460800, 921600, 1843200, 3686400 - *

    - * Supported data bits: 5, 6, 7, 8 - *

    - * Supported stop bits: 1.0, 1.5, 2.0 - *

    - * Default sync read timeout: 10000 milliseconds - * - * @author Enedis Smarties team - * @see ChannelConfiguration - * @see ChannelProtocol#SERIAL_PORT - * @see ChannelDirection#RXTX - */ -public class ChannelSerialPortConfiguration extends ChannelConfiguration { - - /** Key for the serial port identifier configuration parameter. */ - protected static final String KEY_PORT_ID = "portId"; - /** Key for the serial port name configuration parameter. */ - protected static final String KEY_PORT_NAME = "portName"; - /** Key for the baud rate configuration parameter. */ - protected static final String KEY_BAUDRATE = "baudrate"; - /** Key for the parity configuration parameter. */ - protected static final String KEY_PARITY = "parity"; - /** Key for the data bits configuration parameter. */ - protected static final String KEY_DATA_BITS = "dataBits"; - /** Key for the stop bits configuration parameter. */ - protected static final String KEY_STOP_BITS = "stopBits"; - /** Key for the synchronous read timeout configuration parameter. */ - protected static final String KEY_SYNC_READ_TIMEOUT = "syncReadTimeout"; - - /** Accepted protocol value for serial port channels. */ - private static final ChannelProtocol PROTOCOL_ACCEPTED_VALUE = ChannelProtocol.SERIAL_PORT; - /** Accepted direction value for serial port channels (bidirectional). */ - private static final ChannelDirection DIRECTION_ACCEPTED_VALUE = ChannelDirection.RXTX; - /** Array of supported baud rate values for serial communication. */ - private static final Number[] BAUDRATE_ACCEPTED_VALUES = { 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, - 115200, 230400, 460800, 921600, 1843200, 3686400 }; - /** Array of supported data bits values (5, 6, 7, or 8 bits). */ - private static final Number[] DATA_BITS_ACCEPTED_VALUES = { 5, 6, 7, 8 }; - /** Array of supported stop bits values (1.0, 1.5, or 2.0). */ - private static final Number[] STOP_BITS_ACCEPTED_VALUES = { 1.0d, 1.5d, 2.0d }; - /** Default timeout value for synchronous read operations (10000 milliseconds). */ - private static final Number SYNC_READ_TIMEOUT_DEFAULT_VALUE = 10000; - - /** List of key descriptors for configuration validation. */ - private List> keys = new ArrayList>(); - - /** Key descriptor for port ID validation. */ - protected KeyDescriptorString kPortId; - /** Key descriptor for port name validation. */ - protected KeyDescriptorString kPortName; - /** Key descriptor for baud rate validation. */ - protected KeyDescriptorNumber kBaudrate; - /** Key descriptor for parity validation. */ - protected KeyDescriptorEnum kParity; - /** Key descriptor for data bits validation. */ - protected KeyDescriptorNumber kDataBits; - /** Key descriptor for stop bits validation. */ - protected KeyDescriptorNumber kStopBits; - /** Key descriptor for sync read timeout validation. */ - protected KeyDescriptorNumber kSyncReadTimeout; - - /** - * Default constructor for serial port configuration. - *

    - * Initializes the configuration with default values and sets up - * the key descriptors for parameter validation. - */ - protected ChannelSerialPortConfiguration() { - super(); - this.loadKeyDescriptors(); - - this.kProtocol.setAcceptedValues(PROTOCOL_ACCEPTED_VALUE); - this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUE); - } - - /** - * Constructs a new serial port configuration from a map of key-value pairs. - *

    - * This constructor initializes the configuration with values from the provided map, - * performing validation and setting default values for missing parameters. - * - * @param map the map containing configuration parameters - * @throws DataDictionaryException if the map contains invalid values or required - * parameters are missing - */ - public ChannelSerialPortConfiguration(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a new serial port configuration by copying from another data dictionary. - *

    - * This constructor creates a new configuration instance by copying all parameters - * from the provided data dictionary, ensuring consistency between configurations. - * - * @param other the data dictionary to copy configuration from - * @throws DataDictionaryException if the source data dictionary contains invalid - * values or is incompatible with serial port configuration - */ - public ChannelSerialPortConfiguration(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a new serial port configuration with a name and file reference, - * initializing all parameters to their default values. - *

    - * - * @param name the configuration name - * @param file the configuration file associated with this configuration - */ - public ChannelSerialPortConfiguration(String name, File file) { - this(); - this.init(name, file); - } - - /** - * Constructs a new serial port configuration with all parameters set to specific values. - *

    - * This constructor provides a convenient way to create a fully configured serial port - * channel with all necessary parameters specified at construction time. - * - * @param name the configuration name - * @param alias the configuration alias - * @param portId the serial port identifier (alternative to portName) - * @param portName the serial port name (alternative to portId) - * @param baudrate the communication baud rate - * @param parity the parity setting for data validation - * @param dataBits the number of data bits per frame - * @param stopBits the number of stop bits - * @param syncReadTimeout the timeout for synchronous read operations in milliseconds - * @throws DataDictionaryException if any of the provided values are invalid or - * incompatible with serial port communication requirements - */ - public ChannelSerialPortConfiguration(String name, String alias, String portId, String portName, Number baudrate, - Parity parity, Number dataBits, Number stopBits, - Number syncReadTimeout) throws DataDictionaryException { - this(); - - this.setName(name); - this.setAlias(alias); - this.setPortId(portId); - this.setPortName(portName); - this.setBaudrate(baudrate); - this.setParity(parity); - this.setDataBits(dataBits); - this.setStopBits(stopBits); - this.setSyncReadTimeout(syncReadTimeout); - - this.checkAndUpdate(); - } - - /** - * Updates optional parameters with default values if not already set. - *

    - * This method ensures that either port ID or port name is specified, - * and sets default values for protocol, direction, and sync read timeout - * if they are not already configured. - * - * @throws DataDictionaryException if neither port ID nor port name is defined, - * or if there are validation errors with the configuration - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_PORT_ID) && !this.exists(KEY_PORT_NAME)) { - throw new DataDictionaryException("Neither " + KEY_PORT_ID + " nor " + KEY_PORT_NAME + " is defined"); - } - if (!this.exists(KEY_PROTOCOL)) { - this.setProtocol(PROTOCOL_ACCEPTED_VALUE); - } - if (!this.exists(KEY_DIRECTION)) { - this.setDirection(DIRECTION_ACCEPTED_VALUE); - } - if (!this.exists(KEY_SYNC_READ_TIMEOUT)) { - this.setSyncReadTimeout(SYNC_READ_TIMEOUT_DEFAULT_VALUE); - } - super.updateOptionalParameters(); - } - - /** - * Retrieves the serial port identifier. - *

    - * The port ID is an alternative way to identify the serial port, - * typically used when the system provides a unique identifier for the port. - * - * @return the port identifier, or null if not set - */ - public String getPortId() { - return (String) this.data.get(KEY_PORT_ID); - } - - /** - * Retrieves the serial port name. - *

    - * The port name is typically a system-specific identifier such as - * "/dev/ttyUSB0" on Unix systems or "COM1" on Windows systems. - * - * @return the port name, or null if not set - */ - public String getPortName() { - return (String) this.data.get(KEY_PORT_NAME); - } - - /** - * Retrieves the communication baud rate. - *

    - * The baud rate determines the speed of data transmission over the serial port. - * - * @return the baud rate in bits per second, or null if not set - */ - public Number getBaudrate() { - return (Number) this.data.get(KEY_BAUDRATE); - } - - /** - * Retrieves the parity setting for data validation. - *

    - * Parity is used for error detection in serial communication. - * It can be NONE, ODD, or EVEN depending on the communication protocol. - * - * @return the parity setting, or null if not set - */ - public Parity getParity() { - return (Parity) this.data.get(KEY_PARITY); - } - - /** - * Retrieves the number of data bits per frame. - *

    - * Data bits determine how many bits are used to represent each character - * in the serial communication. Common values are 7 or 8 bits. - * - * @return the number of data bits, or null if not set - */ - public Number getDataBits() { - return (Number) this.data.get(KEY_DATA_BITS); - } - - /** - * Retrieves the number of stop bits. - *

    - * Stop bits are used to signal the end of a data frame in serial communication. - * - * @return the number of stop bits, or null if not set - */ - public Number getStopBits() { - return (Number) this.data.get(KEY_STOP_BITS); - } - - /** - * Retrieves the timeout for synchronous read operations. - *

    - * This timeout determines how long the system will wait for data to be - * available on the serial port before timing out the read operation. - * - * @return the timeout value in milliseconds, or null if not set - */ - public Number getSyncReadTimeout() { - return (Number) this.data.get(KEY_SYNC_READ_TIMEOUT); - } - - /** - * Sets the serial port identifier. - *

    - * The port ID provides an alternative way to identify the serial port, - * typically used when the system provides a unique identifier for the port. - * Either port ID or port name must be specified for a valid configuration. - * - * @param portId the port identifier to set - * @throws DataDictionaryException if the port ID is invalid or conflicts - * with other configuration parameters - */ - public void setPortId(String portId) throws DataDictionaryException { - this.setPortId((Object) portId); - } - - /** - * Sets the serial port name. - *

    - * The port name is typically a system-specific identifier such as - * "/dev/ttyUSB0" on Unix systems or "COM1" on Windows systems. - * Either port ID or port name must be specified for a valid configuration. - * - * @param portName the port name to set - * @throws DataDictionaryException if the port name is invalid or conflicts - * with other configuration parameters - */ - public void setPortName(String portName) throws DataDictionaryException { - this.setPortName((Object) portName); - } - - /** - * Sets the communication baud rate. - *

    - * The baud rate determines the speed of data transmission over the serial port. - * Must be one of the supported values: {@link #BAUDRATE_ACCEPTED_VALUES}. - * - * @param baudrate the baud rate in bits per second - * @throws DataDictionaryException if the baud rate is not supported or invalid - */ - public void setBaudrate(Number baudrate) throws DataDictionaryException { - this.setBaudrate((Object) baudrate); - } - - /** - * Sets the parity setting for data validation. - *

    - * Parity is used for error detection in serial communication. - * The parity setting must be one of the supported {@link Parity} enum values. - * - * @param parity the parity setting to use - * @throws DataDictionaryException if the parity value is invalid or not supported - */ - public void setParity(Parity parity) throws DataDictionaryException { - this.setParity((Object) parity); - } - - /** - * Sets the number of data bits per frame. - *

    - * Data bits determine how many bits are used to represent each character - * in the serial communication. Must be one of: {@link #DATA_BITS_ACCEPTED_VALUES}. - * - * @param dataBits the number of data bits to use - * @throws DataDictionaryException if the data bits value is not supported or invalid - */ - public void setDataBits(Number dataBits) throws DataDictionaryException { - this.setDataBits((Object) dataBits); - } - - /** - * Sets the number of stop bits. - *

    - * Stop bits are used to signal the end of a data frame in serial communication. - * Must be one of: {@link #STOP_BITS_ACCEPTED_VALUES}. - * - * @param stopBits the number of stop bits to use - * @throws DataDictionaryException if the stop bits value is not supported or invalid - */ - public void setStopBits(Number stopBits) throws DataDictionaryException { - this.setStopBits((Object) stopBits); - } - - /** - * Sets the timeout for synchronous read operations. - *

    - * This timeout determines how long the system will wait for data to be - * available on the serial port before timing out the read operation. - * If not set, defaults to {@link #SYNC_READ_TIMEOUT_DEFAULT_VALUE}. - * - * @param syncReadTimeout the timeout value in milliseconds - * @throws DataDictionaryException if the timeout value is invalid or negative - */ - public void setSyncReadTimeout(Number syncReadTimeout) throws DataDictionaryException { - this.setSyncReadTimeout((Object) syncReadTimeout); - } - - /** - * Internal method to set the port ID with object conversion. - * - * @param portId the port ID object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setPortId(Object portId) throws DataDictionaryException { - this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); - } - - /** - * Internal method to set the port name with object conversion. - * - * @param portName the port name object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setPortName(Object portName) throws DataDictionaryException { - this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); - } - - /** - * Internal method to set the baud rate with object conversion. - * - * @param baudrate the baud rate object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setBaudrate(Object baudrate) throws DataDictionaryException { - this.data.put(KEY_BAUDRATE, this.kBaudrate.convert(baudrate)); - } - - /** - * Internal method to set the parity with object conversion. - * - * @param parity the parity object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setParity(Object parity) throws DataDictionaryException { - this.data.put(KEY_PARITY, this.kParity.convert(parity)); - } - - /** - * Internal method to set the data bits with object conversion. - * - * @param dataBits the data bits object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setDataBits(Object dataBits) throws DataDictionaryException { - this.data.put(KEY_DATA_BITS, this.kDataBits.convert(dataBits)); - } - - /** - * Internal method to set the stop bits with object conversion. - * - * @param stopBits the stop bits object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setStopBits(Object stopBits) throws DataDictionaryException { - this.data.put(KEY_STOP_BITS, this.kStopBits.convert(stopBits)); - } - - /** - * Internal method to set the sync read timeout with object conversion. - * - * @param syncReadTimeout the sync read timeout object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setSyncReadTimeout(Object syncReadTimeout) throws DataDictionaryException { - this.data.put(KEY_SYNC_READ_TIMEOUT, this.kSyncReadTimeout.convert(syncReadTimeout)); - } - - /** - * Initializes and configures all key descriptors for parameter validation. - *

    - * This method sets up the validation rules for all serial port configuration - * parameters, including accepted values and required/optional status. - * - * @throws RuntimeException if there is an error during key descriptor initialization - */ - private void loadKeyDescriptors() { - try { - this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); - this.keys.add(this.kPortId); - - this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); - this.keys.add(this.kPortName); - - this.kBaudrate = new KeyDescriptorNumber(KEY_BAUDRATE, true); - this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); - this.keys.add(this.kBaudrate); - - this.kParity = new KeyDescriptorEnum(KEY_PARITY, true, Parity.class); - this.keys.add(this.kParity); - - this.kDataBits = new KeyDescriptorNumber(KEY_DATA_BITS, true); - this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUES); - this.keys.add(this.kDataBits); - - this.kStopBits = new KeyDescriptorNumber(KEY_STOP_BITS, true); - this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUES); - this.keys.add(this.kStopBits); - - this.kSyncReadTimeout = new KeyDescriptorNumber(KEY_SYNC_READ_TIMEOUT, false); - this.keys.add(this.kSyncReadTimeout); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels.serialport; + +import enedis.lab.io.channels.ChannelConfiguration; +import enedis.lab.io.channels.ChannelDirection; +import enedis.lab.io.channels.ChannelProtocol; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import enedis.lab.types.datadictionary.KeyDescriptorNumber; +import enedis.lab.types.datadictionary.KeyDescriptorString; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Configuration class for serial port communication channels. + * + *

    This class extends {@link ChannelConfiguration} and provides specific configuration parameters + * for serial port communication, including port identification, baud rate, parity settings, data + * bits, stop bits, and timeout configurations. + * + *

    The configuration supports both port ID and port name identification methods, with validation + * for standard serial communication parameters. + * + *

    Supported baud rates: 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, + * 460800, 921600, 1843200, 3686400 + * + *

    Supported data bits: 5, 6, 7, 8 + * + *

    Supported stop bits: 1.0, 1.5, 2.0 + * + *

    Default sync read timeout: 10000 milliseconds + * + * @author Enedis Smarties team + * @see ChannelConfiguration + * @see ChannelProtocol#SERIAL_PORT + * @see ChannelDirection#RXTX + */ +public class ChannelSerialPortConfiguration extends ChannelConfiguration { + + /** Key for the serial port identifier configuration parameter. */ + protected static final String KEY_PORT_ID = "portId"; + + /** Key for the serial port name configuration parameter. */ + protected static final String KEY_PORT_NAME = "portName"; + + /** Key for the baud rate configuration parameter. */ + protected static final String KEY_BAUDRATE = "baudrate"; + + /** Key for the parity configuration parameter. */ + protected static final String KEY_PARITY = "parity"; + + /** Key for the data bits configuration parameter. */ + protected static final String KEY_DATA_BITS = "dataBits"; + + /** Key for the stop bits configuration parameter. */ + protected static final String KEY_STOP_BITS = "stopBits"; + + /** Key for the synchronous read timeout configuration parameter. */ + protected static final String KEY_SYNC_READ_TIMEOUT = "syncReadTimeout"; + + /** Accepted protocol value for serial port channels. */ + private static final ChannelProtocol PROTOCOL_ACCEPTED_VALUE = ChannelProtocol.SERIAL_PORT; + + /** Accepted direction value for serial port channels (bidirectional). */ + private static final ChannelDirection DIRECTION_ACCEPTED_VALUE = ChannelDirection.RXTX; + + /** Array of supported baud rate values for serial communication. */ + private static final Number[] BAUDRATE_ACCEPTED_VALUES = { + 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1843200, + 3686400 + }; + + /** Array of supported data bits values (5, 6, 7, or 8 bits). */ + private static final Number[] DATA_BITS_ACCEPTED_VALUES = {5, 6, 7, 8}; + + /** Array of supported stop bits values (1.0, 1.5, or 2.0). */ + private static final Number[] STOP_BITS_ACCEPTED_VALUES = {1.0d, 1.5d, 2.0d}; + + /** Default timeout value for synchronous read operations (10000 milliseconds). */ + private static final Number SYNC_READ_TIMEOUT_DEFAULT_VALUE = 10000; + + /** List of key descriptors for configuration validation. */ + private List> keys = new ArrayList>(); + + /** Key descriptor for port ID validation. */ + protected KeyDescriptorString kPortId; + + /** Key descriptor for port name validation. */ + protected KeyDescriptorString kPortName; + + /** Key descriptor for baud rate validation. */ + protected KeyDescriptorNumber kBaudrate; + + /** Key descriptor for parity validation. */ + protected KeyDescriptorEnum kParity; + + /** Key descriptor for data bits validation. */ + protected KeyDescriptorNumber kDataBits; + + /** Key descriptor for stop bits validation. */ + protected KeyDescriptorNumber kStopBits; + + /** Key descriptor for sync read timeout validation. */ + protected KeyDescriptorNumber kSyncReadTimeout; + + /** + * Default constructor for serial port configuration. + * + *

    Initializes the configuration with default values and sets up the key descriptors for + * parameter validation. + */ + protected ChannelSerialPortConfiguration() { + super(); + this.loadKeyDescriptors(); + + this.kProtocol.setAcceptedValues(PROTOCOL_ACCEPTED_VALUE); + this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUE); + } + + /** + * Constructs a new serial port configuration from a map of key-value pairs. + * + *

    This constructor initializes the configuration with values from the provided map, performing + * validation and setting default values for missing parameters. + * + * @param map the map containing configuration parameters + * @throws DataDictionaryException if the map contains invalid values or required parameters are + * missing + */ + public ChannelSerialPortConfiguration(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructs a new serial port configuration by copying from another data dictionary. + * + *

    This constructor creates a new configuration instance by copying all parameters from the + * provided data dictionary, ensuring consistency between configurations. + * + * @param other the data dictionary to copy configuration from + * @throws DataDictionaryException if the source data dictionary contains invalid values or is + * incompatible with serial port configuration + */ + public ChannelSerialPortConfiguration(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructs a new serial port configuration with a name and file reference, initializing all + * parameters to their default values. + * + *

    + * + * @param name the configuration name + * @param file the configuration file associated with this configuration + */ + public ChannelSerialPortConfiguration(String name, File file) { + this(); + this.init(name, file); + } + + /** + * Constructs a new serial port configuration with all parameters set to specific values. + * + *

    This constructor provides a convenient way to create a fully configured serial port channel + * with all necessary parameters specified at construction time. + * + * @param name the configuration name + * @param alias the configuration alias + * @param portId the serial port identifier (alternative to portName) + * @param portName the serial port name (alternative to portId) + * @param baudrate the communication baud rate + * @param parity the parity setting for data validation + * @param dataBits the number of data bits per frame + * @param stopBits the number of stop bits + * @param syncReadTimeout the timeout for synchronous read operations in milliseconds + * @throws DataDictionaryException if any of the provided values are invalid or incompatible with + * serial port communication requirements + */ + public ChannelSerialPortConfiguration( + String name, + String alias, + String portId, + String portName, + Number baudrate, + Parity parity, + Number dataBits, + Number stopBits, + Number syncReadTimeout) + throws DataDictionaryException { + this(); + + this.setName(name); + this.setAlias(alias); + this.setPortId(portId); + this.setPortName(portName); + this.setBaudrate(baudrate); + this.setParity(parity); + this.setDataBits(dataBits); + this.setStopBits(stopBits); + this.setSyncReadTimeout(syncReadTimeout); + + this.checkAndUpdate(); + } + + /** + * Updates optional parameters with default values if not already set. + * + *

    This method ensures that either port ID or port name is specified, and sets default values + * for protocol, direction, and sync read timeout if they are not already configured. + * + * @throws DataDictionaryException if neither port ID nor port name is defined, or if there are + * validation errors with the configuration + */ + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_PORT_ID) && !this.exists(KEY_PORT_NAME)) { + throw new DataDictionaryException( + "Neither " + KEY_PORT_ID + " nor " + KEY_PORT_NAME + " is defined"); + } + if (!this.exists(KEY_PROTOCOL)) { + this.setProtocol(PROTOCOL_ACCEPTED_VALUE); + } + if (!this.exists(KEY_DIRECTION)) { + this.setDirection(DIRECTION_ACCEPTED_VALUE); + } + if (!this.exists(KEY_SYNC_READ_TIMEOUT)) { + this.setSyncReadTimeout(SYNC_READ_TIMEOUT_DEFAULT_VALUE); + } + super.updateOptionalParameters(); + } + + /** + * Retrieves the serial port identifier. + * + *

    The port ID is an alternative way to identify the serial port, typically used when the + * system provides a unique identifier for the port. + * + * @return the port identifier, or null if not set + */ + public String getPortId() { + return (String) this.data.get(KEY_PORT_ID); + } + + /** + * Retrieves the serial port name. + * + *

    The port name is typically a system-specific identifier such as "/dev/ttyUSB0" on Unix + * systems or "COM1" on Windows systems. + * + * @return the port name, or null if not set + */ + public String getPortName() { + return (String) this.data.get(KEY_PORT_NAME); + } + + /** + * Retrieves the communication baud rate. + * + *

    The baud rate determines the speed of data transmission over the serial port. + * + * @return the baud rate in bits per second, or null if not set + */ + public Number getBaudrate() { + return (Number) this.data.get(KEY_BAUDRATE); + } + + /** + * Retrieves the parity setting for data validation. + * + *

    Parity is used for error detection in serial communication. It can be NONE, ODD, or EVEN + * depending on the communication protocol. + * + * @return the parity setting, or null if not set + */ + public Parity getParity() { + return (Parity) this.data.get(KEY_PARITY); + } + + /** + * Retrieves the number of data bits per frame. + * + *

    Data bits determine how many bits are used to represent each character in the serial + * communication. Common values are 7 or 8 bits. + * + * @return the number of data bits, or null if not set + */ + public Number getDataBits() { + return (Number) this.data.get(KEY_DATA_BITS); + } + + /** + * Retrieves the number of stop bits. + * + *

    Stop bits are used to signal the end of a data frame in serial communication. + * + * @return the number of stop bits, or null if not set + */ + public Number getStopBits() { + return (Number) this.data.get(KEY_STOP_BITS); + } + + /** + * Retrieves the timeout for synchronous read operations. + * + *

    This timeout determines how long the system will wait for data to be available on the serial + * port before timing out the read operation. + * + * @return the timeout value in milliseconds, or null if not set + */ + public Number getSyncReadTimeout() { + return (Number) this.data.get(KEY_SYNC_READ_TIMEOUT); + } + + /** + * Sets the serial port identifier. + * + *

    The port ID provides an alternative way to identify the serial port, typically used when the + * system provides a unique identifier for the port. Either port ID or port name must be specified + * for a valid configuration. + * + * @param portId the port identifier to set + * @throws DataDictionaryException if the port ID is invalid or conflicts with other configuration + * parameters + */ + public void setPortId(String portId) throws DataDictionaryException { + this.setPortId((Object) portId); + } + + /** + * Sets the serial port name. + * + *

    The port name is typically a system-specific identifier such as "/dev/ttyUSB0" on Unix + * systems or "COM1" on Windows systems. Either port ID or port name must be specified for a valid + * configuration. + * + * @param portName the port name to set + * @throws DataDictionaryException if the port name is invalid or conflicts with other + * configuration parameters + */ + public void setPortName(String portName) throws DataDictionaryException { + this.setPortName((Object) portName); + } + + /** + * Sets the communication baud rate. + * + *

    The baud rate determines the speed of data transmission over the serial port. Must be one of + * the supported values: {@link #BAUDRATE_ACCEPTED_VALUES}. + * + * @param baudrate the baud rate in bits per second + * @throws DataDictionaryException if the baud rate is not supported or invalid + */ + public void setBaudrate(Number baudrate) throws DataDictionaryException { + this.setBaudrate((Object) baudrate); + } + + /** + * Sets the parity setting for data validation. + * + *

    Parity is used for error detection in serial communication. The parity setting must be one + * of the supported {@link Parity} enum values. + * + * @param parity the parity setting to use + * @throws DataDictionaryException if the parity value is invalid or not supported + */ + public void setParity(Parity parity) throws DataDictionaryException { + this.setParity((Object) parity); + } + + /** + * Sets the number of data bits per frame. + * + *

    Data bits determine how many bits are used to represent each character in the serial + * communication. Must be one of: {@link #DATA_BITS_ACCEPTED_VALUES}. + * + * @param dataBits the number of data bits to use + * @throws DataDictionaryException if the data bits value is not supported or invalid + */ + public void setDataBits(Number dataBits) throws DataDictionaryException { + this.setDataBits((Object) dataBits); + } + + /** + * Sets the number of stop bits. + * + *

    Stop bits are used to signal the end of a data frame in serial communication. Must be one + * of: {@link #STOP_BITS_ACCEPTED_VALUES}. + * + * @param stopBits the number of stop bits to use + * @throws DataDictionaryException if the stop bits value is not supported or invalid + */ + public void setStopBits(Number stopBits) throws DataDictionaryException { + this.setStopBits((Object) stopBits); + } + + /** + * Sets the timeout for synchronous read operations. + * + *

    This timeout determines how long the system will wait for data to be available on the serial + * port before timing out the read operation. If not set, defaults to {@link + * #SYNC_READ_TIMEOUT_DEFAULT_VALUE}. + * + * @param syncReadTimeout the timeout value in milliseconds + * @throws DataDictionaryException if the timeout value is invalid or negative + */ + public void setSyncReadTimeout(Number syncReadTimeout) throws DataDictionaryException { + this.setSyncReadTimeout((Object) syncReadTimeout); + } + + /** + * Internal method to set the port ID with object conversion. + * + * @param portId the port ID object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setPortId(Object portId) throws DataDictionaryException { + this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); + } + + /** + * Internal method to set the port name with object conversion. + * + * @param portName the port name object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setPortName(Object portName) throws DataDictionaryException { + this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); + } + + /** + * Internal method to set the baud rate with object conversion. + * + * @param baudrate the baud rate object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setBaudrate(Object baudrate) throws DataDictionaryException { + this.data.put(KEY_BAUDRATE, this.kBaudrate.convert(baudrate)); + } + + /** + * Internal method to set the parity with object conversion. + * + * @param parity the parity object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setParity(Object parity) throws DataDictionaryException { + this.data.put(KEY_PARITY, this.kParity.convert(parity)); + } + + /** + * Internal method to set the data bits with object conversion. + * + * @param dataBits the data bits object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setDataBits(Object dataBits) throws DataDictionaryException { + this.data.put(KEY_DATA_BITS, this.kDataBits.convert(dataBits)); + } + + /** + * Internal method to set the stop bits with object conversion. + * + * @param stopBits the stop bits object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setStopBits(Object stopBits) throws DataDictionaryException { + this.data.put(KEY_STOP_BITS, this.kStopBits.convert(stopBits)); + } + + /** + * Internal method to set the sync read timeout with object conversion. + * + * @param syncReadTimeout the sync read timeout object to convert and set + * @throws DataDictionaryException if the conversion fails + */ + protected void setSyncReadTimeout(Object syncReadTimeout) throws DataDictionaryException { + this.data.put(KEY_SYNC_READ_TIMEOUT, this.kSyncReadTimeout.convert(syncReadTimeout)); + } + + /** + * Initializes and configures all key descriptors for parameter validation. + * + *

    This method sets up the validation rules for all serial port configuration parameters, + * including accepted values and required/optional status. + * + * @throws RuntimeException if there is an error during key descriptor initialization + */ + private void loadKeyDescriptors() { + try { + this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); + this.keys.add(this.kPortId); + + this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); + this.keys.add(this.kPortName); + + this.kBaudrate = new KeyDescriptorNumber(KEY_BAUDRATE, true); + this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); + this.keys.add(this.kBaudrate); + + this.kParity = new KeyDescriptorEnum(KEY_PARITY, true, Parity.class); + this.keys.add(this.kParity); + + this.kDataBits = new KeyDescriptorNumber(KEY_DATA_BITS, true); + this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUES); + this.keys.add(this.kDataBits); + + this.kStopBits = new KeyDescriptorNumber(KEY_STOP_BITS, true); + this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUES); + this.keys.add(this.kStopBits); + + this.kSyncReadTimeout = new KeyDescriptorNumber(KEY_SYNC_READ_TIMEOUT, false); + this.keys.add(this.kSyncReadTimeout); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/channels/serialport/Parity.java b/src/main/java/enedis/lab/io/channels/serialport/Parity.java index 4872cfe..7b01874 100644 --- a/src/main/java/enedis/lab/io/channels/serialport/Parity.java +++ b/src/main/java/enedis/lab/io/channels/serialport/Parity.java @@ -1,65 +1,64 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -/** - * Enumeration representing parity settings for serial communication. - *

    - * Parity is used for error detection in serial communication by adding an extra - * bit to each data frame. The parity bit is calculated based on the number of - * 1-bits in the data and the selected parity type. - *

    - * This enum is used in {@link ChannelSerialPortConfiguration} to specify - * the parity setting for serial port communication channels. - * - * @author Enedis Smarties team - * @see ChannelSerialPortConfiguration - */ -public enum Parity { - - /** - * No parity bit is added to the data frame. - *

    - * This setting disables parity checking, which means no error detection - * is performed on the data bits. - */ - NONE, - - /** - * Even parity is used for error detection. - *

    - * The parity bit is set to 1 if the number of 1-bits in the data is odd, - * ensuring the total number of 1-bits (including parity) is even. - */ - EVEN, - - /** - * Odd parity is used for error detection. - *

    - * The parity bit is set to 1 if the number of 1-bits in the data is even, - * ensuring the total number of 1-bits (including parity) is odd. - */ - ODD, - - /** - * Mark parity is used for error detection. - *

    - * The parity bit is always set to 1, regardless of the data content. - * This is typically used for special communication protocols. - */ - MARK, - - /** - * Space parity is used for error detection. - *

    - * The parity bit is always set to 0, regardless of the data content. - * This is typically used for special communication protocols. - */ - SPACE - -} \ No newline at end of file +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels.serialport; + +/** + * Enumeration representing parity settings for serial communication. + * + *

    Parity is used for error detection in serial communication by adding an extra bit to each data + * frame. The parity bit is calculated based on the number of 1-bits in the data and the selected + * parity type. + * + *

    This enum is used in {@link ChannelSerialPortConfiguration} to specify the parity setting for + * serial port communication channels. + * + * @author Enedis Smarties team + * @see ChannelSerialPortConfiguration + */ +public enum Parity { + + /** + * No parity bit is added to the data frame. + * + *

    This setting disables parity checking, which means no error detection is performed on the + * data bits. + */ + NONE, + + /** + * Even parity is used for error detection. + * + *

    The parity bit is set to 1 if the number of 1-bits in the data is odd, ensuring the total + * number of 1-bits (including parity) is even. + */ + EVEN, + + /** + * Odd parity is used for error detection. + * + *

    The parity bit is set to 1 if the number of 1-bits in the data is even, ensuring the total + * number of 1-bits (including parity) is odd. + */ + ODD, + + /** + * Mark parity is used for error detection. + * + *

    The parity bit is always set to 1, regardless of the data content. This is typically used + * for special communication protocols. + */ + MARK, + + /** + * Space parity is used for error detection. + * + *

    The parity bit is always set to 0, regardless of the data content. This is typically used + * for special communication protocols. + */ + SPACE +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataInputStream.java b/src/main/java/enedis/lab/io/datastreams/DataInputStream.java index 430d8bd..7fb9e44 100644 --- a/src/main/java/enedis/lab/io/datastreams/DataInputStream.java +++ b/src/main/java/enedis/lab/io/datastreams/DataInputStream.java @@ -1,99 +1,75 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.types.DataDictionary; - -/** - * Data input stream - */ -public abstract class DataInputStream extends DataStreamBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor using configuration - * - * @param configuration - * @throws DataStreamException - */ - public DataInputStream(DataStreamConfiguration configuration) throws DataStreamException - { - super(configuration); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataStream - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public final void write(DataDictionary data) throws DataStreamException - { - DataStreamException.raiseOperationDenied("write"); - } - - @Override - public void setup(DataStreamConfiguration configuration) throws DataStreamException - { - super.setup(configuration); - - if (DataStreamDirection.INPUT != configuration.getDirection()) - { - DataStreamException.raiseInvalidConfiguration(DataStreamConfiguration.KEY_DIRECTION + "=" + configuration.getDirection().name() + "(expected " + DataStreamDirection.INPUT.name() + ")"); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +import enedis.lab.types.DataDictionary; + +/** + * Abstract base class for data input streams. + * + *

    This class provides the foundation for implementing input-only data streams that can read data + * from various sources. It extends {@link DataStreamBase} and enforces input-only operations by + * preventing write operations and validating that the stream direction is set to INPUT. + * + *

    Concrete implementations should override the read methods to provide specific input + * functionality for different data sources (files, network connections, serial ports, etc.). + * + * @author Enedis Smarties team + * @see DataStreamBase + * @see DataStreamConfiguration + * @see DataStreamDirection + */ +public abstract class DataInputStream extends DataStreamBase { + + /** + * Constructs a new DataInputStream with the specified configuration. + * + *

    The configuration must specify INPUT as the direction for this stream to be valid. + * + * @param configuration the configuration for this data input stream + * @throws DataStreamException if the configuration is invalid + */ + public DataInputStream(DataStreamConfiguration configuration) throws DataStreamException { + super(configuration); + } + + /** + * This method is overridden to prevent write operations on input streams. Calling this method + * will always throw a DataStreamException with an "operation denied" message. + * + * @param data the data dictionary to write (ignored) + * @throws DataStreamException always thrown with "operation denied" message + */ + @Override + public final void write(DataDictionary data) throws DataStreamException { + DataStreamException.raiseOperationDenied("write"); + } + + /** + * This method validates that the configuration direction is set to INPUT. If the direction is not + * INPUT, a DataStreamException is thrown with details about the expected and actual direction + * values. + * + * @param configuration the configuration to validate and apply + * @throws DataStreamException if the direction is not INPUT + */ + @Override + public void setup(DataStreamConfiguration configuration) throws DataStreamException { + super.setup(configuration); + + if (DataStreamDirection.INPUT != configuration.getDirection()) { + DataStreamException.raiseInvalidConfiguration( + DataStreamConfiguration.KEY_DIRECTION + + "=" + + configuration.getDirection().name() + + "(expected " + + DataStreamDirection.INPUT.name() + + ")"); + } + } +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStream.java b/src/main/java/enedis/lab/io/datastreams/DataStream.java index fb89041..7fc460b 100644 --- a/src/main/java/enedis/lab/io/datastreams/DataStream.java +++ b/src/main/java/enedis/lab/io/datastreams/DataStream.java @@ -1,38 +1,64 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * DataStream interface - */ -public interface DataStream extends DataStreamUser -{ - /** - * Open the stream - * - * @throws DataStreamException - */ - public void open() throws DataStreamException; - - /** - * Close the stream - * - * @throws DataStreamException - */ - public void close() throws DataStreamException; - - /** - * Setup the stream from a configuration - * - * @param configuration - * configuration given for the stream - * @throws DataStreamException - * if an error occurs - */ - public void setup(DataStreamConfiguration configuration) throws DataStreamException; -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +/** + * Interface defining the lifecycle management operations for data streams. + * + *

    This interface extends {@link DataStreamUser} and provides the essential lifecycle methods for + * managing data streams. It defines the core operations that all data stream implementations must + * support: opening, closing, and configuration setup. + * + * @author Enedis Smarties team + * @see DataStreamException + */ +public interface DataStream extends DataStreamUser { + /** + * Opens the data stream and prepares it for data transmission. + * + *

    This method initializes the underlying communication channel and makes the stream ready to + * handle data operations. The specific implementation depends on the stream type and + * configuration (file, network, serial, etc.). + * + *

    After a successful open operation, the stream should be ready to perform read/write + * operations as defined by the stream direction. + * + * @throws DataStreamException if the stream cannot be opened due to configuration errors, + * resource unavailability, or communication failures + */ + public void open() throws DataStreamException; + + /** + * Closes the data stream and releases associated resources. + * + *

    This method performs cleanup operations and releases any system resources (file handles, + * network connections, serial ports, etc.) that were acquired during the stream's operation. + * + *

    After closing, the stream should not be used for further operations unless it is reopened. + * Multiple close operations should be safe to call. + * + * @throws DataStreamException if an error occurs during the cleanup process + */ + public void close() throws DataStreamException; + + /** + * Configures the data stream with the specified configuration parameters. + * + *

    This method applies the given configuration to the stream, setting up parameters such as + * direction (input/output), protocol type, connection details, and other stream-specific + * settings. + * + *

    The configuration must be valid for the specific stream implementation. Invalid + * configurations will result in a DataStreamException being thrown. + * + * @param configuration the configuration parameters for this data stream + * @throws DataStreamException if the configuration is invalid, incompatible with the stream type, + * or if an error occurs during setup + */ + public void setup(DataStreamConfiguration configuration) throws DataStreamException; +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java b/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java index 578325f..8bb9b3f 100644 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java @@ -1,373 +1,386 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import java.util.Collection; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.io.channels.Channel; -import enedis.lab.io.channels.ChannelListener; -import enedis.lab.io.channels.ChannelStatus; -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.NotifierBase; - -/** - * DataStream Base - */ -public abstract class DataStreamBase implements DataStream, ChannelListener -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected NotifierBase notifier; - protected DataStreamConfiguration configuration; - private DataStreamStatus status; - protected Channel channel; - protected Logger logger; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param configuration - * configuration used to set the stream - * @throws DataStreamException - */ - public DataStreamBase(DataStreamConfiguration configuration) throws DataStreamException - { - this.init(configuration); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ChannelListener - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onStatusChanged(String channelName, ChannelStatus channelStatus) - { - DataStreamStatus status_cpy = this.status; - - switch (channelStatus) - { - case STOPPED: - { - if (DataStreamStatus.CLOSED != this.status) - { - this.setStatus(DataStreamStatus.ERROR); - } - break; - } - case STARTED: - { - if (DataStreamStatus.ERROR == this.status) - { - this.setStatus(DataStreamStatus.OPENED); - } - break; - } - case ERROR: - { - this.setStatus(DataStreamStatus.ERROR); - break; - } - - default: - { - - } - } - - if (!this.notifier.getSubscribers().isEmpty() && (status_cpy != this.status)) - { - this.notifyOnStatusChanged(this.status); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataStream - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void open() throws DataStreamException - { - String channelName = this.configuration.getChannelName(); - - if (null != channelName) - { - if (null != this.channel) - { - this.channel.subscribe(this); - this.setStatus(DataStreamStatus.OPENED); - this.logger.info("Stream " + this.getName() + " start"); - } - else - { - this.setStatus(DataStreamStatus.ERROR); - DataStreamException.raiseChannelError(channelName, "channel does not exist"); - } - } - } - - @Override - public void close() throws DataStreamException - { - String channelName = this.configuration.getChannelName(); - - if (null != channelName) - { - if (null != this.channel) - { - this.channel.unsubscribe(this); - this.setStatus(DataStreamStatus.CLOSED); - this.logger.info("Service " + this.getName() + " stop"); - } - else - { - this.setStatus(DataStreamStatus.ERROR); - DataStreamException.raiseChannelError(channelName, "channel does not exist"); - } - } - } - - @Override - public void setup(DataStreamConfiguration configuration) throws DataStreamException - { - if (this.status == DataStreamStatus.OPENED) - { - DataStreamException.raiseInternalError("Cannot setup stream " + this.getName() + " (already open)"); - } - this.configuration = configuration; - } - - @Override - public void subscribe(DataStreamListener listener) - { - this.notifier.subscribe(listener); - } - - @Override - public void unsubscribe(DataStreamListener listener) - { - this.notifier.unsubscribe(listener); - } - - @Override - public boolean hasSubscriber(DataStreamListener subscriber) - { - return this.notifier.hasSubscriber(subscriber); - } - - @Override - public Collection getSubscribers() - { - return this.notifier.getSubscribers(); - } - - @Override - public String getName() - { - return this.configuration.getName(); - } - - @Override - public DataStreamConfiguration getConfiguration() - { - return (DataStreamConfiguration) this.configuration.clone(); - } - - @Override - public DataStreamDirection getDirection() - { - return this.configuration.getDirection(); - } - - @Override - public DataStreamType getType() - { - return this.configuration.getType(); - } - - @Override - public DataStreamStatus getStatus() - { - return this.status; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Set the channel reference - * - * @param channel - */ - public void setChannel(Channel channel) - { - this.channel = channel; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Notify onDataReceived listeners - * - * @param data - * new data received - */ - protected void notifyOnDataReceived(DataDictionary data) - { - if (data != null) - { - for (DataStreamListener listener : this.notifier.getSubscribers()) - { - listener.onDataReceived(this.getName(), data); - } - } - } - - /** - * Notify onDataSent listeners - * - * @param data - * new data sent - */ - protected void notifyOnDataSent(DataDictionary data) - { - if (data != null) - { - for (DataStreamListener listener : this.notifier.getSubscribers()) - { - listener.onDataSent(this.getName(), data); - } - } - } - - /** - * Notify onStatusChanged listeners - * - * @param newStatus - * new status - */ - protected void notifyOnStatusChanged(DataStreamStatus newStatus) - { - if (newStatus != null) - { - for (DataStreamListener listener : this.notifier.getSubscribers()) - { - listener.onStatusChanged(this.getName(), newStatus); - } - } - } - - /** - * Notify onErrorDetected listeners - * - * @param errorCode - * the error code - * @param errorMessage - * the error message - * @param data - * the data associated with the error - */ - protected void notifyOnErrorDetected(int errorCode, String errorMessage, DataDictionary data) - { - for (DataStreamListener listener : this.notifier.getSubscribers()) - { - listener.onErrorDetected(this.getName(), errorCode, errorMessage, data); - } - } - - /** - * Set the channel status - * - * @param status - * channel status - */ - protected void setStatus(DataStreamStatus status) - { - this.setStatus(status, true); - } - - /** - * Set the channel status. If 'notify' argument is false, no channel listener will be notified about a new status, - * otherwise, the will - * - * @param status - * channel status - * @param notify - * if 'true', the channel's listener will be notified, else it will not - */ - protected void setStatus(DataStreamStatus status, boolean notify) - { - if (status != this.status) - { - this.status = status; - - if (true == notify) - { - this.notifyOnStatusChanged(this.status); - } - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void init(DataStreamConfiguration configuration) throws DataStreamException - { - this.notifier = new NotifierBase(); - this.status = DataStreamStatus.UNKNOWN; - this.configuration = configuration; - this.logger = LogManager.getLogger(this.getClass()); - this.setup(configuration); - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +import enedis.lab.io.channels.Channel; +import enedis.lab.io.channels.ChannelListener; +import enedis.lab.io.channels.ChannelStatus; +import enedis.lab.types.DataDictionary; +import enedis.lab.util.task.NotifierBase; +import java.util.Collection; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Abstract base class providing common functionality for data stream implementations. + * + *

    This class implements both {@link DataStream} and {@link ChannelListener} interfaces, + * providing a foundation for concrete data stream implementations. It manages the stream lifecycle, + * status tracking, and event notification to registered listeners. + * + *

    The class handles channel status changes and automatically updates the stream status + * accordingly. It also provides notification mechanisms for data events and status changes to + * subscribed listeners. + * + * @author Enedis Smarties team + * @see DataStream + * @see ChannelListener + * @see DataStreamListener + * @see DataStreamConfiguration + * @see DataStreamStatus + */ +public abstract class DataStreamBase implements DataStream, ChannelListener { + protected NotifierBase notifier; + protected DataStreamConfiguration configuration; + private DataStreamStatus status; + protected Channel channel; + protected Logger logger; + + /** + * Constructs a new DataStreamBase with the specified configuration. + * + *

    The constructor initializes the stream with the given configuration, sets up the notifier + * for event handling, and configures logging. The stream status is initially set to UNKNOWN. + * + * @param configuration the configuration used to set up the stream + * @throws DataStreamException if the configuration is invalid or setup fails + */ + public DataStreamBase(DataStreamConfiguration configuration) throws DataStreamException { + this.init(configuration); + } + + /** + * Handles channel status changes and updates the stream status accordingly. + * + *

    This method is called when the associated channel's status changes. It maps channel statuses + * to appropriate stream statuses and notifies listeners if the status has changed. + * + * @param channelName the name of the channel that changed status + * @param channelStatus the new status of the channel + */ + @Override + public void onStatusChanged(String channelName, ChannelStatus channelStatus) { + DataStreamStatus status_cpy = this.status; + + switch (channelStatus) { + case STOPPED: + { + if (DataStreamStatus.CLOSED != this.status) { + this.setStatus(DataStreamStatus.ERROR); + } + break; + } + case STARTED: + { + if (DataStreamStatus.ERROR == this.status) { + this.setStatus(DataStreamStatus.OPENED); + } + break; + } + case ERROR: + { + this.setStatus(DataStreamStatus.ERROR); + break; + } + + default: + { + } + } + + if (!this.notifier.getSubscribers().isEmpty() && (status_cpy != this.status)) { + this.notifyOnStatusChanged(this.status); + } + } + + /** + * Opens the data stream and subscribes to the associated channel. + * + *

    This method attempts to open the stream by subscribing to the channel specified in the + * configuration. If the channel exists, the stream status is set to OPENED. If the channel does + * not exist, an error is raised and the status is set to ERROR. + * + * @throws DataStreamException if the channel does not exist or if an error occurs during the + * subscription process + */ + @Override + public void open() throws DataStreamException { + String channelName = this.configuration.getChannelName(); + + if (null != channelName) { + if (null != this.channel) { + this.channel.subscribe(this); + this.setStatus(DataStreamStatus.OPENED); + this.logger.info("Stream " + this.getName() + " start"); + } else { + this.setStatus(DataStreamStatus.ERROR); + DataStreamException.raiseChannelError(channelName, "channel does not exist"); + } + } + } + + /** + * Closes the data stream and unsubscribes from the associated channel. + * + *

    This method closes the stream by unsubscribing from the channel and setting the status to + * CLOSED. If the channel does not exist, an error is raised and the status is set to ERROR. + * + * @throws DataStreamException if the channel does not exist or if an error occurs during the + * unsubscription process + */ + @Override + public void close() throws DataStreamException { + String channelName = this.configuration.getChannelName(); + + if (null != channelName) { + if (null != this.channel) { + this.channel.unsubscribe(this); + this.setStatus(DataStreamStatus.CLOSED); + this.logger.info("Service " + this.getName() + " stop"); + } else { + this.setStatus(DataStreamStatus.ERROR); + DataStreamException.raiseChannelError(channelName, "channel does not exist"); + } + } + } + + /** + * Sets up the data stream with the specified configuration. + * + *

    This method updates the stream configuration. It cannot be called when the stream is already + * open, as this would be an invalid operation. + * + * @param configuration the new configuration for the stream + * @throws DataStreamException if the stream is already open or if the configuration is invalid + */ + @Override + public void setup(DataStreamConfiguration configuration) throws DataStreamException { + if (this.status == DataStreamStatus.OPENED) { + DataStreamException.raiseInternalError( + "Cannot setup stream " + this.getName() + " (already open)"); + } + this.configuration = configuration; + } + + /** + * Subscribes a listener to receive data stream events. + * + * @param listener the listener to subscribe to stream events + */ + @Override + public void subscribe(DataStreamListener listener) { + this.notifier.subscribe(listener); + } + + /** + * Unsubscribes a listener from receiving data stream events. + * + * @param listener the listener to unsubscribe from stream events + */ + @Override + public void unsubscribe(DataStreamListener listener) { + this.notifier.unsubscribe(listener); + } + + /** + * Checks if a specific listener is subscribed to this stream. + * + * @param subscriber the listener to check + * @return true if the listener is subscribed, false otherwise + */ + @Override + public boolean hasSubscriber(DataStreamListener subscriber) { + return this.notifier.hasSubscriber(subscriber); + } + + /** + * Returns a collection of all subscribed listeners. + * + * @return a collection of all subscribed listeners + */ + @Override + public Collection getSubscribers() { + return this.notifier.getSubscribers(); + } + + /** + * Returns the name of this data stream. + * + * @return the stream name from the configuration + */ + @Override + public String getName() { + return this.configuration.getName(); + } + + /** + * Returns a clone of the current stream configuration. + * + * @return a copy of the stream configuration + */ + @Override + public DataStreamConfiguration getConfiguration() { + return (DataStreamConfiguration) this.configuration.clone(); + } + + /** + * Returns the direction of this data stream. + * + * @return the stream direction from the configuration + */ + @Override + public DataStreamDirection getDirection() { + return this.configuration.getDirection(); + } + + /** + * Returns the type of this data stream. + * + * @return the stream type from the configuration + */ + @Override + public DataStreamType getType() { + return this.configuration.getType(); + } + + /** + * Returns the current status of this data stream. + * + * @return the current stream status + */ + @Override + public DataStreamStatus getStatus() { + return this.status; + } + + /** + * Sets the channel reference for this data stream. + * + *

    This method associates a channel with the stream, enabling communication through the + * specified channel. The channel must be compatible with the stream configuration. + * + * @param channel the channel to associate with this stream + */ + public void setChannel(Channel channel) { + this.channel = channel; + } + + /** + * Notifies all subscribed listeners that new data has been received. + * + *

    This method iterates through all subscribed listeners and calls their onDataReceived method + * with the stream name and the received data. + * + * @param data the new data that was received + */ + protected void notifyOnDataReceived(DataDictionary data) { + if (data != null) { + for (DataStreamListener listener : this.notifier.getSubscribers()) { + listener.onDataReceived(this.getName(), data); + } + } + } + + /** + * Notifies all subscribed listeners that data has been sent. + * + *

    This method iterates through all subscribed listeners and calls their onDataSent method with + * the stream name and the sent data. + * + * @param data the data that was sent + */ + protected void notifyOnDataSent(DataDictionary data) { + if (data != null) { + for (DataStreamListener listener : this.notifier.getSubscribers()) { + listener.onDataSent(this.getName(), data); + } + } + } + + /** + * Notifies all subscribed listeners that the stream status has changed. + * + *

    This method iterates through all subscribed listeners and calls their onStatusChanged method + * with the stream name and the new status. + * + * @param newStatus the new status of the stream + */ + protected void notifyOnStatusChanged(DataStreamStatus newStatus) { + if (newStatus != null) { + for (DataStreamListener listener : this.notifier.getSubscribers()) { + listener.onStatusChanged(this.getName(), newStatus); + } + } + } + + /** + * Notifies all subscribed listeners that an error has been detected. + * + *

    This method iterates through all subscribed listeners and calls their onErrorDetected method + * with the stream name, error details, and associated data. + * + * @param errorCode the error code identifying the type of error + * @param errorMessage a descriptive message about the error + * @param data the data associated with the error, if any + */ + protected void notifyOnErrorDetected(int errorCode, String errorMessage, DataDictionary data) { + for (DataStreamListener listener : this.notifier.getSubscribers()) { + listener.onErrorDetected(this.getName(), errorCode, errorMessage, data); + } + } + + /** + * Sets the stream status and notifies listeners of the change. + * + *

    This is a convenience method that calls setStatus(status, true) to update the status and + * notify all listeners. + * + * @param status the new status for the stream + */ + protected void setStatus(DataStreamStatus status) { + this.setStatus(status, true); + } + + /** + * Sets the stream status with optional listener notification. + * + *

    This method updates the internal status and optionally notifies all subscribed listeners of + * the status change. The notification only occurs if the status actually changes and the notify + * parameter is true. + * + * @param status the new status for the stream + * @param notify if true, listeners will be notified of the status change + */ + protected void setStatus(DataStreamStatus status, boolean notify) { + if (status != this.status) { + this.status = status; + + if (true == notify) { + this.notifyOnStatusChanged(this.status); + } + } + } + + /** + * Initializes the data stream with the specified configuration. + * + *

    This private method sets up the internal components of the stream, including the notifier, + * initial status, configuration, and logger. It also calls the setup method to apply the + * configuration. + * + * @param configuration the configuration to initialize the stream with + * @throws DataStreamException if the initialization fails + */ + private void init(DataStreamConfiguration configuration) throws DataStreamException { + this.notifier = new NotifierBase(); + this.status = DataStreamStatus.UNKNOWN; + this.configuration = configuration; + this.logger = LogManager.getLogger(this.getClass()); + this.setup(configuration); + } +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java b/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java index ddd6069..77a8856 100644 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java @@ -1,296 +1,303 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.configuration.ConfigurationBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * DataStreamConfiguration class - * - * Generated - */ -public class DataStreamConfiguration extends ConfigurationBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_NAME = "name"; - protected static final String KEY_TYPE = "type"; - protected static final String KEY_DIRECTION = "direction"; - protected static final String KEY_CHANNEL_NAME = "channelName"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kName; - protected KeyDescriptorEnum kType; - protected KeyDescriptorEnum kDirection; - protected KeyDescriptorString kChannelName; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected DataStreamConfiguration() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public DataStreamConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public DataStreamConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public DataStreamConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param type - * @param direction - * @param channelName - * @throws DataDictionaryException - */ - public DataStreamConfiguration(String name, DataStreamType type, DataStreamDirection direction, String channelName) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setType(type); - this.setDirection(direction); - this.setChannelName(channelName); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ConfigurationBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get name - * - * @return the name - */ - public String getName() - { - return (String) this.data.get(KEY_NAME); - } - - /** - * Get type - * - * @return the type - */ - public DataStreamType getType() - { - return (DataStreamType) this.data.get(KEY_TYPE); - } - - /** - * Get direction - * - * @return the direction - */ - public DataStreamDirection getDirection() - { - return (DataStreamDirection) this.data.get(KEY_DIRECTION); - } - - /** - * Get channel name - * - * @return the channel name - */ - public String getChannelName() - { - return (String) this.data.get(KEY_CHANNEL_NAME); - } - - /** - * Set name - * - * @param name - * @throws DataDictionaryException - */ - public void setName(String name) throws DataDictionaryException - { - this.setName((Object) name); - } - - /** - * Set type - * - * @param type - * @throws DataDictionaryException - */ - public void setType(DataStreamType type) throws DataDictionaryException - { - this.setType((Object) type); - } - - /** - * Set direction - * - * @param direction - * @throws DataDictionaryException - */ - public void setDirection(DataStreamDirection direction) throws DataDictionaryException - { - this.setDirection((Object) direction); - } - - /** - * Set channel name - * - * @param channelName - * @throws DataDictionaryException - */ - public void setChannelName(String channelName) throws DataDictionaryException - { - this.setChannelName((Object) channelName); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setName(Object name) throws DataDictionaryException - { - this.data.put(KEY_NAME, this.kName.convert(name)); - } - - protected void setType(Object type) throws DataDictionaryException - { - this.data.put(KEY_TYPE, this.kType.convert(type)); - } - - protected void setDirection(Object direction) throws DataDictionaryException - { - this.data.put(KEY_DIRECTION, this.kDirection.convert(direction)); - } - - protected void setChannelName(Object channelName) throws DataDictionaryException - { - this.data.put(KEY_CHANNEL_NAME, this.kChannelName.convert(channelName)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kName = new KeyDescriptorString(KEY_NAME, true, false); - this.keys.add(this.kName); - - this.kType = new KeyDescriptorEnum(KEY_TYPE, true, DataStreamType.class); - this.keys.add(this.kType); - - this.kDirection = new KeyDescriptorEnum(KEY_DIRECTION, true, DataStreamDirection.class); - this.keys.add(this.kDirection); - - this.kChannelName = new KeyDescriptorString(KEY_CHANNEL_NAME, false, false); - this.keys.add(this.kChannelName); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.configuration.ConfigurationBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import enedis.lab.types.datadictionary.KeyDescriptorString; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Configuration class for data streams. + * + *

    This class extends {@link ConfigurationBase} and provides configuration management for data + * streams. It defines the essential parameters needed to configure a data stream, including name, + * type, direction, and associated channel name. + * + *

    The configuration supports multiple construction methods: from maps, data dictionaries, files, + * or direct parameter specification. It includes validation and type conversion capabilities + * through the key descriptor system. + * + * @author Enedis Smarties team + * @see ConfigurationBase + * @see DataStreamType + * @see DataStreamDirection + * @see DataDictionaryException + */ +public class DataStreamConfiguration extends ConfigurationBase { + protected static final String KEY_NAME = "name"; + protected static final String KEY_TYPE = "type"; + protected static final String KEY_DIRECTION = "direction"; + protected static final String KEY_CHANNEL_NAME = "channelName"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorString kName; + protected KeyDescriptorEnum kType; + protected KeyDescriptorEnum kDirection; + protected KeyDescriptorString kChannelName; + + /** + * Protected default constructor for internal use. + * + *

    This constructor initializes the configuration with default values and loads the key + * descriptors for validation and type conversion. + */ + protected DataStreamConfiguration() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructs a DataStreamConfiguration from a map of key-value pairs. + * + *

    This constructor creates a configuration by copying values from the provided map. The map + * should contain keys corresponding to the configuration parameters (name, type, direction, + * channelName). + * + * @param map the map containing configuration parameters + * @throws DataDictionaryException if the map contains invalid values or missing required keys + */ + public DataStreamConfiguration(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructs a DataStreamConfiguration by copying from another DataDictionary. + * + *

    This constructor creates a new configuration by copying all values from the provided + * DataDictionary. This is useful for creating configurations from existing data structures. + * + * @param other the DataDictionary to copy configuration from + * @throws DataDictionaryException if the source dictionary contains invalid values + */ + public DataStreamConfiguration(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructs a DataStreamConfiguration with a name and configuration file. + * + *

    This constructor initializes the configuration with a specific name and loads additional + * parameters from the specified file. The file should contain valid configuration data that can + * be parsed and applied to this configuration. + * + * @param name the configuration name + * @param file the configuration file to load parameters from + */ + public DataStreamConfiguration(String name, File file) { + this(); + this.init(name, file); + } + + /** + * Constructs a DataStreamConfiguration with specific parameter values. + * + *

    This constructor creates a configuration with all parameters explicitly set. It validates + * the parameters and updates the configuration accordingly. + * + * @param name the name of the data stream + * @param type the type of the data stream + * @param direction the direction of the data stream + * @param channelName the name of the associated channel + * @throws DataDictionaryException if any of the parameters are invalid + */ + public DataStreamConfiguration( + String name, DataStreamType type, DataStreamDirection direction, String channelName) + throws DataDictionaryException { + this(); + + this.setName(name); + this.setType(type); + this.setDirection(direction); + this.setChannelName(channelName); + + this.checkAndUpdate(); + } + + /** + * Updates optional parameters in the configuration. + * + *

    This method is called during configuration setup to update any optional parameters that may + * depend on other configuration values. + * + * @throws DataDictionaryException if the parameter update fails + */ + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + super.updateOptionalParameters(); + } + + /** + * Returns the name of the data stream. + * + * @return the name of the data stream + */ + public String getName() { + return (String) this.data.get(KEY_NAME); + } + + /** + * Returns the type of the data stream. + * + * @return the type of the data stream + */ + public DataStreamType getType() { + return (DataStreamType) this.data.get(KEY_TYPE); + } + + /** + * Returns the direction of the data stream. + * + * @return the direction of the data stream + */ + public DataStreamDirection getDirection() { + return (DataStreamDirection) this.data.get(KEY_DIRECTION); + } + + /** + * Returns the name of the associated channel. + * + * @return the name of the channel associated with this data stream + */ + public String getChannelName() { + return (String) this.data.get(KEY_CHANNEL_NAME); + } + + /** + * Sets the name of the data stream. + * + * @param name the name to set for the data stream + * @throws DataDictionaryException if the name is invalid or null + */ + public void setName(String name) throws DataDictionaryException { + this.setName((Object) name); + } + + /** + * Sets the type of the data stream. + * + * @param type the type to set for the data stream + * @throws DataDictionaryException if the type is invalid + */ + public void setType(DataStreamType type) throws DataDictionaryException { + this.setType((Object) type); + } + + /** + * Sets the direction of the data stream. + * + * @param direction the direction to set for the data stream + * @throws DataDictionaryException if the direction is invalid + */ + public void setDirection(DataStreamDirection direction) throws DataDictionaryException { + this.setDirection((Object) direction); + } + + /** + * Sets the name of the associated channel. + * + * @param channelName the name of the channel to associate with this data stream + * @throws DataDictionaryException if the channel name is invalid + */ + public void setChannelName(String channelName) throws DataDictionaryException { + this.setChannelName((Object) channelName); + } + + /** + * Sets the name using object conversion and validation. + * + *

    This protected method performs the actual name setting with type conversion and validation + * through the key descriptor system. + * + * @param name the name object to convert and set + * @throws DataDictionaryException if the name conversion or validation fails + */ + protected void setName(Object name) throws DataDictionaryException { + this.data.put(KEY_NAME, this.kName.convert(name)); + } + + /** + * Sets the type using object conversion and validation. + * + *

    This protected method performs the actual type setting with enum conversion and validation + * through the key descriptor system. + * + * @param type the type object to convert and set + * @throws DataDictionaryException if the type conversion or validation fails + */ + protected void setType(Object type) throws DataDictionaryException { + this.data.put(KEY_TYPE, this.kType.convert(type)); + } + + /** + * Sets the direction using object conversion and validation. + * + *

    This protected method performs the actual direction setting with enum conversion and + * validation through the key descriptor system. + * + * @param direction the direction object to convert and set + * @throws DataDictionaryException if the direction conversion or validation fails + */ + protected void setDirection(Object direction) throws DataDictionaryException { + this.data.put(KEY_DIRECTION, this.kDirection.convert(direction)); + } + + /** + * Sets the channel name using object conversion and validation. + * + *

    This protected method performs the actual channel name setting with string conversion and + * validation through the key descriptor system. + * + * @param channelName the channel name object to convert and set + * @throws DataDictionaryException if the channel name conversion or validation fails + */ + protected void setChannelName(Object channelName) throws DataDictionaryException { + this.data.put(KEY_CHANNEL_NAME, this.kChannelName.convert(channelName)); + } + + /** + * Loads and initializes the key descriptors for configuration validation. + * + *

    This private method sets up the key descriptors that define the structure and validation + * rules for the configuration parameters. It creates descriptors for name, type, direction, and + * channel name with appropriate validation rules. + * + *

    If any descriptor creation fails, a RuntimeException is thrown with the original exception + * as the cause. + */ + private void loadKeyDescriptors() { + try { + this.kName = new KeyDescriptorString(KEY_NAME, true, false); + this.keys.add(this.kName); + + this.kType = new KeyDescriptorEnum(KEY_TYPE, true, DataStreamType.class); + this.keys.add(this.kType); + + this.kDirection = + new KeyDescriptorEnum( + KEY_DIRECTION, true, DataStreamDirection.class); + this.keys.add(this.kDirection); + + this.kChannelName = new KeyDescriptorString(KEY_CHANNEL_NAME, false, false); + this.keys.add(this.kChannelName); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java b/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java index 66e83e7..4305ebf 100644 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java @@ -1,21 +1,39 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * DataStream direction - */ -public enum DataStreamDirection -{ - /** Input dataStream */ - INPUT, - /** Output dataStream */ - OUTPUT, - /** Inoutput dataStream */ - INOUTPUT; -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +/** + * Enumeration representing the data flow direction of a stream. + * + *

    This enum defines the possible directions for data transmission in a data stream, determining + * whether the stream handles incoming data, outgoing data, or both. The direction is a fundamental + * characteristic of a stream that affects its behavior and the operations it supports. + * + * @author Enedis Smarties team + * @see DataStreamConfiguration + * @see DataStream + */ +public enum DataStreamDirection { + /** + * Input direction for streams that receive data. Streams with this direction are configured to + * read incoming data from a source. + */ + INPUT, + + /** + * Output direction for streams that send data. Streams with this direction are configured to + * write outgoing data to a destination. + */ + OUTPUT, + + /** + * Bidirectional streams that can both receive and send data. Streams with this direction support + * both read and write operations, allowing two-way communication. + */ + INOUTPUT; +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamException.java b/src/main/java/enedis/lab/io/datastreams/DataStreamException.java index 1410e0f..dd57d46 100644 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamException.java +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamException.java @@ -1,205 +1,183 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * Specific exception for data stream - */ -public class DataStreamException extends Exception -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = 4079445021346677107L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Raise channel error exception - * - * @param channelName - * @param info - * @throws DataStreamException - */ - public static void raiseChannelError(String channelName, String info) throws DataStreamException - { - throw new DataStreamException("Error occurs on channel " + channelName + " (" + info + ")"); - } - - /** - * Raise unhandle type exception - * - * @param type - * @throws DataStreamException - */ - public static void raiseUnhandledType(String type) throws DataStreamException - { - throw new DataStreamException(type + " datastream is not handled"); - } - - /** - * Raise internal error exception - * - * @param info - * @throws DataStreamException - */ - public static void raiseInternalError(String info) throws DataStreamException - { - throw new DataStreamException(info); - } - - /** - * Raise invalid configuration exception - * - * @param configuration - * @param expected_configuration_name - * @throws DataStreamException - */ - public static void raiseInvalidConfiguration(DataStreamConfiguration configuration, String expected_configuration_name) throws DataStreamException - { - throw new DataStreamException("Configuration " + configuration.getClass().getSimpleName() + " is not valid (" + expected_configuration_name + " expected)"); - } - - /** - * Raise invalid configuration exceotion - * - * @param info - * @throws DataStreamException - */ - public static void raiseInvalidConfiguration(String info) throws DataStreamException - { - throw new DataStreamException(info); - } - - /** - * Raise operation denied excpetion - * - * @param operation - * @throws DataStreamException - */ - public static void raiseOperationDenied(String operation) throws DataStreamException - { - throw new DataStreamException("Operation \'" + operation + "\' is not allowed"); - } - - /** - * Raise unexpected error exception - * - * @param info - * @throws DataStreamException - */ - public static void raiseUnexpectedError(String info) throws DataStreamException - { - throw new DataStreamException("Unexpected error occurs : " + info); - } - - /** - * Raise already exists exception - * - * @param dataStream - * @throws DataStreamException - */ - public static void raiseAlreadyExists(String dataStream) throws DataStreamException - { - throw new DataStreamException("DataStream " + dataStream + " already exists"); - } - - /** - * Raise creation failed exception - * - * @param dataStream - * @param info - * @throws DataStreamException - */ - public static void raiseCreationFailed(String dataStream, String info) throws DataStreamException - { - throw new DataStreamException("Creation of dataStream " + dataStream + " creation failed : " + info); - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Super class constructor - * - * @param message - * @param cause - */ - public DataStreamException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Super class constructor - * - * @param message - */ - public DataStreamException(String message) - { - super(message); - } - - /** - * Super class constructor - * - * @param cause - */ - public DataStreamException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +/** + * Exception class for data stream related errors. + * + *

    This exception is thrown when errors occur during data stream operations such as + * configuration, initialization, opening, closing, reading, or writing. It provides static factory + * methods for creating exceptions with standardized error messages for common error scenarios. + * + *

    The class includes utility methods to raise exceptions for specific error types: channel + * errors, configuration errors, operation denials, and creation failures. + * + * @author Enedis Smarties team + * @see DataStream + * @see DataStreamConfiguration + */ +public class DataStreamException extends Exception { + private static final long serialVersionUID = 4079445021346677107L; + + /** + * Raises a channel error exception. + * + *

    This method creates and throws an exception when an error occurs on a specific channel + * during stream operations. + * + * @param channelName the name of the channel where the error occurred + * @param info additional information about the error + * @throws DataStreamException always thrown with a formatted error message + */ + public static void raiseChannelError(String channelName, String info) throws DataStreamException { + throw new DataStreamException("Error occurs on channel " + channelName + " (" + info + ")"); + } + + /** + * Raises an unhandled type exception. + * + *

    This method creates and throws an exception when a data stream type is not supported or + * handled by the current implementation. + * + * @param type the type of data stream that is not handled + * @throws DataStreamException always thrown with a formatted error message + */ + public static void raiseUnhandledType(String type) throws DataStreamException { + throw new DataStreamException(type + " datastream is not handled"); + } + + /** + * Raises an internal error exception. + * + *

    This method creates and throws an exception for internal errors that occur during stream + * processing or operations. + * + * @param info information describing the internal error + * @throws DataStreamException always thrown with the provided error information + */ + public static void raiseInternalError(String info) throws DataStreamException { + throw new DataStreamException(info); + } + + /** + * Raises an invalid configuration exception with type validation. + * + *

    This method creates and throws an exception when the provided configuration is not of the + * expected type or is incompatible with the stream requirements. + * + * @param configuration the invalid configuration object + * @param expected_configuration_name the name of the expected configuration type + * @throws DataStreamException always thrown with details about the configuration mismatch + */ + public static void raiseInvalidConfiguration( + DataStreamConfiguration configuration, String expected_configuration_name) + throws DataStreamException { + throw new DataStreamException( + "Configuration " + + configuration.getClass().getSimpleName() + + " is not valid (" + + expected_configuration_name + + " expected)"); + } + + /** + * Raises an invalid configuration exception with custom message. + * + *

    This method creates and throws an exception when a configuration is invalid, with a custom + * error message describing the specific validation failure. + * + * @param info information describing why the configuration is invalid + * @throws DataStreamException always thrown with the provided error information + */ + public static void raiseInvalidConfiguration(String info) throws DataStreamException { + throw new DataStreamException(info); + } + + /** + * Raises an operation denied exception. + * + *

    This method creates and throws an exception when an operation is not allowed on the stream, + * such as attempting to write to an input-only stream. + * + * @param operation the name of the operation that was denied + * @throws DataStreamException always thrown with a formatted error message + */ + public static void raiseOperationDenied(String operation) throws DataStreamException { + throw new DataStreamException("Operation \'" + operation + "\' is not allowed"); + } + + /** + * Raises an unexpected error exception. + * + *

    This method creates and throws an exception when an unexpected error occurs during stream + * operations that doesn't fit into other specific error categories. + * + * @param info information describing the unexpected error + * @throws DataStreamException always thrown with a formatted error message + */ + public static void raiseUnexpectedError(String info) throws DataStreamException { + throw new DataStreamException("Unexpected error occurs : " + info); + } + + /** + * Raises an already exists exception. + * + *

    This method creates and throws an exception when attempting to create a data stream that + * already exists in the system. + * + * @param dataStream the name of the data stream that already exists + * @throws DataStreamException always thrown with a formatted error message + */ + public static void raiseAlreadyExists(String dataStream) throws DataStreamException { + throw new DataStreamException("DataStream " + dataStream + " already exists"); + } + + /** + * Raises a creation failed exception. + * + *

    This method creates and throws an exception when the creation of a data stream fails for any + * reason. + * + * @param dataStream the name of the data stream that failed to be created + * @param info additional information about why the creation failed + * @throws DataStreamException always thrown with a formatted error message + */ + public static void raiseCreationFailed(String dataStream, String info) + throws DataStreamException { + throw new DataStreamException( + "Creation of dataStream " + dataStream + " creation failed : " + info); + } + + /** + * Constructs a new DataStreamException with a message and a cause. + * + * @param message the detail message describing the exception + * @param cause the cause of this exception + */ + public DataStreamException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new DataStreamException with a detail message. + * + * @param message the detail message describing the exception + */ + public DataStreamException(String message) { + super(message); + } + + /** + * Constructs a new DataStreamException with a cause. + * + * @param cause the cause of this exception + */ + public DataStreamException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java b/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java index f9094ef..e0aa95b 100644 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java @@ -1,61 +1,87 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.Subscriber; - -/** - * Data stream listener - */ -public interface DataStreamListener extends Subscriber -{ - /** - * Function call when data has been received - * - * @param dataStreamName - * the stream name - * @param data - * the data received - */ - public void onDataReceived(String dataStreamName, DataDictionary data); - - /** - * Function call when data has been sent - * - * @param dataStreamName - * the stream name - * @param data - * the data sent - */ - public void onDataSent(String dataStreamName, DataDictionary data); - - /** - * Function call when data stream status changed - * - * @param dataStreamName - * the stream name - * @param status - * the new status - */ - public void onStatusChanged(String dataStreamName, DataStreamStatus status); - - /** - * On error detected - * - * @param dataStreamName - * the stream name - * @param errorCode - * the error code - * @param errorMessage - * the error message - * @param data - * the data associated with error - */ - public void onErrorDetected(String dataStreamName, int errorCode, String errorMessage, DataDictionary data); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +import enedis.lab.types.DataDictionary; +import enedis.lab.util.task.Subscriber; + +/** + * Listener interface for receiving data stream events and notifications. + * + *

    This interface extends {@link Subscriber} and defines callback methods that are invoked when + * specific events occur on a data stream. Implementations of this interface can be registered with + * data streams to receive notifications about data transmission, status changes, and error + * conditions. + * + *

    The listener provides four types of notifications: + * + *

      + *
    • Data reception events when new data arrives on the stream + *
    • Data transmission events when data is sent through the stream + *
    • Status change events when the stream status changes + *
    • Error detection events when errors occur during stream operations + *
    + * + * @author Enedis Smarties team + * @see DataStream + * @see DataStreamBase + * @see DataStreamStatus + * @see DataDictionary + */ +public interface DataStreamListener extends Subscriber { + /** + * Called when data has been received on the data stream. + * + *

    This method is invoked whenever new data arrives on the stream and has been successfully + * read. Implementations should handle the received data according to their specific requirements. + * + * @param dataStreamName the name of the stream that received the data + * @param data the data dictionary containing the received data + */ + public void onDataReceived(String dataStreamName, DataDictionary data); + + /** + * Called when data has been sent through the data stream. + * + *

    This method is invoked after data has been successfully written to the stream. + * Implementations can use this to track data transmission, log sent data, or perform + * post-transmission processing. + * + * @param dataStreamName the name of the stream that sent the data + * @param data the data dictionary containing the sent data + */ + public void onDataSent(String dataStreamName, DataDictionary data); + + /** + * Called when the status of the data stream changes. + * + *

    This method is invoked whenever the stream transitions to a new status or when an error + * state is entered. Implementations can use this to monitor stream health and react to status + * changes. + * + * @param dataStreamName the name of the stream whose status changed + * @param status the new status of the stream + */ + public void onStatusChanged(String dataStreamName, DataStreamStatus status); + + /** + * Called when an error is detected during data stream operations. + * + *

    This method is invoked when an error occurs during stream processing, such as communication + * failures, data validation errors, or protocol violations. The error code and message provide + * details about the error, and the data parameter may contain the data associated with the error + * for diagnostic purposes. + * + * @param dataStreamName the name of the stream where the error was detected + * @param errorCode the error code identifying the type of error + * @param errorMessage a descriptive message about the error + * @param data the data dictionary associated with the error, if any + */ + public void onErrorDetected( + String dataStreamName, int errorCode, String errorMessage, DataDictionary data); +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java b/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java index 282be74..512496c 100644 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java @@ -1,23 +1,49 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * DataStream status - */ -public enum DataStreamStatus -{ - /** Unknown, default status */ - UNKNOWN, - /** Closed */ - CLOSED, - /** Opened */ - OPENED, - /** Error */ - ERROR -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +/** + * Enumeration representing the operational status of a data stream. + * + *

    This enum defines the possible states that a data stream can be in during its lifecycle. The + * status reflects the current operational state of the stream and is used to monitor and manage + * stream health and availability. Status changes are typically notified to registered listeners + * through the {@link DataStreamListener} interface. + * + * @author Enedis Smarties team + * @see DataStream + * @see DataStreamBase + * @see DataStreamListener + */ +public enum DataStreamStatus { + /** + * Unknown status - the default initial state. This status is assigned when a stream is first + * created and before it has been properly initialized or configured. + */ + UNKNOWN, + + /** + * Closed status - the stream is not active. This status indicates that the stream has been closed + * and is not processing data. A stream can be reopened after being closed. + */ + CLOSED, + + /** + * Opened status - the stream is active and operational. This status indicates that the stream has + * been successfully opened and is ready to perform read/write operations according to its + * direction. + */ + OPENED, + + /** + * Error status - the stream has encountered an error. This status indicates that an error has + * occurred during stream operations and the stream may not be functioning properly. Recovery or + * intervention may be required. + */ + ERROR +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamType.java b/src/main/java/enedis/lab/io/datastreams/DataStreamType.java index 9d778d7..4d1461d 100644 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamType.java +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamType.java @@ -1,27 +1,60 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * DataStream type - */ -public enum DataStreamType -{ - /** raw stream */ - RAW, - /** TIC stream */ - TIC, - /** IEC61850 stream */ - IEC61850, - /** Slave modbus stream */ - MODBUS_SLAVE, - /** Master modbus stream */ - MODBUS_MASTER, - /** LEM DC product */ - LEM_DC; -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +/** + * Enumeration representing different types of data streams based on protocol and data format. + * + *

    This enum defines the various data stream types supported by the system, each corresponding to + * a specific communication protocol or data format. The type determines how data is encoded, + * decoded, and processed by the stream. + * + *

    Stream types range from raw unformatted data to specialized industrial protocols like Modbus + * and IEC 61850, as well as specific product protocols like TIC and LEM DC. + * + * @author Enedis Smarties team + * @see DataStreamConfiguration + * @see DataStream + */ +public enum DataStreamType { + /** + * Raw data stream without specific protocol formatting. Data is transmitted without + * protocol-specific encoding or decoding, suitable for binary data or custom formats. + */ + RAW, + + /** + * TIC (Télé Information Client) protocol stream. Used for handling data from smart meters and + * energy management devices using the TIC protocol standard. + */ + TIC, + + /** + * IEC 61850 protocol stream. Used for communication with electrical substation automation systems + * following the IEC 61850 international standard. + */ + IEC61850, + + /** + * Modbus slave mode stream. The stream operates as a Modbus slave device, responding to requests + * from a Modbus master. + */ + MODBUS_SLAVE, + + /** + * Modbus master mode stream. The stream operates as a Modbus master device, initiating requests + * to Modbus slave devices. + */ + MODBUS_MASTER, + + /** + * LEM DC product protocol stream. Used for communication with LEM (Liaison + * Électronique-Mécanique) DC measurement devices. + */ + LEM_DC; +} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java b/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java index 6e0e865..a5221db 100644 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java +++ b/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java @@ -1,123 +1,113 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.Notifier; - -/** - * DataStream interface - */ -public interface DataStreamUser extends Notifier -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * read data on the stream - * - * @return data read - * @throws DataStreamException - */ - public DataDictionary read() throws DataStreamException; - - /** - * write data on the stream - * - * @param data - * @throws DataStreamException - */ - public void write(DataDictionary data) throws DataStreamException; - - /** - * Get data stream name - * - * @return the stream name - */ - public String getName(); - - /** - * Get stream configuration - * - * @return stream configuration - */ - public DataStreamConfiguration getConfiguration(); - - /** - * Get stream direction - * - * @return stream direction - */ - public DataStreamDirection getDirection(); - - /** - * Get stream type - * - * @return stream type - */ - public DataStreamType getType(); - - /** - * Get stream status - * - * @return stream status - */ - public DataStreamStatus getStatus(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.datastreams; + +import enedis.lab.types.DataDictionary; +import enedis.lab.util.task.Notifier; + +/** + * Interface defining the user-facing operations for data stream interaction. + * + *

    This interface extends {@link Notifier} with {@link DataStreamListener} support, providing the + * core functionality for reading and writing data, as well as querying stream properties and + * configuration. It represents the primary interface through which users interact with data + * streams. + * + *

    The interface provides methods for: + * + *

      + *
    • Data operations (read/write) + *
    • Stream identification (name) + *
    • Configuration access + *
    • Stream property queries (direction, type, status) + *
    • Listener management (through Notifier interface) + *
    + * + * @author Enedis Smarties team + * @see DataStream + * @see DataStreamBase + * @see DataStreamListener + * @see DataStreamConfiguration + */ +public interface DataStreamUser extends Notifier { + /** + * Reads data from the stream. + * + *

    This method retrieves data from the stream and returns it as a DataDictionary. The method + * blocks until data is available or an error occurs. The availability and behavior of this + * operation depend on the stream direction and status. + * + * @return the data read from the stream as a DataDictionary + * @throws DataStreamException if the stream is not readable, an I/O error occurs, or the + * operation is not supported by the stream direction + */ + public DataDictionary read() throws DataStreamException; + + /** + * Writes data to the stream. + * + *

    This method sends the provided data through the stream. The data is encoded according to the + * stream type and transmitted to the configured destination. The availability of this operation + * depends on the stream direction and status. + * + * @param data the data to write to the stream + * @throws DataStreamException if the stream is not writable, an I/O error occurs, the data is + * invalid, or the operation is not supported by the stream direction + */ + public void write(DataDictionary data) throws DataStreamException; + + /** + * Returns the name of this data stream. + * + *

    The name uniquely identifies the stream within the system and is used for logging, + * monitoring, and listener notifications. + * + * @return the name of the data stream + */ + public String getName(); + + /** + * Returns the configuration of this data stream. + * + *

    The configuration contains all parameters that define how the stream operates, including + * name, type, direction, and channel association. + * + * @return the stream configuration + */ + public DataStreamConfiguration getConfiguration(); + + /** + * Returns the direction of this data stream. + * + *

    The direction indicates whether the stream is configured for input (reading), output + * (writing), or bidirectional (both reading and writing) operations. + * + * @return the stream direction + */ + public DataStreamDirection getDirection(); + + /** + * Returns the type of this data stream. + * + *

    The type identifies the protocol or data format used by the stream, such as RAW, TIC, + * Modbus, or IEC 61850. + * + * @return the stream type + */ + public DataStreamType getType(); + + /** + * Returns the current status of this data stream. + * + *

    The status indicates the operational state of the stream, such as UNKNOWN, CLOSED, OPENED, + * or ERROR. This is useful for monitoring stream health and availability. + * + * @return the current stream status + */ + public DataStreamStatus getStatus(); +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java b/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java index 3c30eee..6e0746a 100644 --- a/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java +++ b/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,449 +7,435 @@ package enedis.lab.io.serialport; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorNumberMinMax; import enedis.lab.types.datadictionary.KeyDescriptorString; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** - * Class used to get the following serial port informations:
    - * - port unique identifier
    - * - port name used to open serial port
    - * - port description
    - * - USB device product identifier
    - * - USB device vendor identifier
    - * - USB device product name
    - * - USB device manufacturer
    - * - USB device serial number + * Descriptor class for serial port information and USB device properties. + * + *

    This class extends {@link DataDictionaryBase} and provides a structured way to store and + * access serial port information, including both native serial ports and USB-based serial devices. + * It encapsulates all relevant properties needed to identify, configure, and connect to serial + * ports. + * + *

    The descriptor includes the following information: + * + *

      + *
    • Port unique identifier + *
    • Port name used to open the serial port + *
    • Port description + *
    • USB device product identifier (PID) + *
    • USB device vendor identifier (VID) + *
    • USB device product name + *
    • USB device manufacturer + *
    • USB device serial number + *
    + * + *

    The class supports distinguishing between native serial ports (e.g., COM ports) and USB serial + * devices through the {@link #isNative()} method. + * + * @author Enedis Smarties team + * @see DataDictionaryBase + * @see DataDictionaryException */ -public class SerialPortDescriptor extends DataDictionaryBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_PORT_ID = "portId"; - protected static final String KEY_PORT_NAME = "portName"; - protected static final String KEY_DESCRIPTION = "description"; - protected static final String KEY_PRODUCT_ID = "productId"; - protected static final String KEY_VENDOR_ID = "vendorId"; - protected static final String KEY_PRODUCT_NAME = "productName"; - protected static final String KEY_MANUFACTURER = "manufacturer"; - protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - - private static final Number PRODUCT_ID_MIN = 0; - private static final Number PRODUCT_ID_MAX = 65535; - private static final Number VENDOR_ID_MIN = 0; - private static final Number VENDOR_ID_MAX = 65535; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kPortId; - protected KeyDescriptorString kPortName; - protected KeyDescriptorString kDescription; - protected KeyDescriptorNumberMinMax kProductId; - protected KeyDescriptorNumberMinMax kVendorId; - protected KeyDescriptorString kProductName; - protected KeyDescriptorString kManufacturer; - protected KeyDescriptorString kSerialNumber; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Empty constructor - */ - public SerialPortDescriptor() - { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public SerialPortDescriptor(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using data dictionary - * - * @param other - * @throws DataDictionaryException - */ - public SerialPortDescriptor(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param portId - * the port unique identifier - * @param portName - * the port name used to open serial port - * @param description - * the port description - * @param productId - * the USB device product identifier - * @param vendorId - * the USB device vendor identifier - * @param productName - * the USB device product name - * @param manufacturer - * the USB device manufacturer - * @param serialNumber - * the USB device serial number - * @throws DataDictionaryException - * if any parameters is invalid - */ - public SerialPortDescriptor(String portId, String portName, String description, Number productId, Number vendorId, String productName, String manufacturer, String serialNumber) - throws DataDictionaryException - { - this(); - - this.setPortId(portId); - this.setPortName(portName); - this.setDescription(description); - this.setProductId(productId); - this.setVendorId(vendorId); - this.setProductName(productName); - this.setManufacturer(manufacturer); - this.setSerialNumber(serialNumber); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get port id - * - * @return the port unique identifier - */ - public String getPortId() - { - return (String) this.data.get(KEY_PORT_ID); - } - - /** - * Get port name - * - * @return the port name used to open serial port - */ - public String getPortName() - { - return (String) this.data.get(KEY_PORT_NAME); - } - - /** - * Get description - * - * @return the port description - */ - public String getDescription() - { - return (String) this.data.get(KEY_DESCRIPTION); - } - - /** - * Get product id - * - * @return the USB device product identifier - */ - public Number getProductId() - { - return (Number) this.data.get(KEY_PRODUCT_ID); - } - - /** - * Get vendor id - * - * @return the USB device vendor identifier - */ - public Number getVendorId() - { - return (Number) this.data.get(KEY_VENDOR_ID); - } - - /** - * Get product name - * - * @return the USB device product name - */ - public String getProductName() - { - return (String) this.data.get(KEY_PRODUCT_NAME); - } - - /** - * Get manufacturer - * - * @return the USB device manufacturer - */ - public String getManufacturer() - { - return (String) this.data.get(KEY_MANUFACTURER); - } - - /** - * Get serial number - * - * @return the USB device serial number - */ - public String getSerialNumber() - { - return (String) this.data.get(KEY_SERIAL_NUMBER); - } - - /** - * Set port id - * - * @param portId - * the port unique identifier - * @throws DataDictionaryException - * if portId is empty - */ - public void setPortId(String portId) throws DataDictionaryException - { - this.setPortId((Object) portId); - } - - /** - * Set port name - * - * @param portName - * the port name used to open serial port - * @throws DataDictionaryException - * if portName is empty - */ - public void setPortName(String portName) throws DataDictionaryException - { - this.setPortName((Object) portName); - } - - /** - * Set description - * - * @param description - * the port description - * @throws DataDictionaryException - * if description is empty - */ - public void setDescription(String description) throws DataDictionaryException - { - this.setDescription((Object) description); - } - - /** - * Set product id - * - * @param productId - * the USB device product identifier - * @throws DataDictionaryException - * if productId is out of range [0-65535] - */ - public void setProductId(Number productId) throws DataDictionaryException - { - this.setProductId((Object) productId); - } - - /** - * Set vendor id - * - * @param vendorId - * the USB device vendor identifier - * @throws DataDictionaryException - * if vendorId is out of range [0-65535] - */ - public void setVendorId(Number vendorId) throws DataDictionaryException - { - this.setVendorId((Object) vendorId); - } - - /** - * Set product name - * - * @param productName - * the USB device product name - * @throws DataDictionaryException - * if productName is empty - */ - public void setProductName(String productName) throws DataDictionaryException - { - this.setProductName((Object) productName); - } - - /** - * Set manufacturer - * - * @param manufacturer - * the USB device manufacturer - * @throws DataDictionaryException - * if manufacturer is empty - */ - public void setManufacturer(String manufacturer) throws DataDictionaryException - { - this.setManufacturer((Object) manufacturer); - } - - /** - * Set serial number - * - * @param serialNumber - * the USB device serial number - * @throws DataDictionaryException - * if serialNumber is empty - */ - public void setSerialNumber(String serialNumber) throws DataDictionaryException - { - this.setSerialNumber((Object) serialNumber); - } - - /** - * Check if serial port is native (not USB) - * - * @return true if native port, false otherwise - */ - public boolean isNative() - { - return this.getPortName() != null && !this.getPortName().isEmpty() && this.getPortId() == null; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setPortId(Object portId) throws DataDictionaryException - { - this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); - } - - protected void setPortName(Object portName) throws DataDictionaryException - { - this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); - } - - protected void setDescription(Object description) throws DataDictionaryException - { - this.data.put(KEY_DESCRIPTION, this.kDescription.convert(description)); - } - - protected void setProductId(Object productId) throws DataDictionaryException - { - this.data.put(KEY_PRODUCT_ID, this.kProductId.convert(productId)); - } - - protected void setVendorId(Object vendorId) throws DataDictionaryException - { - this.data.put(KEY_VENDOR_ID, this.kVendorId.convert(vendorId)); - } - - protected void setProductName(Object productName) throws DataDictionaryException - { - this.data.put(KEY_PRODUCT_NAME, this.kProductName.convert(productName)); - } - - protected void setManufacturer(Object manufacturer) throws DataDictionaryException - { - this.data.put(KEY_MANUFACTURER, this.kManufacturer.convert(manufacturer)); - } - - protected void setSerialNumber(Object serialNumber) throws DataDictionaryException - { - this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); - this.keys.add(this.kPortId); - - this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); - this.keys.add(this.kPortName); - - this.kDescription = new KeyDescriptorString(KEY_DESCRIPTION, false, false); - this.keys.add(this.kDescription); - - this.kProductId = new KeyDescriptorNumberMinMax(KEY_PRODUCT_ID, false, PRODUCT_ID_MIN, PRODUCT_ID_MAX); - this.keys.add(this.kProductId); - - this.kVendorId = new KeyDescriptorNumberMinMax(KEY_VENDOR_ID, false, VENDOR_ID_MIN, VENDOR_ID_MAX); - this.keys.add(this.kVendorId); - - this.kProductName = new KeyDescriptorString(KEY_PRODUCT_NAME, false, false); - this.keys.add(this.kProductName); - - this.kManufacturer = new KeyDescriptorString(KEY_MANUFACTURER, false, false); - this.keys.add(this.kManufacturer); - - this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, false); - this.keys.add(this.kSerialNumber); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class SerialPortDescriptor extends DataDictionaryBase { + protected static final String KEY_PORT_ID = "portId"; + protected static final String KEY_PORT_NAME = "portName"; + protected static final String KEY_DESCRIPTION = "description"; + protected static final String KEY_PRODUCT_ID = "productId"; + protected static final String KEY_VENDOR_ID = "vendorId"; + protected static final String KEY_PRODUCT_NAME = "productName"; + protected static final String KEY_MANUFACTURER = "manufacturer"; + protected static final String KEY_SERIAL_NUMBER = "serialNumber"; + + private static final Number PRODUCT_ID_MIN = 0; + private static final Number PRODUCT_ID_MAX = 65535; + private static final Number VENDOR_ID_MIN = 0; + private static final Number VENDOR_ID_MAX = 65535; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorString kPortId; + protected KeyDescriptorString kPortName; + protected KeyDescriptorString kDescription; + protected KeyDescriptorNumberMinMax kProductId; + protected KeyDescriptorNumberMinMax kVendorId; + protected KeyDescriptorString kProductName; + protected KeyDescriptorString kManufacturer; + protected KeyDescriptorString kSerialNumber; + + /** + * Constructs an empty SerialPortDescriptor. + * + *

    This constructor initializes the descriptor with default values and loads the key + * descriptors for validation and type conversion. + */ + public SerialPortDescriptor() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructs a SerialPortDescriptor from a map of key-value pairs. + * + *

    This constructor creates a descriptor by copying values from the provided map. The map + * should contain keys corresponding to the descriptor properties. + * + * @param map the map containing serial port descriptor parameters + * @throws DataDictionaryException if the map contains invalid values + */ + public SerialPortDescriptor(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructs a SerialPortDescriptor by copying from another DataDictionary. + * + *

    This constructor creates a new descriptor by copying all values from the provided + * DataDictionary. This is useful for creating descriptors from existing data structures. + * + * @param other the DataDictionary to copy descriptor data from + * @throws DataDictionaryException if the source dictionary contains invalid values + */ + public SerialPortDescriptor(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructs a SerialPortDescriptor with all parameters explicitly set. + * + *

    This constructor creates a descriptor with all serial port and USB device properties + * specified. It validates the parameters and updates the descriptor accordingly. + * + * @param portId the port unique identifier + * @param portName the port name used to open the serial port + * @param description the port description + * @param productId the USB device product identifier (PID), must be in range [0-65535] + * @param vendorId the USB device vendor identifier (VID), must be in range [0-65535] + * @param productName the USB device product name + * @param manufacturer the USB device manufacturer + * @param serialNumber the USB device serial number + * @throws DataDictionaryException if any parameter is invalid or out of range + */ + public SerialPortDescriptor( + String portId, + String portName, + String description, + Number productId, + Number vendorId, + String productName, + String manufacturer, + String serialNumber) + throws DataDictionaryException { + this(); + + this.setPortId(portId); + this.setPortName(portName); + this.setDescription(description); + this.setProductId(productId); + this.setVendorId(vendorId); + this.setProductName(productName); + this.setManufacturer(manufacturer); + this.setSerialNumber(serialNumber); + + this.checkAndUpdate(); + } + + /** + * Returns the port unique identifier. + * + * @return the port unique identifier + */ + public String getPortId() { + return (String) this.data.get(KEY_PORT_ID); + } + + /** + * Returns the port name used to open the serial port. + * + * @return the port name used to open the serial port + */ + public String getPortName() { + return (String) this.data.get(KEY_PORT_NAME); + } + + /** + * Returns the port description. + * + * @return the port description + */ + public String getDescription() { + return (String) this.data.get(KEY_DESCRIPTION); + } + + /** + * Returns the USB device product identifier (PID). + * + * @return the USB device product identifier in range [0-65535] + */ + public Number getProductId() { + return (Number) this.data.get(KEY_PRODUCT_ID); + } + + /** + * Returns the USB device vendor identifier (VID). + * + * @return the USB device vendor identifier in range [0-65535] + */ + public Number getVendorId() { + return (Number) this.data.get(KEY_VENDOR_ID); + } + + /** + * Returns the USB device product name. + * + * @return the USB device product name + */ + public String getProductName() { + return (String) this.data.get(KEY_PRODUCT_NAME); + } + + /** + * Returns the USB device manufacturer. + * + * @return the USB device manufacturer + */ + public String getManufacturer() { + return (String) this.data.get(KEY_MANUFACTURER); + } + + /** + * Returns the USB device serial number. + * + * @return the USB device serial number + */ + public String getSerialNumber() { + return (String) this.data.get(KEY_SERIAL_NUMBER); + } + + /** + * Sets the port unique identifier. + * + * @param portId the port unique identifier + * @throws DataDictionaryException if portId is invalid + */ + public void setPortId(String portId) throws DataDictionaryException { + this.setPortId((Object) portId); + } + + /** + * Sets the port name used to open the serial port. + * + * @param portName the port name used to open the serial port + * @throws DataDictionaryException if portName is invalid + */ + public void setPortName(String portName) throws DataDictionaryException { + this.setPortName((Object) portName); + } + + /** + * Sets the port description. + * + * @param description the port description + * @throws DataDictionaryException if description is invalid + */ + public void setDescription(String description) throws DataDictionaryException { + this.setDescription((Object) description); + } + + /** + * Sets the USB device product identifier (PID). + * + * @param productId the USB device product identifier + * @throws DataDictionaryException if productId is out of range [0-65535] + */ + public void setProductId(Number productId) throws DataDictionaryException { + this.setProductId((Object) productId); + } + + /** + * Sets the USB device vendor identifier (VID). + * + * @param vendorId the USB device vendor identifier + * @throws DataDictionaryException if vendorId is out of range [0-65535] + */ + public void setVendorId(Number vendorId) throws DataDictionaryException { + this.setVendorId((Object) vendorId); + } + + /** + * Sets the USB device product name. + * + * @param productName the USB device product name + * @throws DataDictionaryException if productName is invalid + */ + public void setProductName(String productName) throws DataDictionaryException { + this.setProductName((Object) productName); + } + + /** + * Sets the USB device manufacturer. + * + * @param manufacturer the USB device manufacturer + * @throws DataDictionaryException if manufacturer is invalid + */ + public void setManufacturer(String manufacturer) throws DataDictionaryException { + this.setManufacturer((Object) manufacturer); + } + + /** + * Sets the USB device serial number. + * + * @param serialNumber the USB device serial number + * @throws DataDictionaryException if serialNumber is invalid + */ + public void setSerialNumber(String serialNumber) throws DataDictionaryException { + this.setSerialNumber((Object) serialNumber); + } + + /** + * Checks if this serial port is a native port (not USB). + * + *

    A port is considered native if it has a port name but no port ID. This typically indicates a + * built-in serial port (e.g., COM1) rather than a USB-to-serial adapter. + * + * @return true if this is a native serial port, false if it's a USB device + */ + public boolean isNative() { + return this.getPortName() != null && !this.getPortName().isEmpty() && this.getPortId() == null; + } + + /** + * Sets the port ID using object conversion and validation. + * + * @param portId the port ID object to convert and set + * @throws DataDictionaryException if the conversion or validation fails + */ + protected void setPortId(Object portId) throws DataDictionaryException { + this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); + } + + /** + * Sets the port name using object conversion and validation. + * + * @param portName the port name object to convert and set + * @throws DataDictionaryException if the conversion or validation fails + */ + protected void setPortName(Object portName) throws DataDictionaryException { + this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); + } + + /** + * Sets the description using object conversion and validation. + * + * @param description the description object to convert and set + * @throws DataDictionaryException if the conversion or validation fails + */ + protected void setDescription(Object description) throws DataDictionaryException { + this.data.put(KEY_DESCRIPTION, this.kDescription.convert(description)); + } + + /** + * Sets the product ID using object conversion and validation. + * + * @param productId the product ID object to convert and set + * @throws DataDictionaryException if the conversion or validation fails + */ + protected void setProductId(Object productId) throws DataDictionaryException { + this.data.put(KEY_PRODUCT_ID, this.kProductId.convert(productId)); + } + + /** + * Sets the vendor ID using object conversion and validation. + * + * @param vendorId the vendor ID object to convert and set + * @throws DataDictionaryException if the conversion or validation fails + */ + protected void setVendorId(Object vendorId) throws DataDictionaryException { + this.data.put(KEY_VENDOR_ID, this.kVendorId.convert(vendorId)); + } + + /** + * Sets the product name using object conversion and validation. + * + * @param productName the product name object to convert and set + * @throws DataDictionaryException if the conversion or validation fails + */ + protected void setProductName(Object productName) throws DataDictionaryException { + this.data.put(KEY_PRODUCT_NAME, this.kProductName.convert(productName)); + } + + /** + * Sets the manufacturer using object conversion and validation. + * + * @param manufacturer the manufacturer object to convert and set + * @throws DataDictionaryException if the conversion or validation fails + */ + protected void setManufacturer(Object manufacturer) throws DataDictionaryException { + this.data.put(KEY_MANUFACTURER, this.kManufacturer.convert(manufacturer)); + } + + /** + * Sets the serial number using object conversion and validation. + * + * @param serialNumber the serial number object to convert and set + * @throws DataDictionaryException if the conversion or validation fails + */ + protected void setSerialNumber(Object serialNumber) throws DataDictionaryException { + this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); + } + + /** + * Loads and initializes the key descriptors for descriptor validation. + * + *

    This private method sets up the key descriptors that define the structure and validation + * rules for all serial port descriptor properties. It creates descriptors for both string and + * numeric fields with appropriate validation constraints (e.g., product and vendor IDs must be in + * range [0-65535]). + * + *

    If any descriptor creation fails, a RuntimeException is thrown with the original exception + * as the cause. + */ + private void loadKeyDescriptors() { + try { + this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); + this.keys.add(this.kPortId); + + this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); + this.keys.add(this.kPortName); + + this.kDescription = new KeyDescriptorString(KEY_DESCRIPTION, false, false); + this.keys.add(this.kDescription); + + this.kProductId = + new KeyDescriptorNumberMinMax(KEY_PRODUCT_ID, false, PRODUCT_ID_MIN, PRODUCT_ID_MAX); + this.keys.add(this.kProductId); + + this.kVendorId = + new KeyDescriptorNumberMinMax(KEY_VENDOR_ID, false, VENDOR_ID_MIN, VENDOR_ID_MAX); + this.keys.add(this.kVendorId); + + this.kProductName = new KeyDescriptorString(KEY_PRODUCT_NAME, false, false); + this.keys.add(this.kProductName); + + this.kManufacturer = new KeyDescriptorString(KEY_MANUFACTURER, false, false); + this.keys.add(this.kManufacturer); + + this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, false); + this.keys.add(this.kSerialNumber); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java index 4090a4b..e94cb91 100644 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java +++ b/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java @@ -1,120 +1,137 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.serialport; - -import enedis.lab.io.PortFinder; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; - -/** - * Interface used to find all serial port descriptor - */ -public interface SerialPortFinder extends PortFinder -{ - /** - * Find serial port descriptor matching with port id - * - * @param portId - * the unique port identifier desired - * - * @return Serial port descriptor found, or null if nothing matches with portId - */ - public default SerialPortDescriptor findByPortId(String portId) - { - return this.findAll().stream().filter(k -> (k.getPortId() != null) ? k.getPortId().equals(portId) : portId == null).findFirst().orElse(null); - } - - /** - * Find serial port descriptor matching with port name - * - * @param portName - * the port name desired - * - * @return Serial port descriptor found, or null if nothing matches with portName - */ - public default SerialPortDescriptor findByPortName(String portName) - { - return this.findAll().stream().filter(k -> (k.getPortName() != null) ? k.getPortName().equals(portName) : portName == null).findFirst().orElse(null); - } - - /** - * Find native serial port (not USB) descriptor matching with port name - * - * @param portName - * the port name desired - * - * @return Serial port descriptor found, or null if nothing matches with portName - */ - public default SerialPortDescriptor findNative(String portName) - { - return this.findAll().stream().filter(k -> k.isNative() && k.getPortName().equals(portName)).findFirst().orElse(null); - } - - /** - * Find serial port descriptor matching with port id (having priority) or port name - * - * @param portId - * the unique port identifier desired - * @param portName - * the port name desired - * - * @return Serial port descriptor found, or null if nothing matches with portName - */ - public default SerialPortDescriptor findByPortIdOrPortName(String portId, String portName) - { - SerialPortDescriptor descriptor; - - if (portId != null) - { - descriptor = this.findByPortId(portId); - } - else if (portName != null) - { - descriptor = this.findByPortName(portName); - } - else - { - descriptor = null; - } - - return descriptor; - } - - /** - * Find serial port descriptor matching with pid/vid - * - * @param productId - * the USB device product identifier - * @param vendorId - * the USB device vendor identifier - * - * @return Serial port descriptor list found - */ - public default DataList findByProductIdAndVendorId(Number productId, Number vendorId) - { - DataList descriptorList = new DataArrayList(); - - for (SerialPortDescriptor descriptor : this.findAll()) - { - if (descriptor.getProductId() == null && productId != null) - { - continue; - } - if (descriptor.getVendorId() == null && vendorId != null) - { - continue; - } - if (descriptor.getProductId().equals(productId) && descriptor.getVendorId().equals(vendorId)) - { - descriptorList.add(descriptor); - } - } - - return descriptorList; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.serialport; + +import enedis.lab.io.PortFinder; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataList; + +/** + * Interface for discovering and searching serial ports on the system. + * + *

    This interface extends {@link PortFinder} with {@link SerialPortDescriptor} support, providing + * specialized methods for finding serial ports based on various criteria such as port ID, port + * name, USB product/vendor identifiers, or native port detection. + * + *

    The interface provides default implementations for all search methods, allowing + * implementations to focus on the {@link PortFinder#findAll()} method which retrieves all available + * serial ports on the system. + * + * @author Enedis Smarties team + * @see PortFinder + * @see SerialPortDescriptor + */ +public interface SerialPortFinder extends PortFinder { + /** + * Finds a serial port descriptor matching the specified port ID. + * + *

    This method searches through all available serial ports and returns the first descriptor + * with a matching port ID. The port ID is typically a unique identifier for USB serial devices. + * + * @param portId the unique port identifier to search for + * @return the matching serial port descriptor, or null if no match is found + */ + public default SerialPortDescriptor findByPortId(String portId) { + return this.findAll().stream() + .filter(k -> (k.getPortId() != null) ? k.getPortId().equals(portId) : portId == null) + .findFirst() + .orElse(null); + } + + /** + * Finds a serial port descriptor matching the specified port name. + * + *

    This method searches through all available serial ports and returns the first descriptor + * with a matching port name. The port name is the system identifier used to open the serial port + * (e.g., COM1, /dev/ttyUSB0). + * + * @param portName the port name to search for + * @return the matching serial port descriptor, or null if no match is found + */ + public default SerialPortDescriptor findByPortName(String portName) { + return this.findAll().stream() + .filter( + k -> (k.getPortName() != null) ? k.getPortName().equals(portName) : portName == null) + .findFirst() + .orElse(null); + } + + /** + * Finds a native serial port (not USB) matching the specified port name. + * + *

    This method searches specifically for native serial ports, excluding USB-to-serial adapters. + * A port is considered native if it has a port name but no port ID, typically indicating a + * built-in hardware serial port. + * + * @param portName the port name to search for + * @return the matching native serial port descriptor, or null if no match is found + */ + public default SerialPortDescriptor findNative(String portName) { + return this.findAll().stream() + .filter(k -> k.isNative() && k.getPortName().equals(portName)) + .findFirst() + .orElse(null); + } + + /** + * Finds a serial port descriptor matching either port ID or port name, with port ID having + * priority. + * + *

    This method provides a convenient way to search by either identifier. If a port ID is + * provided, it is used first. If no port ID is provided but a port name is available, the search + * is performed by port name. If neither is provided, null is returned. + * + * @param portId the unique port identifier to search for (has priority) + * @param portName the port name to search for (used if portId is null) + * @return the matching serial port descriptor, or null if no match is found + */ + public default SerialPortDescriptor findByPortIdOrPortName(String portId, String portName) { + SerialPortDescriptor descriptor; + + if (portId != null) { + descriptor = this.findByPortId(portId); + } else if (portName != null) { + descriptor = this.findByPortName(portName); + } else { + descriptor = null; + } + + return descriptor; + } + + /** + * Finds all serial port descriptors matching the specified USB product and vendor identifiers. + * + *

    This method searches for USB serial devices matching both the product ID (PID) and vendor ID + * (VID). Both identifiers must match for a descriptor to be included in the results. This is + * useful for finding specific USB serial devices from a particular manufacturer. + * + * @param productId the USB device product identifier (PID) to match + * @param vendorId the USB device vendor identifier (VID) to match + * @return a list of matching serial port descriptors (empty list if no matches found) + */ + public default DataList findByProductIdAndVendorId( + Number productId, Number vendorId) { + DataList descriptorList = new DataArrayList(); + + for (SerialPortDescriptor descriptor : this.findAll()) { + if (descriptor.getProductId() == null && productId != null) { + continue; + } + if (descriptor.getVendorId() == null && vendorId != null) { + continue; + } + if (descriptor.getProductId().equals(productId) + && descriptor.getVendorId().equals(vendorId)) { + descriptorList.add(descriptor); + } + } + + return descriptorList; + } +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java index 9b9ba0f..f9d934a 100644 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java +++ b/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java @@ -1,125 +1,105 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.serialport; - -import org.apache.commons.lang3.SystemUtils; - -import enedis.lab.types.DataList; - -/** - * Class used to find all serial port descriptor for any operating system - */ -public class SerialPortFinderBase implements SerialPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program writing the serial port descriptor list (JSON format) on the output stream - * - * @param args - * not used - */ - public static void main(String[] args) - { - DataList descriptors = getInstance().findAll(); - - System.out.println(descriptors.toString(2)); - } - - /** - * Get instance - * - * @return Unique instance - */ - public static SerialPortFinder getInstance() - { - if (instance == null) - { - if (SystemUtils.IS_OS_WINDOWS) - { - instance = SerialPortFinderForWindows.getInstance(); - } - else if (SystemUtils.IS_OS_LINUX) - { - instance = SerialPortFinderForLinux.getInstance(); - } - else - { - throw new RuntimeException("Operating system " + SystemUtils.OS_NAME + " is not handled by " + SerialPortFinderBase.class.getName()); - } - } - - return instance; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static SerialPortFinder instance; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private SerialPortFinderBase() - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortFinder - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataList findAll() - { - return instance.findAll(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.serialport; + +import enedis.lab.types.DataList; +import org.apache.commons.lang3.SystemUtils; + +/** + * Cross-platform serial port finder implementation with automatic OS detection. + * + *

    This class implements {@link SerialPortFinder} and provides a unified interface for + * discovering serial ports across different operating systems. It automatically detects the current + * OS and delegates to the appropriate platform-specific implementation. + * + *

    Supported operating systems: + * + *

      + *
    • Windows - uses {@code SerialPortFinderForWindows} + *
    • Linux - uses {@code SerialPortFinderForLinux} + *
    + * + *

    The class follows the singleton pattern with lazy initialization, ensuring only one instance + * exists for the lifetime of the application. + * + * @author Enedis Smarties team + * @see SerialPortFinder + * @see SerialPortDescriptor + */ +public class SerialPortFinderBase implements SerialPortFinder { + private static SerialPortFinder instance; + + /** + * Main method that outputs all serial port descriptors in JSON format. + * + *

    This utility method can be used to quickly inspect available serial ports on the system. The + * output is formatted JSON with an indentation of 2 spaces. + * + * @param args command-line arguments (not used) + */ + public static void main(String[] args) { + DataList descriptors = getInstance().findAll(); + + System.out.println(descriptors.toString(2)); + } + + /** + * Returns the singleton instance of the serial port finder. + * + *

    This method performs lazy initialization and automatically selects the appropriate + * platform-specific implementation based on the detected operating system. The instance is cached + * after the first call. + * + *

    Supported platforms are detected using Apache Commons SystemUtils: + * + *

      + *
    • Windows - returns Windows-specific finder + *
    • Linux - returns Linux-specific finder + *
    + * + * @return the singleton SerialPortFinder instance for the current OS + * @throws RuntimeException if the current operating system is not supported + */ + public static SerialPortFinder getInstance() { + if (instance == null) { + if (SystemUtils.IS_OS_WINDOWS) { + instance = SerialPortFinderForWindows.getInstance(); + } else if (SystemUtils.IS_OS_LINUX) { + instance = SerialPortFinderForLinux.getInstance(); + } else { + throw new RuntimeException( + "Operating system " + + SystemUtils.OS_NAME + + " is not handled by " + + SerialPortFinderBase.class.getName()); + } + } + + return instance; + } + + /** + * Private constructor to prevent direct instantiation. + * + *

    Use {@link #getInstance()} to obtain the singleton instance. + */ + private SerialPortFinderBase() {} + + /** + * Finds all available serial ports on the system. + * + *

    This method delegates to the platform-specific instance obtained through {@link + * #getInstance()}. The actual implementation depends on the detected operating system. + * + * @return a list of all serial port descriptors available on the system + */ + @Override + public DataList findAll() { + return instance.findAll(); + } +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java index f8c774a..60783fa 100644 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java +++ b/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java @@ -1,703 +1,648 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.serialport; - -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.sun.jna.platform.linux.Udev; -import com.sun.jna.platform.linux.Udev.UdevContext; -import com.sun.jna.platform.linux.Udev.UdevDevice; -import com.sun.jna.platform.linux.Udev.UdevEnumerate; -import com.sun.jna.platform.linux.Udev.UdevListEntry; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * Class used to find all serial port descriptor for Linux - */ -public class SerialPortFinderForLinux implements SerialPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static String MOXA_IDENTIFIER = "ttyr"; - private static Pattern MOXA_PATTERN = Pattern.compile("(.*)" + MOXA_IDENTIFIER + "(\\d+)"); - private static String MOXA_FORMAT = MOXA_IDENTIFIER + "%02d"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get instance - * - * @return Unique instance - */ - public static SerialPortFinderForLinux getInstance() - { - if (instance == null) - { - instance = new SerialPortFinderForLinux(); - } - - return instance; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static SerialPortFinderForLinux instance; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private SerialPortFinderForLinux() - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortFinder - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataList findAll() - { - DataList serialPortDescriptorList = new DataArrayList(); - - serialPortDescriptorList = availablePortsByUdev(); - if (serialPortDescriptorList.isEmpty()) - { - serialPortDescriptorList = availablePortsBySysfs(); - } - if (serialPortDescriptorList.isEmpty()) - { - serialPortDescriptorList = availablePortsByFiltersOfDevices(); - } - - return serialPortDescriptorList; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static DataList availablePortsByUdev() - { - UdevContext udev = Udev.INSTANCE.udev_new(); - UdevEnumerate enumerate = Udev.INSTANCE.udev_enumerate_new(udev); - Udev.INSTANCE.udev_enumerate_add_match_subsystem(enumerate, "tty"); - Udev.INSTANCE.udev_enumerate_scan_devices(enumerate); - - DataList serialPortDescriptorList = new DataArrayList(); - - for (UdevListEntry dev_list_entry = Udev.INSTANCE.udev_enumerate_get_list_entry(enumerate); dev_list_entry != null; dev_list_entry = dev_list_entry.getNext()) - { - UdevDevice device = Udev.INSTANCE.udev_device_new_from_syspath(udev, Udev.INSTANCE.udev_list_entry_get_name(dev_list_entry)); - if (device == null) - { - break; - } - SerialPortDescriptor serialPortDescriptor; - - String location = deviceLocation(device); - String portName = deviceName(device); - - UdevDevice parentdev = Udev.INSTANCE.udev_device_get_parent(device); - - if (parentdev != null) - { - String driverName = deviceDriver(parentdev); - if (isSerial8250Driver(driverName) && !isValidSerial8250(location)) - { - Udev.INSTANCE.udev_device_unref(device); - continue; - } - String description = deviceDescription(device); - String portId = devicePortId(device); - String manufacturer = deviceManufacturer(device); - String serialNumber = deviceSerialNumber(device); - Number vendorIdentifier = deviceVendorIdentifier(device); - Number productIdentifier = deviceProductIdentifier(device); - String productName = deviceProductName(device); - try - { - serialPortDescriptor = new SerialPortDescriptor(portId, location, description, productIdentifier, vendorIdentifier, productName, manufacturer, serialNumber); - } - catch (DataDictionaryException e) - { - e.printStackTrace(); - Udev.INSTANCE.udev_device_unref(device); - continue; - } - } - else - { - if (!isRfcommDevice(portName) && !isVirtualNullModemDevice(portName) && !isGadgetDevice(portName) && !isMoxaDevice(portName)) - { - Udev.INSTANCE.udev_device_unref(device); - continue; - } - try - { - serialPortDescriptor = new SerialPortDescriptor(null, location, null, null, null, null, null, null); - } - catch (DataDictionaryException e) - { - e.printStackTrace(); - Udev.INSTANCE.udev_device_unref(device); - continue; - } - } - serialPortDescriptorList.add(serialPortDescriptor); - Udev.INSTANCE.udev_device_unref(device); - } - Udev.INSTANCE.udev_enumerate_unref(enumerate); - Udev.INSTANCE.udev_unref(udev); - - return serialPortDescriptorList; - } - - private static DataList availablePortsBySysfs() - { - DataList serialPortDescriptorList = new DataArrayList(); - File ttySysClassDir = new File("/sys/class/tty"); - - if (!(ttySysClassDir.exists() && ttySysClassDir.canRead())) - { - return serialPortDescriptorList; - } - FileFilter filter = new FileFilter() - { - @Override - public boolean accept(File pathname) - { - - return pathname.isDirectory() && !pathname.getName().equals(".") && !pathname.getName().equals(".."); - } - }; - File[] fileInfos = ttySysClassDir.listFiles(filter); - for (File fileInfo : fileInfos) - { - if (!Files.isSymbolicLink(fileInfo.toPath())) - { - continue; - } - File targetDir = fileInfo.getAbsoluteFile(); - SerialPortDescriptor serialPortDescriptor; - - String portName = deviceName(targetDir); - if (portName == null) - { - continue; - } - String driverName = deviceDriver(targetDir); - if (driverName == null) - { - if (!isRfcommDevice(portName) && !isVirtualNullModemDevice(portName) && !isGadgetDevice(portName) && !isMoxaDevice(portName)) - { - continue; - } - } - - String device = portNameToSystemLocation(portName); - if (isSerial8250Driver(driverName) && !isValidSerial8250(device)) - { - continue; - } - String description = null; - String manufacturer = null; - String serialNumber = null; - Number vendorIdentifier = null; - Number productIdentifier = null; - String productName = null; - do - { - if (description == null) - { - description = deviceDescription(targetDir); - } - if (manufacturer == null) - { - manufacturer = deviceManufacturer(targetDir); - } - if (serialNumber == null) - { - serialNumber = deviceSerialNumber(targetDir); - } - if (vendorIdentifier == null) - { - vendorIdentifier = deviceVendorIdentifier(targetDir); - } - if (productIdentifier == null) - { - productIdentifier = deviceProductIdentifier(targetDir); - } - if (productName == null) - { - productName = deviceProductName(targetDir); - } - if (description != null || manufacturer != null || serialNumber != null || vendorIdentifier != null || productIdentifier != null || productName != null) - { - break; - } - targetDir = targetDir.getParentFile(); - } while (targetDir != null); - try - { - serialPortDescriptor = new SerialPortDescriptor(null, portName, description, productIdentifier, vendorIdentifier, productName, manufacturer, serialNumber); - } - catch (DataDictionaryException e) - { - e.printStackTrace(); - continue; - } - serialPortDescriptorList.add(serialPortDescriptor); - } - - return serialPortDescriptorList; - } - - private static DataList availablePortsByFiltersOfDevices() - { - DataList serialPortDescriptorList = new DataArrayList(); - - List deviceFilePaths = filteredDeviceFilePaths(); - for (String deviceFilePath : deviceFilePaths) - { - SerialPortDescriptor serialPortDescriptor; - String portName = portNameFromSystemLocation(deviceFilePath); - try - { - serialPortDescriptor = new SerialPortDescriptor(null, portName, null, null, null, null, null, null); - } - catch (DataDictionaryException e) - { - e.printStackTrace(); - continue; - } - serialPortDescriptorList.add(serialPortDescriptor); - } - - return serialPortDescriptorList; - } - - private static List filteredDeviceFilePaths() - { - List deviceFileNameFilterList = new ArrayList(); - - // Linux - deviceFileNameFilterList.add("ttyS*"); // Standard UART 8250 and etc. - deviceFileNameFilterList.add("ttyO*"); // OMAP UART 8250 and etc. - deviceFileNameFilterList.add("ttyUSB*"); // Usb/serial converters PL2303 and etc. - deviceFileNameFilterList.add("ttyACM*"); // CDC_ACM converters (i.e. Mobile Phones). - deviceFileNameFilterList.add("ttyGS*"); // Gadget serial device (i.e. Mobile Phones with gadget serial driver). - deviceFileNameFilterList.add("ttyMI*"); // MOXA pci/serial converters. - deviceFileNameFilterList.add("ttymxc*"); // Motorola IMX serial ports (i.e. Freescale i.MX). - deviceFileNameFilterList.add("ttyAMA*"); // AMBA serial device for embedded platform on ARM (i.e. Raspberry Pi). - deviceFileNameFilterList.add("ttyTHS*"); // Serial device for embedded platform on ARM (i.e. Tegra Jetson TK1). - deviceFileNameFilterList.add("rfcomm*"); // Bluetooth serial device. - deviceFileNameFilterList.add("ircomm*"); // IrDA serial device. - deviceFileNameFilterList.add("tnt*"); // Virtual tty0tty serial device. - deviceFileNameFilterList.add(MOXA_IDENTIFIER + "*"); // MOXA ethernet device. - // FreeBSD - deviceFileNameFilterList.add("cu*"); - // QNX - deviceFileNameFilterList.add("ser*"); - - List result = new ArrayList(); - - File deviceDir = new File("/dev"); - if (deviceDir.exists()) - { - FileFilter deviceFileNameFilter = new FileFilter() - { - @Override - public boolean accept(File pathname) - { - Path path = pathname.toPath(); - - return deviceFileNameFilterList.contains(pathname.getName()) && pathname.isFile() && !Files.isSymbolicLink(path); - } - }; - File[] deviceFileInfos = deviceDir.listFiles(deviceFileNameFilter); - for (File deviceFileInfo : deviceFileInfos) - { - String deviceAbsoluteFilePath = deviceFileInfo.getAbsolutePath(); - // it is a quick workaround to skip the non-serial devices (FreeBSD) - if (deviceAbsoluteFilePath.endsWith(".init") || deviceAbsoluteFilePath.endsWith(".lock")) - { - continue; - } - if (!result.contains(deviceAbsoluteFilePath)) - { - result.add(deviceAbsoluteFilePath); - } - } - } - - return result; - } - - private static boolean isSerial8250Driver(String driverName) - { - return (driverName == "serial8250"); - } - - private static boolean isValidSerial8250(String systemLocation) - { - return false; - } - - private static boolean isRfcommDevice(String portName) - { - if (!portName.startsWith("rfcomm")) - { - return false; - } - try - { - String number = portName.substring(6); - int portNumber = Integer.parseInt(number); - if (portNumber < 0 || portNumber > 255) - { - return false; - } - } - catch (Exception e) - { - return false; - } - return true; - } - - // provided by the tty0tty driver - private static boolean isVirtualNullModemDevice(String portName) - { - return portName.startsWith("tnt"); - } - - // provided by the g_serial driver - private static boolean isGadgetDevice(String portName) - { - return portName.startsWith("ttyGS"); - } - - // provided by the MOXA driver - private static boolean isMoxaDevice(String portName) - { - return portName.startsWith(MOXA_IDENTIFIER); - } - - private static String devicePortId(UdevDevice dev) - { - return deviceProperty(dev, "ID_PATH"); - } - - private static String deviceDescription(UdevDevice dev) - { - String description = deviceProperty(dev, "ID_MODEL_FROM_DATABASE"); - - return (description != null) ? description.replace('_', ' ') : null; - } - - private static String deviceDescription(File targetDir) - { - return deviceProperty(new File(targetDir, "product").getAbsolutePath()); - } - - private static String deviceManufacturer(UdevDevice dev) - { - String manufacturer = deviceProperty(dev, "ID_VENDOR"); - - return (manufacturer != null) ? manufacturer.replace('_', ' ') : null; - } - - private static String deviceManufacturer(File targetDir) - { - return deviceProperty(new File(targetDir, "manufacturer").getAbsolutePath()); - } - - private static Number deviceProductIdentifier(UdevDevice dev) - { - Number identifierValue; - try - { - String indentifierText = deviceProperty(dev, "ID_MODEL_ID"); - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; - } - catch (Exception e) - { - identifierValue = null; - } - return identifierValue; - } - - private static String deviceProductName(UdevDevice dev) - { - return deviceProperty(dev, "ID_MODEL"); - } - - private static String deviceProductName(File targetDir) - { - return deviceProperty(new File(targetDir, "product").getAbsolutePath()); - } - - private static Number deviceProductIdentifier(File targetDir) - { - String indentifierText = deviceProperty(new File(targetDir, "idProduct").getAbsolutePath()); - - if (indentifierText == null) - { - indentifierText = deviceProperty(new File(targetDir, "device").getAbsolutePath()); - } - Number identifierValue; - try - { - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; - } - catch (Exception e) - { - identifierValue = null; - } - return identifierValue; - } - - private static Number deviceVendorIdentifier(File targetDir) - { - String indentifierText = deviceProperty(new File(targetDir, "idVendor").getAbsolutePath()); - - if (indentifierText == null) - { - indentifierText = deviceProperty(new File(targetDir, "vendor").getAbsolutePath()); - } - Number identifierValue; - try - { - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; - } - catch (Exception e) - { - identifierValue = null; - } - return identifierValue; - } - - private static String deviceSerialNumber(File targetDir) - { - return deviceProperty(new File(targetDir, "serial").getAbsolutePath()); - } - - private static Number deviceVendorIdentifier(UdevDevice dev) - { - Number identifierValue; - try - { - String indentifierText = deviceProperty(dev, "ID_VENDOR_ID"); - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; - } - catch (Exception e) - { - identifierValue = null; - } - return identifierValue; - } - - private static String deviceSerialNumber(UdevDevice dev) - { - return deviceProperty(dev, "ID_SERIAL_SHORT"); - } - - private static String deviceProperty(UdevDevice dev, String name) - { - return Udev.INSTANCE.udev_device_get_property_value(dev, name); - } - - private static String deviceProperty(String targetFilePath) - { - FileInputStream f; - - try - { - f = new FileInputStream(targetFilePath); - } - catch (FileNotFoundException e) - { - e.printStackTrace(); - return null; - } - String text; - try - { - int available = f.available(); - byte[] buffer = new byte[available]; - f.read(buffer); - text = new String(buffer); - } - catch (IOException e) - { - e.printStackTrace(); - return null; - } - finally - { - try - { - f.close(); - } - catch (IOException e) - { - e.printStackTrace(); - } - } - - return text.trim(); - } - - private static String deviceDriver(UdevDevice dev) - { - return null; - } - - private static String deviceDriver(File targetDir) - { - File deviceDir = new File(targetDir, "device"); - - return ueventProperty(deviceDir, "DRIVER="); - } - - private static String deviceName(UdevDevice dev) - { - return Udev.INSTANCE.udev_device_get_sysname(dev); - } - - static String deviceName(File targetDir) - { - return ueventProperty(targetDir, "DEVNAME="); - } - - private static String deviceLocation(UdevDevice dev) - { - String location = Udev.INSTANCE.udev_device_get_devnode(dev); - // Patch for MOXA device - // Udev prints location as /dev/ttyrN but only /dev/ttyr0N exists (with N belongs to [1-4]) - // So we are renaming location from /dev/ttyrN to /dev/ttyr0N - Matcher matcher = MOXA_PATTERN.matcher(location); - - if (matcher.matches()) - { - String prefix = ""; - String indexValue; - if (matcher.groupCount() == 2) - { - prefix = matcher.group(1); - indexValue = matcher.group(2); - } - else - { - indexValue = matcher.group(2); - } - int index = Integer.parseUnsignedInt(indexValue); - location = String.format("%s" + MOXA_FORMAT, prefix, index); - } - - return location; - } - - private static String portNameToSystemLocation(String source) - { - return (source.startsWith("/") || source.startsWith("./") || source.startsWith("../")) ? source : (("/dev/") + source); - } - - private static String portNameFromSystemLocation(String source) - { - return source.startsWith("/dev/") ? source.substring(5) : source; - } - - @SuppressWarnings("resource") - private static String ueventProperty(File targetDir, String pattern) - { - FileInputStream f; - - try - { - f = new FileInputStream(new File(targetDir, "uevent")); - } - catch (FileNotFoundException e) - { - e.printStackTrace(); - return null; - } - String content; - try - { - int available = f.available(); - byte[] buffer = new byte[available]; - f.read(buffer); - content = new String(buffer); - } - catch (IOException e) - { - e.printStackTrace(); - return null; - } - int firstbound = content.indexOf(pattern); - if (firstbound == -1) - { - return null; - } - int lastbound = content.indexOf('\n', firstbound); - - return content.substring(firstbound, lastbound); - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.serialport; + +import com.sun.jna.platform.linux.Udev; +import com.sun.jna.platform.linux.Udev.UdevContext; +import com.sun.jna.platform.linux.Udev.UdevDevice; +import com.sun.jna.platform.linux.Udev.UdevEnumerate; +import com.sun.jna.platform.linux.Udev.UdevListEntry; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Linux-specific implementation of serial port discovery. + * + *

    This class implements {@link SerialPortFinder} and provides specialized functionality for + * discovering serial ports on Linux systems. It uses multiple detection methods with fallback + * strategies to ensure comprehensive port discovery: + * + *

      + *
    • Udev - Primary method using the Linux udev device manager (most reliable) + *
    • Sysfs - Fallback method reading from /sys/class/tty + *
    • Device filtering - Final fallback scanning /dev for known device patterns + *
    + * + *

    The class supports a wide range of serial devices + * + *

    The class follows the singleton pattern with lazy initialization. + * + * @author Enedis Smarties team + * @see SerialPortFinder + * @see SerialPortDescriptor + */ +public class SerialPortFinderForLinux implements SerialPortFinder { + private static String MOXA_IDENTIFIER = "ttyr"; + private static Pattern MOXA_PATTERN = Pattern.compile("(.*)" + MOXA_IDENTIFIER + "(\\d+)"); + private static String MOXA_FORMAT = MOXA_IDENTIFIER + "%02d"; + + private static SerialPortFinderForLinux instance; + + /** + * Returns the singleton instance of the Linux serial port finder. + * + *

    This method performs lazy initialization and caches the instance for subsequent calls. + * + * @return the singleton SerialPortFinderForLinux instance + */ + public static SerialPortFinderForLinux getInstance() { + if (instance == null) { + instance = new SerialPortFinderForLinux(); + } + + return instance; + } + + /** + * Private constructor to prevent direct instantiation. + * + *

    Use {@link #getInstance()} to obtain the singleton instance. + */ + private SerialPortFinderForLinux() {} + + /** + * Finds all available serial ports on the Linux system. + * + *

    This method uses a cascading fallback strategy with three detection methods: + * + *

      + *
    1. Udev - Queries the Linux device manager for tty devices (preferred) + *
    2. Sysfs - Reads from /sys/class/tty if udev fails or returns no results + *
    3. Device filtering - Scans /dev for known serial device patterns as last resort + *
    + * + * @return a list of all serial port descriptors found on the system + */ + @Override + public DataList findAll() { + DataList serialPortDescriptorList = + new DataArrayList(); + + serialPortDescriptorList = availablePortsByUdev(); + if (serialPortDescriptorList.isEmpty()) { + serialPortDescriptorList = availablePortsBySysfs(); + } + if (serialPortDescriptorList.isEmpty()) { + serialPortDescriptorList = availablePortsByFiltersOfDevices(); + } + + return serialPortDescriptorList; + } + + /** + * Discovers serial ports using the Linux udev device manager. + * + *

    This is the primary and most reliable method for finding serial ports on Linux. It queries + * the udev subsystem for all tty devices, extracts USB device information (PID, VID, + * manufacturer, serial number), and creates descriptors for each port. + * + *

    The method filters out invalid serial8250 ports and includes special handling for rfcomm, + * virtual null modem, gadget serial, and MOXA devices. + * + * @return a list of serial port descriptors discovered via udev + */ + private static DataList availablePortsByUdev() { + UdevContext udev = Udev.INSTANCE.udev_new(); + UdevEnumerate enumerate = Udev.INSTANCE.udev_enumerate_new(udev); + Udev.INSTANCE.udev_enumerate_add_match_subsystem(enumerate, "tty"); + Udev.INSTANCE.udev_enumerate_scan_devices(enumerate); + + DataList serialPortDescriptorList = + new DataArrayList(); + + for (UdevListEntry dev_list_entry = Udev.INSTANCE.udev_enumerate_get_list_entry(enumerate); + dev_list_entry != null; + dev_list_entry = dev_list_entry.getNext()) { + UdevDevice device = + Udev.INSTANCE.udev_device_new_from_syspath( + udev, Udev.INSTANCE.udev_list_entry_get_name(dev_list_entry)); + if (device == null) { + break; + } + SerialPortDescriptor serialPortDescriptor; + + String location = deviceLocation(device); + String portName = deviceName(device); + + UdevDevice parentdev = Udev.INSTANCE.udev_device_get_parent(device); + + if (parentdev != null) { + String driverName = deviceDriver(parentdev); + if (isSerial8250Driver(driverName) && !isValidSerial8250(location)) { + Udev.INSTANCE.udev_device_unref(device); + continue; + } + String description = deviceDescription(device); + String portId = devicePortId(device); + String manufacturer = deviceManufacturer(device); + String serialNumber = deviceSerialNumber(device); + Number vendorIdentifier = deviceVendorIdentifier(device); + Number productIdentifier = deviceProductIdentifier(device); + String productName = deviceProductName(device); + try { + serialPortDescriptor = + new SerialPortDescriptor( + portId, + location, + description, + productIdentifier, + vendorIdentifier, + productName, + manufacturer, + serialNumber); + } catch (DataDictionaryException e) { + e.printStackTrace(); + Udev.INSTANCE.udev_device_unref(device); + continue; + } + } else { + if (!isRfcommDevice(portName) + && !isVirtualNullModemDevice(portName) + && !isGadgetDevice(portName) + && !isMoxaDevice(portName)) { + Udev.INSTANCE.udev_device_unref(device); + continue; + } + try { + serialPortDescriptor = + new SerialPortDescriptor(null, location, null, null, null, null, null, null); + } catch (DataDictionaryException e) { + e.printStackTrace(); + Udev.INSTANCE.udev_device_unref(device); + continue; + } + } + serialPortDescriptorList.add(serialPortDescriptor); + Udev.INSTANCE.udev_device_unref(device); + } + Udev.INSTANCE.udev_enumerate_unref(enumerate); + Udev.INSTANCE.udev_unref(udev); + + return serialPortDescriptorList; + } + + /** + * Discovers serial ports by reading from the sysfs filesystem. + * + *

    This method serves as a fallback when udev is not available or returns no results. It reads + * from /sys/class/tty and follows symbolic links to extract device information. The method + * traverses the device hierarchy to find USB properties. + * + * @return a list of serial port descriptors discovered via sysfs + */ + private static DataList availablePortsBySysfs() { + DataList serialPortDescriptorList = + new DataArrayList(); + File ttySysClassDir = new File("/sys/class/tty"); + + if (!(ttySysClassDir.exists() && ttySysClassDir.canRead())) { + return serialPortDescriptorList; + } + FileFilter filter = + new FileFilter() { + @Override + public boolean accept(File pathname) { + + return pathname.isDirectory() + && !pathname.getName().equals(".") + && !pathname.getName().equals(".."); + } + }; + File[] fileInfos = ttySysClassDir.listFiles(filter); + for (File fileInfo : fileInfos) { + if (!Files.isSymbolicLink(fileInfo.toPath())) { + continue; + } + File targetDir = fileInfo.getAbsoluteFile(); + SerialPortDescriptor serialPortDescriptor; + + String portName = deviceName(targetDir); + if (portName == null) { + continue; + } + String driverName = deviceDriver(targetDir); + if (driverName == null) { + if (!isRfcommDevice(portName) + && !isVirtualNullModemDevice(portName) + && !isGadgetDevice(portName) + && !isMoxaDevice(portName)) { + continue; + } + } + + String device = portNameToSystemLocation(portName); + if (isSerial8250Driver(driverName) && !isValidSerial8250(device)) { + continue; + } + String description = null; + String manufacturer = null; + String serialNumber = null; + Number vendorIdentifier = null; + Number productIdentifier = null; + String productName = null; + do { + if (description == null) { + description = deviceDescription(targetDir); + } + if (manufacturer == null) { + manufacturer = deviceManufacturer(targetDir); + } + if (serialNumber == null) { + serialNumber = deviceSerialNumber(targetDir); + } + if (vendorIdentifier == null) { + vendorIdentifier = deviceVendorIdentifier(targetDir); + } + if (productIdentifier == null) { + productIdentifier = deviceProductIdentifier(targetDir); + } + if (productName == null) { + productName = deviceProductName(targetDir); + } + if (description != null + || manufacturer != null + || serialNumber != null + || vendorIdentifier != null + || productIdentifier != null + || productName != null) { + break; + } + targetDir = targetDir.getParentFile(); + } while (targetDir != null); + try { + serialPortDescriptor = + new SerialPortDescriptor( + null, + portName, + description, + productIdentifier, + vendorIdentifier, + productName, + manufacturer, + serialNumber); + } catch (DataDictionaryException e) { + e.printStackTrace(); + continue; + } + serialPortDescriptorList.add(serialPortDescriptor); + } + + return serialPortDescriptorList; + } + + /** + * Discovers serial ports by scanning /dev for known device file patterns. + * + *

    This is the last resort fallback method when both udev and sysfs fail. It scans the /dev + * directory for files matching known serial port patterns (ttyS*, ttyUSB*, ttyACM*, etc.) and + * creates minimal descriptors without USB device information. + * + * @return a list of serial port descriptors discovered by device file filtering + */ + private static DataList availablePortsByFiltersOfDevices() { + DataList serialPortDescriptorList = + new DataArrayList(); + + List deviceFilePaths = filteredDeviceFilePaths(); + for (String deviceFilePath : deviceFilePaths) { + SerialPortDescriptor serialPortDescriptor; + String portName = portNameFromSystemLocation(deviceFilePath); + try { + serialPortDescriptor = + new SerialPortDescriptor(null, portName, null, null, null, null, null, null); + } catch (DataDictionaryException e) { + e.printStackTrace(); + continue; + } + serialPortDescriptorList.add(serialPortDescriptor); + } + + return serialPortDescriptorList; + } + + /** + * Returns a list of device file paths matching known serial port patterns. + * + *

    This method scans the /dev directory and filters files based on predefined patterns that + * correspond to various types of serial devices on Linux, FreeBSD, and QNX systems. It excludes + * symbolic links and certain non-serial files (.init, .lock). + * + * @return a list of absolute paths to potential serial port device files + */ + private static List filteredDeviceFilePaths() { + List deviceFileNameFilterList = new ArrayList(); + + // Linux + deviceFileNameFilterList.add("ttyS*"); // Standard UART 8250 and etc. + deviceFileNameFilterList.add("ttyO*"); // OMAP UART 8250 and etc. + deviceFileNameFilterList.add("ttyUSB*"); // Usb/serial converters PL2303 and etc. + deviceFileNameFilterList.add("ttyACM*"); // CDC_ACM converters (i.e. Mobile Phones). + deviceFileNameFilterList.add( + "ttyGS*"); // Gadget serial device (i.e. Mobile Phones with gadget serial driver). + deviceFileNameFilterList.add("ttyMI*"); // MOXA pci/serial converters. + deviceFileNameFilterList.add("ttymxc*"); // Motorola IMX serial ports (i.e. Freescale i.MX). + deviceFileNameFilterList.add( + "ttyAMA*"); // AMBA serial device for embedded platform on ARM (i.e. Raspberry Pi). + deviceFileNameFilterList.add( + "ttyTHS*"); // Serial device for embedded platform on ARM (i.e. Tegra Jetson TK1). + deviceFileNameFilterList.add("rfcomm*"); // Bluetooth serial device. + deviceFileNameFilterList.add("ircomm*"); // IrDA serial device. + deviceFileNameFilterList.add("tnt*"); // Virtual tty0tty serial device. + deviceFileNameFilterList.add(MOXA_IDENTIFIER + "*"); // MOXA ethernet device. + // FreeBSD + deviceFileNameFilterList.add("cu*"); + // QNX + deviceFileNameFilterList.add("ser*"); + + List result = new ArrayList(); + + File deviceDir = new File("/dev"); + if (deviceDir.exists()) { + FileFilter deviceFileNameFilter = + new FileFilter() { + @Override + public boolean accept(File pathname) { + Path path = pathname.toPath(); + + return deviceFileNameFilterList.contains(pathname.getName()) + && pathname.isFile() + && !Files.isSymbolicLink(path); + } + }; + File[] deviceFileInfos = deviceDir.listFiles(deviceFileNameFilter); + for (File deviceFileInfo : deviceFileInfos) { + String deviceAbsoluteFilePath = deviceFileInfo.getAbsolutePath(); + // it is a quick workaround to skip the non-serial devices (FreeBSD) + if (deviceAbsoluteFilePath.endsWith(".init") || deviceAbsoluteFilePath.endsWith(".lock")) { + continue; + } + if (!result.contains(deviceAbsoluteFilePath)) { + result.add(deviceAbsoluteFilePath); + } + } + } + + return result; + } + + private static boolean isSerial8250Driver(String driverName) { + return (driverName == "serial8250"); + } + + private static boolean isValidSerial8250(String systemLocation) { + return false; + } + + private static boolean isRfcommDevice(String portName) { + if (!portName.startsWith("rfcomm")) { + return false; + } + try { + String number = portName.substring(6); + int portNumber = Integer.parseInt(number); + if (portNumber < 0 || portNumber > 255) { + return false; + } + } catch (Exception e) { + return false; + } + return true; + } + + // provided by the tty0tty driver + private static boolean isVirtualNullModemDevice(String portName) { + return portName.startsWith("tnt"); + } + + // provided by the g_serial driver + private static boolean isGadgetDevice(String portName) { + return portName.startsWith("ttyGS"); + } + + // provided by the MOXA driver + private static boolean isMoxaDevice(String portName) { + return portName.startsWith(MOXA_IDENTIFIER); + } + + private static String devicePortId(UdevDevice dev) { + return deviceProperty(dev, "ID_PATH"); + } + + private static String deviceDescription(UdevDevice dev) { + String description = deviceProperty(dev, "ID_MODEL_FROM_DATABASE"); + + return (description != null) ? description.replace('_', ' ') : null; + } + + private static String deviceDescription(File targetDir) { + return deviceProperty(new File(targetDir, "product").getAbsolutePath()); + } + + private static String deviceManufacturer(UdevDevice dev) { + String manufacturer = deviceProperty(dev, "ID_VENDOR"); + + return (manufacturer != null) ? manufacturer.replace('_', ' ') : null; + } + + private static String deviceManufacturer(File targetDir) { + return deviceProperty(new File(targetDir, "manufacturer").getAbsolutePath()); + } + + private static Number deviceProductIdentifier(UdevDevice dev) { + Number identifierValue; + try { + String indentifierText = deviceProperty(dev, "ID_MODEL_ID"); + identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + } catch (Exception e) { + identifierValue = null; + } + return identifierValue; + } + + private static String deviceProductName(UdevDevice dev) { + return deviceProperty(dev, "ID_MODEL"); + } + + private static String deviceProductName(File targetDir) { + return deviceProperty(new File(targetDir, "product").getAbsolutePath()); + } + + private static Number deviceProductIdentifier(File targetDir) { + String indentifierText = deviceProperty(new File(targetDir, "idProduct").getAbsolutePath()); + + if (indentifierText == null) { + indentifierText = deviceProperty(new File(targetDir, "device").getAbsolutePath()); + } + Number identifierValue; + try { + identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + } catch (Exception e) { + identifierValue = null; + } + return identifierValue; + } + + private static Number deviceVendorIdentifier(File targetDir) { + String indentifierText = deviceProperty(new File(targetDir, "idVendor").getAbsolutePath()); + + if (indentifierText == null) { + indentifierText = deviceProperty(new File(targetDir, "vendor").getAbsolutePath()); + } + Number identifierValue; + try { + identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + } catch (Exception e) { + identifierValue = null; + } + return identifierValue; + } + + private static String deviceSerialNumber(File targetDir) { + return deviceProperty(new File(targetDir, "serial").getAbsolutePath()); + } + + private static Number deviceVendorIdentifier(UdevDevice dev) { + Number identifierValue; + try { + String indentifierText = deviceProperty(dev, "ID_VENDOR_ID"); + identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + } catch (Exception e) { + identifierValue = null; + } + return identifierValue; + } + + private static String deviceSerialNumber(UdevDevice dev) { + return deviceProperty(dev, "ID_SERIAL_SHORT"); + } + + private static String deviceProperty(UdevDevice dev, String name) { + return Udev.INSTANCE.udev_device_get_property_value(dev, name); + } + + private static String deviceProperty(String targetFilePath) { + FileInputStream f; + + try { + f = new FileInputStream(targetFilePath); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } + String text; + try { + int available = f.available(); + byte[] buffer = new byte[available]; + f.read(buffer); + text = new String(buffer); + } catch (IOException e) { + e.printStackTrace(); + return null; + } finally { + try { + f.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return text.trim(); + } + + private static String deviceDriver(UdevDevice dev) { + return null; + } + + private static String deviceDriver(File targetDir) { + File deviceDir = new File(targetDir, "device"); + + return ueventProperty(deviceDir, "DRIVER="); + } + + private static String deviceName(UdevDevice dev) { + return Udev.INSTANCE.udev_device_get_sysname(dev); + } + + static String deviceName(File targetDir) { + return ueventProperty(targetDir, "DEVNAME="); + } + + private static String deviceLocation(UdevDevice dev) { + String location = Udev.INSTANCE.udev_device_get_devnode(dev); + // Patch for MOXA device + // Udev prints location as /dev/ttyrN but only /dev/ttyr0N exists (with N belongs to [1-4]) + // So we are renaming location from /dev/ttyrN to /dev/ttyr0N + Matcher matcher = MOXA_PATTERN.matcher(location); + + if (matcher.matches()) { + String prefix = ""; + String indexValue; + if (matcher.groupCount() == 2) { + prefix = matcher.group(1); + indexValue = matcher.group(2); + } else { + indexValue = matcher.group(2); + } + int index = Integer.parseUnsignedInt(indexValue); + location = String.format("%s" + MOXA_FORMAT, prefix, index); + } + + return location; + } + + private static String portNameToSystemLocation(String source) { + return (source.startsWith("/") || source.startsWith("./") || source.startsWith("../")) + ? source + : (("/dev/") + source); + } + + private static String portNameFromSystemLocation(String source) { + return source.startsWith("/dev/") ? source.substring(5) : source; + } + + @SuppressWarnings("resource") + private static String ueventProperty(File targetDir, String pattern) { + FileInputStream f; + + try { + f = new FileInputStream(new File(targetDir, "uevent")); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } + String content; + try { + int available = f.available(); + byte[] buffer = new byte[available]; + f.read(buffer); + content = new String(buffer); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + int firstbound = content.indexOf(pattern); + if (firstbound == -1) { + return null; + } + int lastbound = content.indexOf('\n', firstbound); + + return content.substring(firstbound, lastbound); + } +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java index 6f66a58..5cbdfe0 100644 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java +++ b/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java @@ -1,455 +1,437 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.serialport; - -import static com.sun.jna.platform.win32.SetupApi.DICS_FLAG_GLOBAL; -import static com.sun.jna.platform.win32.SetupApi.DIGCF_DEVICEINTERFACE; -import static com.sun.jna.platform.win32.SetupApi.DIGCF_PRESENT; -import static com.sun.jna.platform.win32.SetupApi.DIREG_DEV; -import static com.sun.jna.platform.win32.SetupApi.GUID_DEVINTERFACE_COMPORT; -import static com.sun.jna.platform.win32.SetupApi.SPDRP_DEVICEDESC; -import static com.sun.jna.platform.win32.WinBase.INVALID_HANDLE_VALUE; -import static com.sun.jna.platform.win32.WinDef.MAX_PATH; -import static com.sun.jna.platform.win32.WinError.ERROR_SUCCESS; -import static com.sun.jna.platform.win32.WinNT.KEY_QUERY_VALUE; -import static com.sun.jna.platform.win32.WinNT.KEY_READ; -import static com.sun.jna.platform.win32.WinNT.REG_DWORD; -import static com.sun.jna.platform.win32.WinNT.REG_SZ; -import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE; - -import java.util.ArrayList; -import java.util.List; - -import com.sun.jna.Memory; -import com.sun.jna.Pointer; -import com.sun.jna.platform.win32.Advapi32; -import com.sun.jna.platform.win32.Advapi32Util; -import com.sun.jna.platform.win32.Cfgmgr32; -import com.sun.jna.platform.win32.Cfgmgr32Util; -import com.sun.jna.platform.win32.Guid.GUID; -import com.sun.jna.platform.win32.SetupApi; -import com.sun.jna.platform.win32.SetupApi.SP_DEVINFO_DATA; -import com.sun.jna.platform.win32.Win32Exception; -import com.sun.jna.platform.win32.WinNT.HANDLE; -import com.sun.jna.platform.win32.WinReg.HKEY; -import com.sun.jna.ptr.IntByReference; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * Class used to find all serial port descriptor for Windows - */ -public class SerialPortFinderForWindows implements SerialPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final int SPDRP_MFG = 0x0000000B; - private static final int SPDRP_ADDRESS = 0x0000001C; - private static final int PROPERTY_MAX_SIZE = 1024; - - private static final GUID GUID_DEVCLASS_PORTS = new GUID("{4d36e978-e325-11ce-bfc1-08002be10318}"); - private static final GUID GUID_DEVCLASS_MODEM = new GUID("{4d36e96d-e325-11ce-bfc1-08002be10318}"); - private static final GUID GUID_DEVINTERFACE_MODEM = new GUID("{2c7089aa-2e0e-11d1-b114-00c04fc2aae4}"); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get instance - * - * @return Unique instance - */ - public static SerialPortFinderForWindows getInstance() - { - if(instance == null) - { - instance = new SerialPortFinderForWindows(); - } - - return instance; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static SerialPortFinderForWindows instance; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private SerialPortFinderForWindows() - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortFinder - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataList findAll() - { - final class SetupToken - { - public GUID guid; - public int flags; - - public SetupToken(GUID guid, int flags) - { - super(); - this.guid = guid; - this.flags = flags; - } - } - - SetupToken[] setupTokens = new SetupToken[] { new SetupToken(GUID_DEVCLASS_PORTS, DIGCF_PRESENT), new SetupToken(GUID_DEVCLASS_MODEM, DIGCF_PRESENT), - new SetupToken(GUID_DEVINTERFACE_COMPORT, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE), new SetupToken(GUID_DEVINTERFACE_MODEM, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE) }; - - DataList serialPortDescriptorList = new DataArrayList(); - - for (int i = 0; i < setupTokens.length; ++i) - { - HANDLE deviceInfoSet = SetupApi.INSTANCE.SetupDiGetClassDevs(setupTokens[i].guid, null, null, setupTokens[i].flags); - if (deviceInfoSet == INVALID_HANDLE_VALUE) - { - return serialPortDescriptorList; - } - SP_DEVINFO_DATA deviceInfoData = new SP_DEVINFO_DATA(); - int index = 0; - while (SetupApi.INSTANCE.SetupDiEnumDeviceInfo(deviceInfoSet, index++, deviceInfoData)) - { - SerialPortDescriptor serialPortDescriptor; - - String portName = devicePortName(deviceInfoSet, deviceInfoData); - if (portName == null || portName.isEmpty() || portName.contains("LPT")) - { - continue; - } - if (anyOfPorts(serialPortDescriptorList, portName)) - { - continue; - } - String description = deviceDescription(deviceInfoSet, deviceInfoData); - String address = deviceAddress(deviceInfoSet, deviceInfoData); - String manufacturer = deviceManufacturer(deviceInfoSet, deviceInfoData); - String instanceIdentifier = deviceInstanceIdentifier(deviceInfoData.DevInst); - String serialNumber = deviceSerialNumber(instanceIdentifier, deviceInfoData.DevInst); - Number vendorIdentifier = deviceVendorIdentifier(instanceIdentifier); - Number productIdentifier = deviceProductIdentifier(instanceIdentifier); - try - { - serialPortDescriptor = new SerialPortDescriptor(address, portName, description, productIdentifier, vendorIdentifier, null, manufacturer, serialNumber); - } - catch (DataDictionaryException e) - { - e.printStackTrace(System.err); - continue; - } - serialPortDescriptorList.add(serialPortDescriptor); - - } - SetupApi.INSTANCE.SetupDiDestroyDeviceInfoList(deviceInfoSet); - } - - List portNames = portNamesFromHardwareDeviceMap(); - for (String portName : portNames) - { - if (!anyOfPorts(serialPortDescriptorList, portName)) - { - try - { - SerialPortDescriptor serialPortDescriptor = new SerialPortDescriptor(null, portName, null, null, null, null, null, null); - serialPortDescriptorList.add(serialPortDescriptor); - } - catch (DataDictionaryException e) - { - e.printStackTrace(System.err); - } - } - } - - return serialPortDescriptorList; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static boolean anyOfPorts(DataList descriptors, String portName) - { - if (descriptors == null) - { - return false; - } - for (SerialPortDescriptor descriptor : descriptors) - { - if (descriptor.getPortName() == null) - { - if (portName != null) - { - continue; - } - else - { - return true; - } - } - if (descriptor.getPortName().equals(portName)) - { - return true; - } - } - - return false; - } - - private static String devicePortName(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) - { - HKEY key = SetupApi.INSTANCE.SetupDiOpenDevRegKey(deviceInfoSet, deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); - - if (key == INVALID_HANDLE_VALUE) - { - return null; - } - String[] keyTokens = { "PortName", "PortNumber" }; - String portName = null; - for (int i = 0; i < keyTokens.length && portName == null; i++) - { - portName = Advapi32Util.registryGetStringValue(key, keyTokens[i]); - } - - return portName; - } - - private static String deviceDescription(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) - { - return (String) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_DEVICEDESC); - } - - private static String deviceManufacturer(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) - { - return (String) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_MFG); - } - - private static String deviceAddress(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) - { - Integer address = (Integer) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_ADDRESS); - return (address != null) ? address.toString() : null; - } - - private static Number deviceVendorIdentifier(String instanceIdentifier) - { - final int vendorIdentifierSize = 4; - - Number result = parseDeviceIdentifier(instanceIdentifier, "VID_", vendorIdentifierSize); - if (result == null) - { - result = parseDeviceIdentifier(instanceIdentifier, "VEN_", vendorIdentifierSize); - } - - return result; - } - - private static Number deviceProductIdentifier(String instanceIdentifier) - { - final int productIdentifierSize = 4; - - Number result = parseDeviceIdentifier(instanceIdentifier, "PID_", productIdentifierSize); - if (result == null) - { - result = parseDeviceIdentifier(instanceIdentifier, "DEV_", productIdentifierSize); - } - - return result; - } - - private static String deviceInstanceIdentifier(int deviceInstanceNumber) - { - return Cfgmgr32Util.CM_Get_Device_ID(deviceInstanceNumber); - } - - private static String deviceSerialNumber(String instanceIdentifier, int deviceInstanceNumber) - { - for (;;) - { - String serialNumber = parseDeviceSerialNumber(instanceIdentifier); - if (serialNumber != null) - { - return serialNumber; - } - deviceInstanceNumber = parentDeviceInstanceNumber(deviceInstanceNumber); - if (deviceInstanceNumber == 0) - { - break; - } - instanceIdentifier = deviceInstanceIdentifier(deviceInstanceNumber); - if (instanceIdentifier.isEmpty()) - { - break; - } - } - - return null; - } - - private static Object deviceRegistryProperty(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData, int property) - { - Object propertyValue = null; - IntByReference dataType = new IntByReference(0); - Pointer outputBuffer = new Memory(PROPERTY_MAX_SIZE); - IntByReference bytesRequired = new IntByReference(PROPERTY_MAX_SIZE); - - if (SetupApi.INSTANCE.SetupDiGetDeviceRegistryProperty(deviceInfoSet, deviceInfoData, property, dataType, outputBuffer, bytesRequired.getValue(), bytesRequired)) - { - switch (dataType.getValue()) - { - case REG_DWORD: - propertyValue = outputBuffer.getInt(0); - break; - case REG_SZ: - propertyValue = outputBuffer.getWideString(0); - break; - } - } - - return propertyValue; - } - - private static int parentDeviceInstanceNumber(int childDeviceInstanceNumber) - { - IntByReference parentInstanceNumber = new IntByReference(0); - if (Cfgmgr32.INSTANCE.CM_Get_Parent(parentInstanceNumber, childDeviceInstanceNumber, 0) != Cfgmgr32.CR_SUCCESS) - { - return 0; - } - return parentInstanceNumber.getValue(); - } - - private static String parseDeviceSerialNumber(String instanceIdentifier) - { - int firstbound = instanceIdentifier.lastIndexOf('\\'); - int lastbound = instanceIdentifier.indexOf('_', firstbound); - if (instanceIdentifier.startsWith("USB\\")) - { - if (lastbound != instanceIdentifier.length() - 3) - lastbound = instanceIdentifier.length(); - int ampersand = instanceIdentifier.indexOf('&', firstbound); - if (ampersand != -1 && ampersand < lastbound) - return null; - } - else if (instanceIdentifier.startsWith("FTDIBUS\\")) - { - firstbound = instanceIdentifier.lastIndexOf('+'); - lastbound = instanceIdentifier.indexOf('\\', firstbound); - if (lastbound == -1) - return null; - } - else - { - return null; - } - - return instanceIdentifier.substring(firstbound + 1, lastbound); - } - - private static Number parseDeviceIdentifier(String instanceIdentifier, String identifierPrefix, int identifierSize) - { - int index = instanceIdentifier.indexOf(identifierPrefix); - - if (index == -1) - { - return null; - } - String indentifierText = instanceIdentifier.substring(index + identifierPrefix.length(), index + identifierPrefix.length() + identifierSize); - Number identifierValue; - try - { - identifierValue = Integer.parseInt(indentifierText, 16); - } - catch (Exception e) - { - identifierValue = null; - } - - return identifierValue; - } - - private static List portNamesFromHardwareDeviceMap() - { - List result = new ArrayList(); - HKEY hKey = null; - try - { - hKey = Advapi32Util.registryGetKey(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM", KEY_QUERY_VALUE).getValue(); - } - catch(Win32Exception exception) - { - return result; - } - int index = 0; - for (;;) - { - // This is a maximum length of value name, see: - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724872%28v=vs.85%29.aspx - IntByReference requiredValueNameChars = new IntByReference(16383); - char[] outputValueName = new char[requiredValueNameChars.getValue()]; - Pointer outputBuffer = new Memory(MAX_PATH); - IntByReference bytesRequired = new IntByReference(MAX_PATH); - int ret = Advapi32.INSTANCE.RegEnumValue(hKey, index, outputValueName, requiredValueNameChars, null, null, outputBuffer, bytesRequired); - if (ret == ERROR_SUCCESS) - { - result.add(outputBuffer.getWideString(0)); - ++index; - } - else - { - break; - } - } - Advapi32.INSTANCE.RegCloseKey(hKey); - - return result; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.serialport; + +import static com.sun.jna.platform.win32.SetupApi.DICS_FLAG_GLOBAL; +import static com.sun.jna.platform.win32.SetupApi.DIGCF_DEVICEINTERFACE; +import static com.sun.jna.platform.win32.SetupApi.DIGCF_PRESENT; +import static com.sun.jna.platform.win32.SetupApi.DIREG_DEV; +import static com.sun.jna.platform.win32.SetupApi.GUID_DEVINTERFACE_COMPORT; +import static com.sun.jna.platform.win32.SetupApi.SPDRP_DEVICEDESC; +import static com.sun.jna.platform.win32.WinBase.INVALID_HANDLE_VALUE; +import static com.sun.jna.platform.win32.WinDef.MAX_PATH; +import static com.sun.jna.platform.win32.WinError.ERROR_SUCCESS; +import static com.sun.jna.platform.win32.WinNT.KEY_QUERY_VALUE; +import static com.sun.jna.platform.win32.WinNT.KEY_READ; +import static com.sun.jna.platform.win32.WinNT.REG_DWORD; +import static com.sun.jna.platform.win32.WinNT.REG_SZ; +import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; +import com.sun.jna.platform.win32.Advapi32; +import com.sun.jna.platform.win32.Advapi32Util; +import com.sun.jna.platform.win32.Cfgmgr32; +import com.sun.jna.platform.win32.Cfgmgr32Util; +import com.sun.jna.platform.win32.Guid.GUID; +import com.sun.jna.platform.win32.SetupApi; +import com.sun.jna.platform.win32.SetupApi.SP_DEVINFO_DATA; +import com.sun.jna.platform.win32.Win32Exception; +import com.sun.jna.platform.win32.WinNT.HANDLE; +import com.sun.jna.platform.win32.WinReg.HKEY; +import com.sun.jna.ptr.IntByReference; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; +import java.util.ArrayList; +import java.util.List; + +/** + * Windows-specific implementation of serial port discovery. + * + *

    This class implements {@link SerialPortFinder} and provides specialized functionality for + * discovering serial ports on Windows systems. It uses the Windows Setup API and registry access to + * enumerate COM ports and modem devices. + * + *

    The class extracts detailed information about each port including USB device identifiers + * (VID/PID), manufacturer, serial numbers, and device descriptions. It supports both hardware COM + * ports and USB-to-serial adapters. + * + *

    The class follows the singleton pattern with lazy initialization. + * + * @author Enedis Smarties team + * @see SerialPortFinder + * @see SerialPortDescriptor + */ +public class SerialPortFinderForWindows implements SerialPortFinder { + private static final int SPDRP_MFG = 0x0000000B; + private static final int SPDRP_ADDRESS = 0x0000001C; + private static final int PROPERTY_MAX_SIZE = 1024; + + private static final GUID GUID_DEVCLASS_PORTS = + new GUID("{4d36e978-e325-11ce-bfc1-08002be10318}"); + private static final GUID GUID_DEVCLASS_MODEM = + new GUID("{4d36e96d-e325-11ce-bfc1-08002be10318}"); + private static final GUID GUID_DEVINTERFACE_MODEM = + new GUID("{2c7089aa-2e0e-11d1-b114-00c04fc2aae4}"); + + private static SerialPortFinderForWindows instance; + + /** + * Returns the singleton instance of the Windows serial port finder. + * + *

    This method performs lazy initialization and caches the instance for subsequent calls. + * + * @return the singleton SerialPortFinderForWindows instance + */ + public static SerialPortFinderForWindows getInstance() { + if (instance == null) { + instance = new SerialPortFinderForWindows(); + } + + return instance; + } + + /** + * Private constructor to prevent direct instantiation. + * + *

    Use {@link #getInstance()} to obtain the singleton instance. + */ + private SerialPortFinderForWindows() {} + + /** + * Finds all available serial ports on the Windows system. + * + *

    This method uses the Windows Setup API to enumerate devices across multiple device classes + * and interfaces: + * + *

      + *
    • Ports device class (GUID_DEVCLASS_PORTS) + *
    • Modem device class (GUID_DEVCLASS_MODEM) + *
    • COM port device interface (GUID_DEVINTERFACE_COMPORT) + *
    • Modem device interface (GUID_DEVINTERFACE_MODEM) + *
    + * + *

    For each device, it extracts port name, description, manufacturer, USB VID/PID, and serial + * number. The method filters out LPT (parallel) ports and duplicates. As a fallback, it scans the + * Windows registry HARDWARE\DEVICEMAP\SERIALCOMM to find any ports not discovered through + * SetupAPI. + * + * @return a list of all serial port descriptors found on the system + */ + @Override + public DataList findAll() { + final class SetupToken { + public GUID guid; + public int flags; + + public SetupToken(GUID guid, int flags) { + super(); + this.guid = guid; + this.flags = flags; + } + } + + SetupToken[] setupTokens = + new SetupToken[] { + new SetupToken(GUID_DEVCLASS_PORTS, DIGCF_PRESENT), + new SetupToken(GUID_DEVCLASS_MODEM, DIGCF_PRESENT), + new SetupToken(GUID_DEVINTERFACE_COMPORT, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE), + new SetupToken(GUID_DEVINTERFACE_MODEM, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE) + }; + + DataList serialPortDescriptorList = + new DataArrayList(); + + for (int i = 0; i < setupTokens.length; ++i) { + HANDLE deviceInfoSet = + SetupApi.INSTANCE.SetupDiGetClassDevs( + setupTokens[i].guid, null, null, setupTokens[i].flags); + if (deviceInfoSet == INVALID_HANDLE_VALUE) { + return serialPortDescriptorList; + } + SP_DEVINFO_DATA deviceInfoData = new SP_DEVINFO_DATA(); + int index = 0; + while (SetupApi.INSTANCE.SetupDiEnumDeviceInfo(deviceInfoSet, index++, deviceInfoData)) { + SerialPortDescriptor serialPortDescriptor; + + String portName = devicePortName(deviceInfoSet, deviceInfoData); + if (portName == null || portName.isEmpty() || portName.contains("LPT")) { + continue; + } + if (anyOfPorts(serialPortDescriptorList, portName)) { + continue; + } + String description = deviceDescription(deviceInfoSet, deviceInfoData); + String address = deviceAddress(deviceInfoSet, deviceInfoData); + String manufacturer = deviceManufacturer(deviceInfoSet, deviceInfoData); + String instanceIdentifier = deviceInstanceIdentifier(deviceInfoData.DevInst); + String serialNumber = deviceSerialNumber(instanceIdentifier, deviceInfoData.DevInst); + Number vendorIdentifier = deviceVendorIdentifier(instanceIdentifier); + Number productIdentifier = deviceProductIdentifier(instanceIdentifier); + try { + serialPortDescriptor = + new SerialPortDescriptor( + address, + portName, + description, + productIdentifier, + vendorIdentifier, + null, + manufacturer, + serialNumber); + } catch (DataDictionaryException e) { + e.printStackTrace(System.err); + continue; + } + serialPortDescriptorList.add(serialPortDescriptor); + } + SetupApi.INSTANCE.SetupDiDestroyDeviceInfoList(deviceInfoSet); + } + + List portNames = portNamesFromHardwareDeviceMap(); + for (String portName : portNames) { + if (!anyOfPorts(serialPortDescriptorList, portName)) { + try { + SerialPortDescriptor serialPortDescriptor = + new SerialPortDescriptor(null, portName, null, null, null, null, null, null); + serialPortDescriptorList.add(serialPortDescriptor); + } catch (DataDictionaryException e) { + e.printStackTrace(System.err); + } + } + } + + return serialPortDescriptorList; + } + + /** + * Checks if a port with the given name already exists in the descriptor list. + * + * @param descriptors the list of serial port descriptors to check + * @param portName the port name to search for + * @return true if the port name is found in the list, false otherwise + */ + private static boolean anyOfPorts(DataList descriptors, String portName) { + if (descriptors == null) { + return false; + } + for (SerialPortDescriptor descriptor : descriptors) { + if (descriptor.getPortName() == null) { + if (portName != null) { + continue; + } else { + return true; + } + } + if (descriptor.getPortName().equals(portName)) { + return true; + } + } + + return false; + } + + /** + * Retrieves the port name (e.g., COM1, COM2) for a device from the Windows registry. + * + * @param deviceInfoSet handle to the device information set + * @param deviceInfoData device information data structure + * @return the port name string, or null if not found + */ + private static String devicePortName(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) { + HKEY key = + SetupApi.INSTANCE.SetupDiOpenDevRegKey( + deviceInfoSet, deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); + + if (key == INVALID_HANDLE_VALUE) { + return null; + } + String[] keyTokens = {"PortName", "PortNumber"}; + String portName = null; + for (int i = 0; i < keyTokens.length && portName == null; i++) { + portName = Advapi32Util.registryGetStringValue(key, keyTokens[i]); + } + + return portName; + } + + private static String deviceDescription(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) { + return (String) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_DEVICEDESC); + } + + private static String deviceManufacturer(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) { + return (String) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_MFG); + } + + private static String deviceAddress(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) { + Integer address = + (Integer) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_ADDRESS); + return (address != null) ? address.toString() : null; + } + + private static Number deviceVendorIdentifier(String instanceIdentifier) { + final int vendorIdentifierSize = 4; + + Number result = parseDeviceIdentifier(instanceIdentifier, "VID_", vendorIdentifierSize); + if (result == null) { + result = parseDeviceIdentifier(instanceIdentifier, "VEN_", vendorIdentifierSize); + } + + return result; + } + + private static Number deviceProductIdentifier(String instanceIdentifier) { + final int productIdentifierSize = 4; + + Number result = parseDeviceIdentifier(instanceIdentifier, "PID_", productIdentifierSize); + if (result == null) { + result = parseDeviceIdentifier(instanceIdentifier, "DEV_", productIdentifierSize); + } + + return result; + } + + private static String deviceInstanceIdentifier(int deviceInstanceNumber) { + return Cfgmgr32Util.CM_Get_Device_ID(deviceInstanceNumber); + } + + private static String deviceSerialNumber(String instanceIdentifier, int deviceInstanceNumber) { + for (; ; ) { + String serialNumber = parseDeviceSerialNumber(instanceIdentifier); + if (serialNumber != null) { + return serialNumber; + } + deviceInstanceNumber = parentDeviceInstanceNumber(deviceInstanceNumber); + if (deviceInstanceNumber == 0) { + break; + } + instanceIdentifier = deviceInstanceIdentifier(deviceInstanceNumber); + if (instanceIdentifier.isEmpty()) { + break; + } + } + + return null; + } + + private static Object deviceRegistryProperty( + HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData, int property) { + Object propertyValue = null; + IntByReference dataType = new IntByReference(0); + Pointer outputBuffer = new Memory(PROPERTY_MAX_SIZE); + IntByReference bytesRequired = new IntByReference(PROPERTY_MAX_SIZE); + + if (SetupApi.INSTANCE.SetupDiGetDeviceRegistryProperty( + deviceInfoSet, + deviceInfoData, + property, + dataType, + outputBuffer, + bytesRequired.getValue(), + bytesRequired)) { + switch (dataType.getValue()) { + case REG_DWORD: + propertyValue = outputBuffer.getInt(0); + break; + case REG_SZ: + propertyValue = outputBuffer.getWideString(0); + break; + } + } + + return propertyValue; + } + + private static int parentDeviceInstanceNumber(int childDeviceInstanceNumber) { + IntByReference parentInstanceNumber = new IntByReference(0); + if (Cfgmgr32.INSTANCE.CM_Get_Parent(parentInstanceNumber, childDeviceInstanceNumber, 0) + != Cfgmgr32.CR_SUCCESS) { + return 0; + } + return parentInstanceNumber.getValue(); + } + + private static String parseDeviceSerialNumber(String instanceIdentifier) { + int firstbound = instanceIdentifier.lastIndexOf('\\'); + int lastbound = instanceIdentifier.indexOf('_', firstbound); + if (instanceIdentifier.startsWith("USB\\")) { + if (lastbound != instanceIdentifier.length() - 3) lastbound = instanceIdentifier.length(); + int ampersand = instanceIdentifier.indexOf('&', firstbound); + if (ampersand != -1 && ampersand < lastbound) return null; + } else if (instanceIdentifier.startsWith("FTDIBUS\\")) { + firstbound = instanceIdentifier.lastIndexOf('+'); + lastbound = instanceIdentifier.indexOf('\\', firstbound); + if (lastbound == -1) return null; + } else { + return null; + } + + return instanceIdentifier.substring(firstbound + 1, lastbound); + } + + private static Number parseDeviceIdentifier( + String instanceIdentifier, String identifierPrefix, int identifierSize) { + int index = instanceIdentifier.indexOf(identifierPrefix); + + if (index == -1) { + return null; + } + String indentifierText = + instanceIdentifier.substring( + index + identifierPrefix.length(), index + identifierPrefix.length() + identifierSize); + Number identifierValue; + try { + identifierValue = Integer.parseInt(indentifierText, 16); + } catch (Exception e) { + identifierValue = null; + } + + return identifierValue; + } + + /** + * Retrieves port names from the Windows registry HARDWARE\DEVICEMAP\SERIALCOMM key. + * + *

    This method serves as a fallback to discover serial ports that may not be detected through + * the Setup API. It directly reads the registry key where Windows maps device identifiers to COM + * port names. + * + * @return a list of port names found in the registry + */ + private static List portNamesFromHardwareDeviceMap() { + List result = new ArrayList(); + HKEY hKey = null; + try { + hKey = + Advapi32Util.registryGetKey( + HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM", KEY_QUERY_VALUE) + .getValue(); + } catch (Win32Exception exception) { + return result; + } + int index = 0; + for (; ; ) { + // This is a maximum length of value name, see: + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724872%28v=vs.85%29.aspx + IntByReference requiredValueNameChars = new IntByReference(16383); + char[] outputValueName = new char[requiredValueNameChars.getValue()]; + Pointer outputBuffer = new Memory(MAX_PATH); + IntByReference bytesRequired = new IntByReference(MAX_PATH); + int ret = + Advapi32.INSTANCE.RegEnumValue( + hKey, + index, + outputValueName, + requiredValueNameChars, + null, + null, + outputBuffer, + bytesRequired); + if (ret == ERROR_SUCCESS) { + result.add(outputBuffer.getWideString(0)); + ++index; + } else { + break; + } + } + Advapi32.INSTANCE.RegCloseKey(hKey); + + return result; + } +} diff --git a/src/main/java/enedis/lab/io/tic/TICModemType.java b/src/main/java/enedis/lab/io/tic/TICModemType.java index b1f3688..c99f0bd 100644 --- a/src/main/java/enedis/lab/io/tic/TICModemType.java +++ b/src/main/java/enedis/lab/io/tic/TICModemType.java @@ -1,48 +1,42 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -/** - * TIC Modem type - */ -public enum TICModemType -{ - /** Modem michaud */ - MICHAUD(0x6001, 0x0403), - /** Télé info */ - TELEINFO(0x6015, 0x0403); - - private int productId; - private int vendorId; - - TICModemType(int productId, int vendorId) - { - this.productId = productId; - this.vendorId = vendorId; - } - - /** - * Get product id - * - * @return product id - */ - public int getProductId() - { - return this.productId; - } - - /** - * Get vendor id - * - * @return vendor id - */ - public int getVendorId() - { - return this.vendorId; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +/** TIC Modem type */ +public enum TICModemType { + /** Modem michaud */ + MICHAUD(0x6001, 0x0403), + /** Télé info */ + TELEINFO(0x6015, 0x0403); + + private int productId; + private int vendorId; + + TICModemType(int productId, int vendorId) { + this.productId = productId; + this.vendorId = vendorId; + } + + /** + * Get product id + * + * @return product id + */ + public int getProductId() { + return this.productId; + } + + /** + * Get vendor id + * + * @return vendor id + */ + public int getVendorId() { + return this.vendorId; + } +} diff --git a/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java b/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java index 675a073..9b48ccc 100644 --- a/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java +++ b/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java @@ -1,291 +1,292 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.io.serialport.SerialPortDescriptor; -import enedis.lab.io.usb.USBPortDescriptor; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; - -/** - * TICPortDescriptor class - * - * Generated - */ -public class TICPortDescriptor extends SerialPortDescriptor -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_MODEM_TYPE = "modemType"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kModemType; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected TICPortDescriptor() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICPortDescriptor(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICPortDescriptor(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param portId - * @param portName - * @param description - * @param productName - * @param manufacturer - * @param serialNumber - * @param modemType - * @throws DataDictionaryException - */ - public TICPortDescriptor(String portId, String portName, String description, String productName, String manufacturer, String serialNumber, TICModemType modemType) - throws DataDictionaryException - { - this(); - - this.setPortId(portId); - this.setPortName(portName); - this.setDescription(description); - this.setProductName(productName); - this.setManufacturer(manufacturer); - this.setSerialNumber(serialNumber); - this.setModemType(modemType); - - this.checkAndUpdate(); - } - - /** - * Constructor setting parameters to specific values - * - * @param serialPortDescriptor - * @param modemType - * @throws DataDictionaryException - */ - public TICPortDescriptor(SerialPortDescriptor serialPortDescriptor, TICModemType modemType) throws DataDictionaryException - { - this(); - - this.checkProductId(serialPortDescriptor.getProductId(), modemType); - this.checkVendorId(serialPortDescriptor.getVendorId(), modemType); - - this.setPortId(serialPortDescriptor.getPortId()); - this.setPortName(serialPortDescriptor.getPortName()); - this.setDescription(serialPortDescriptor.getDescription()); - this.setProductId(serialPortDescriptor.getProductId()); - this.setVendorId(serialPortDescriptor.getVendorId()); - this.setProductName(serialPortDescriptor.getProductName()); - this.setManufacturer(serialPortDescriptor.getManufacturer()); - this.setSerialNumber(serialPortDescriptor.getSerialNumber()); - this.setModemType(modemType); - - this.checkAndUpdate(); - } - - /** - * Constructor setting parameters to specific values - * - * @param usbPortDescriptor - * @param modemType - * @throws DataDictionaryException - */ - public TICPortDescriptor(USBPortDescriptor usbPortDescriptor, TICModemType modemType) throws DataDictionaryException - { - this(); - - this.checkProductId(usbPortDescriptor.getIdProduct(), modemType); - this.checkVendorId(usbPortDescriptor.getIdVendor(), modemType); - - this.setProductId(usbPortDescriptor.getIdProduct()); - this.setVendorId(usbPortDescriptor.getIdVendor()); - this.setProductName(usbPortDescriptor.getProduct()); - this.setManufacturer(usbPortDescriptor.getManufacturer()); - this.setSerialNumber(usbPortDescriptor.getSerialNumber()); - this.setModemType(modemType); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (this.exists(KEY_MODEM_TYPE)) - { - if (this.getModemType() != null) - { - this.setProductId(this.getModemType().getProductId()); - this.setVendorId(this.getModemType().getVendorId()); - } - else - { - this.setProductId(null); - this.setVendorId(null); - } - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get modem type - * - * @return the modem type - */ - public TICModemType getModemType() - { - return (TICModemType) this.data.get(KEY_MODEM_TYPE); - } - - /** - * Set modem type - * - * @param modemType - * @throws DataDictionaryException - */ - public void setModemType(TICModemType modemType) throws DataDictionaryException - { - this.setModemType((Object) modemType); - if (modemType != null) - { - this.setProductId(modemType.getProductId()); - this.setVendorId(modemType.getVendorId()); - } - else - { - this.setProductId(null); - this.setVendorId(null); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setModemType(Object modemType) throws DataDictionaryException - { - if (modemType == null) - { - this.data.put(KEY_MODEM_TYPE, null); - } - else - { - this.data.put(KEY_MODEM_TYPE, this.kModemType.convert(modemType)); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void checkProductId(Number productId, TICModemType modemType) throws DataDictionaryException - { - if (modemType != null && productId != null && productId.intValue() != modemType.getProductId()) - { - throw new DataDictionaryException("TIC modem productId is inconsistent with the given one"); - } - } - - private void checkVendorId(Number vendorId, TICModemType modemType) throws DataDictionaryException - { - if (modemType != null && vendorId != null && vendorId.intValue() != modemType.getVendorId()) - { - throw new DataDictionaryException("TIC modem vendorId is inconsistent with the given one"); - } - } - - private void loadKeyDescriptors() - { - try - { - this.kModemType = new KeyDescriptorEnum(KEY_MODEM_TYPE, false, TICModemType.class); - this.keys.add(this.kModemType); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +import enedis.lab.io.serialport.SerialPortDescriptor; +import enedis.lab.io.usb.USBPortDescriptor; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * TICPortDescriptor class + * + *

    Generated + */ +public class TICPortDescriptor extends SerialPortDescriptor { + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_MODEM_TYPE = "modemType"; + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kModemType; + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected TICPortDescriptor() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICPortDescriptor(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICPortDescriptor(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param portId + * @param portName + * @param description + * @param productName + * @param manufacturer + * @param serialNumber + * @param modemType + * @throws DataDictionaryException + */ + public TICPortDescriptor( + String portId, + String portName, + String description, + String productName, + String manufacturer, + String serialNumber, + TICModemType modemType) + throws DataDictionaryException { + this(); + + this.setPortId(portId); + this.setPortName(portName); + this.setDescription(description); + this.setProductName(productName); + this.setManufacturer(manufacturer); + this.setSerialNumber(serialNumber); + this.setModemType(modemType); + + this.checkAndUpdate(); + } + + /** + * Constructor setting parameters to specific values + * + * @param serialPortDescriptor + * @param modemType + * @throws DataDictionaryException + */ + public TICPortDescriptor(SerialPortDescriptor serialPortDescriptor, TICModemType modemType) + throws DataDictionaryException { + this(); + + this.checkProductId(serialPortDescriptor.getProductId(), modemType); + this.checkVendorId(serialPortDescriptor.getVendorId(), modemType); + + this.setPortId(serialPortDescriptor.getPortId()); + this.setPortName(serialPortDescriptor.getPortName()); + this.setDescription(serialPortDescriptor.getDescription()); + this.setProductId(serialPortDescriptor.getProductId()); + this.setVendorId(serialPortDescriptor.getVendorId()); + this.setProductName(serialPortDescriptor.getProductName()); + this.setManufacturer(serialPortDescriptor.getManufacturer()); + this.setSerialNumber(serialPortDescriptor.getSerialNumber()); + this.setModemType(modemType); + + this.checkAndUpdate(); + } + + /** + * Constructor setting parameters to specific values + * + * @param usbPortDescriptor + * @param modemType + * @throws DataDictionaryException + */ + public TICPortDescriptor(USBPortDescriptor usbPortDescriptor, TICModemType modemType) + throws DataDictionaryException { + this(); + + this.checkProductId(usbPortDescriptor.getIdProduct(), modemType); + this.checkVendorId(usbPortDescriptor.getIdVendor(), modemType); + + this.setProductId(usbPortDescriptor.getIdProduct()); + this.setVendorId(usbPortDescriptor.getIdVendor()); + this.setProductName(usbPortDescriptor.getProduct()); + this.setManufacturer(usbPortDescriptor.getManufacturer()); + this.setSerialNumber(usbPortDescriptor.getSerialNumber()); + this.setModemType(modemType); + + this.checkAndUpdate(); + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// SerialPortDescriptor + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (this.exists(KEY_MODEM_TYPE)) { + if (this.getModemType() != null) { + this.setProductId(this.getModemType().getProductId()); + this.setVendorId(this.getModemType().getVendorId()); + } else { + this.setProductId(null); + this.setVendorId(null); + } + } + super.updateOptionalParameters(); + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get modem type + * + * @return the modem type + */ + public TICModemType getModemType() { + return (TICModemType) this.data.get(KEY_MODEM_TYPE); + } + + /** + * Set modem type + * + * @param modemType + * @throws DataDictionaryException + */ + public void setModemType(TICModemType modemType) throws DataDictionaryException { + this.setModemType((Object) modemType); + if (modemType != null) { + this.setProductId(modemType.getProductId()); + this.setVendorId(modemType.getVendorId()); + } else { + this.setProductId(null); + this.setVendorId(null); + } + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setModemType(Object modemType) throws DataDictionaryException { + if (modemType == null) { + this.data.put(KEY_MODEM_TYPE, null); + } else { + this.data.put(KEY_MODEM_TYPE, this.kModemType.convert(modemType)); + } + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void checkProductId(Number productId, TICModemType modemType) + throws DataDictionaryException { + if (modemType != null + && productId != null + && productId.intValue() != modemType.getProductId()) { + throw new DataDictionaryException("TIC modem productId is inconsistent with the given one"); + } + } + + private void checkVendorId(Number vendorId, TICModemType modemType) + throws DataDictionaryException { + if (modemType != null && vendorId != null && vendorId.intValue() != modemType.getVendorId()) { + throw new DataDictionaryException("TIC modem vendorId is inconsistent with the given one"); + } + } + + private void loadKeyDescriptors() { + try { + this.kModemType = + new KeyDescriptorEnum(KEY_MODEM_TYPE, false, TICModemType.class); + this.keys.add(this.kModemType); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/tic/TICPortFinder.java b/src/main/java/enedis/lab/io/tic/TICPortFinder.java index 370121b..aefb888 100644 --- a/src/main/java/enedis/lab/io/tic/TICPortFinder.java +++ b/src/main/java/enedis/lab/io/tic/TICPortFinder.java @@ -1,74 +1,65 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import enedis.lab.io.PortFinder; - -/** - * Interface used to find all TIC port descriptor - */ -public interface TICPortFinder extends PortFinder -{ - /** - * Find TIC port descriptor matching with port id - * - * @param portId - * the unique port identifier desired - * - * @return TIC port descriptor found, or null if nothing matches with portId - */ - public default TICPortDescriptor findByPortId(String portId) - { - return this.findAll().stream().filter(p -> (p.getPortId() != null) ? p.getPortId().equals(portId) : portId == null).findFirst().orElse(null); - } - - /** - * Find TIC port descriptor matching with port name - * - * @param portName - * the port name desired - * - * @return TIC port descriptor found, or null if nothing matches with portName - */ - public default TICPortDescriptor findByPortName(String portName) - { - return this.findAll().stream().filter(p -> (p.getPortName() != null) ? p.getPortName().equals(portName) : portName == null).findFirst().orElse(null); - } - - /** - * Find native TIC port (not USB) descriptor matching with port name - * - * @param portName - * the port name desired - * - * @return TIC port descriptor found, or null if nothing matches with portName - */ - public TICPortDescriptor findNative(String portName); - - /** - * Find TIC port descriptor matching with port id or port name - * - * @param portId - * the unique port identifier desired - * @param portName - * the port name desired - * - * @return TIC port descriptor found, or null if nothing matches with portName - */ - public default TICPortDescriptor findByPortIdOrPortName(String portId, String portName) - { - TICPortDescriptor descriptor = this.findByPortId(portId); - - if (descriptor == null) - { - descriptor = this.findByPortName(portId); - } - - return descriptor; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +import enedis.lab.io.PortFinder; + +/** Interface used to find all TIC port descriptor */ +public interface TICPortFinder extends PortFinder { + /** + * Find TIC port descriptor matching with port id + * + * @param portId the unique port identifier desired + * @return TIC port descriptor found, or null if nothing matches with portId + */ + public default TICPortDescriptor findByPortId(String portId) { + return this.findAll().stream() + .filter(p -> (p.getPortId() != null) ? p.getPortId().equals(portId) : portId == null) + .findFirst() + .orElse(null); + } + + /** + * Find TIC port descriptor matching with port name + * + * @param portName the port name desired + * @return TIC port descriptor found, or null if nothing matches with portName + */ + public default TICPortDescriptor findByPortName(String portName) { + return this.findAll().stream() + .filter( + p -> (p.getPortName() != null) ? p.getPortName().equals(portName) : portName == null) + .findFirst() + .orElse(null); + } + + /** + * Find native TIC port (not USB) descriptor matching with port name + * + * @param portName the port name desired + * @return TIC port descriptor found, or null if nothing matches with portName + */ + public TICPortDescriptor findNative(String portName); + + /** + * Find TIC port descriptor matching with port id or port name + * + * @param portId the unique port identifier desired + * @param portName the port name desired + * @return TIC port descriptor found, or null if nothing matches with portName + */ + public default TICPortDescriptor findByPortIdOrPortName(String portId, String portName) { + TICPortDescriptor descriptor = this.findByPortId(portId); + + if (descriptor == null) { + descriptor = this.findByPortName(portId); + } + + return descriptor; + } +} diff --git a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java b/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java index d4777c1..463b845 100644 --- a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java +++ b/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java @@ -1,206 +1,196 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import java.util.List; - -import enedis.lab.io.serialport.SerialPortDescriptor; -import enedis.lab.io.serialport.SerialPortFinder; -import enedis.lab.io.serialport.SerialPortFinderBase; -import enedis.lab.io.usb.USBPortDescriptor; -import enedis.lab.io.usb.USBPortFinder; -import enedis.lab.io.usb.USBPortFinderBase; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * Class used to find all TIC port descriptor - */ -public class TICPortFinderBase implements TICPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program writing the TIC port descriptor list (JSON format) on the output stream - * - * @param args - * not used - */ - public static void main(String[] args) - { - DataList descriptors = getInstance().findAll(); - - System.out.println(descriptors.toString(2)); - } - - /** - * Get instance - * - * @return Unique instance - */ - public static TICPortFinderBase getInstance() - { - if (instance == null) - { - instance = new TICPortFinderBase(); - } - - return instance; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static TICPortFinderBase instance; - - private SerialPortFinder serialPortFinder; - private USBPortFinder usbPortFinder; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICPortFinderBase() - { - this(SerialPortFinderBase.getInstance(), USBPortFinderBase.getInstance()); - } - - /** - * Constructor with finder parameters - * - * @param serialPortFinder - * the serial port finder interface - * @param usbPortFinder - * the USB port finder interface - */ - public TICPortFinderBase(SerialPortFinder serialPortFinder, USBPortFinder usbPortFinder) - { - if (serialPortFinder == null) - { - throw new IllegalArgumentException("Cannot set null serial port finder"); - } - this.serialPortFinder = serialPortFinder; - if (usbPortFinder == null) - { - throw new IllegalArgumentException("Cannot set null USB port finder"); - } - this.usbPortFinder = usbPortFinder; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICPortFinder - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataList findAll() - { - DataList ticSerialPort = new DataArrayList(); - - for (TICModemType modemType : TICModemType.values()) - { - List tmpSerialPort = this.serialPortFinder.findByProductIdAndVendorId(modemType.getProductId(), modemType.getVendorId()); - - if (tmpSerialPort.isEmpty()) - { - List tmpUSBPort = this.usbPortFinder.findByProductIdAndVendorId(modemType.getProductId(), modemType.getVendorId()); - - for (USBPortDescriptor upd : tmpUSBPort) - { - try - { - TICPortDescriptor tic = new TICPortDescriptor(upd, modemType); - ticSerialPort.add(tic); - } - catch (DataDictionaryException e) - { - } - } - } - else - { - for (SerialPortDescriptor spd : tmpSerialPort) - { - try - { - TICPortDescriptor tic = new TICPortDescriptor(spd, modemType); - ticSerialPort.add(tic); - } - catch (DataDictionaryException e) - { - } - } - } - } - - return ticSerialPort; - } - - @Override - public TICPortDescriptor findNative(String portName) - { - TICPortDescriptor ticPortDescriptor = null; - SerialPortDescriptor serialPortDescriptor = this.serialPortFinder.findNative(portName); - - if (serialPortDescriptor != null) - { - try - { - ticPortDescriptor = new TICPortDescriptor(serialPortDescriptor, null); - } - catch (DataDictionaryException e) - { - } - } - - return ticPortDescriptor; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +import enedis.lab.io.serialport.SerialPortDescriptor; +import enedis.lab.io.serialport.SerialPortFinder; +import enedis.lab.io.serialport.SerialPortFinderBase; +import enedis.lab.io.usb.USBPortDescriptor; +import enedis.lab.io.usb.USBPortFinder; +import enedis.lab.io.usb.USBPortFinderBase; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; +import java.util.List; + +/** Class used to find all TIC port descriptor */ +public class TICPortFinderBase implements TICPortFinder { + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Program writing the TIC port descriptor list (JSON format) on the output stream + * + * @param args not used + */ + public static void main(String[] args) { + DataList descriptors = getInstance().findAll(); + + System.out.println(descriptors.toString(2)); + } + + /** + * Get instance + * + * @return Unique instance + */ + public static TICPortFinderBase getInstance() { + if (instance == null) { + instance = new TICPortFinderBase(); + } + + return instance; + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static TICPortFinderBase instance; + + private SerialPortFinder serialPortFinder; + private USBPortFinder usbPortFinder; + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private TICPortFinderBase() { + this(SerialPortFinderBase.getInstance(), USBPortFinderBase.getInstance()); + } + + /** + * Constructor with finder parameters + * + * @param serialPortFinder the serial port finder interface + * @param usbPortFinder the USB port finder interface + */ + public TICPortFinderBase(SerialPortFinder serialPortFinder, USBPortFinder usbPortFinder) { + if (serialPortFinder == null) { + throw new IllegalArgumentException("Cannot set null serial port finder"); + } + this.serialPortFinder = serialPortFinder; + if (usbPortFinder == null) { + throw new IllegalArgumentException("Cannot set null USB port finder"); + } + this.usbPortFinder = usbPortFinder; + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// TICPortFinder + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public DataList findAll() { + DataList ticSerialPort = new DataArrayList(); + + for (TICModemType modemType : TICModemType.values()) { + List tmpSerialPort = + this.serialPortFinder.findByProductIdAndVendorId( + modemType.getProductId(), modemType.getVendorId()); + + if (tmpSerialPort.isEmpty()) { + List tmpUSBPort = + this.usbPortFinder.findByProductIdAndVendorId( + modemType.getProductId(), modemType.getVendorId()); + + for (USBPortDescriptor upd : tmpUSBPort) { + try { + TICPortDescriptor tic = new TICPortDescriptor(upd, modemType); + ticSerialPort.add(tic); + } catch (DataDictionaryException e) { + } + } + } else { + for (SerialPortDescriptor spd : tmpSerialPort) { + try { + TICPortDescriptor tic = new TICPortDescriptor(spd, modemType); + ticSerialPort.add(tic); + } catch (DataDictionaryException e) { + } + } + } + } + + return ticSerialPort; + } + + @Override + public TICPortDescriptor findNative(String portName) { + TICPortDescriptor ticPortDescriptor = null; + SerialPortDescriptor serialPortDescriptor = this.serialPortFinder.findNative(portName); + + if (serialPortDescriptor != null) { + try { + ticPortDescriptor = new TICPortDescriptor(serialPortDescriptor, null); + } catch (DataDictionaryException e) { + } + } + + return ticPortDescriptor; + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java b/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java index d16edc8..f5ffc49 100644 --- a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java +++ b/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java @@ -1,131 +1,140 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import org.json.JSONObject; - -import enedis.lab.io.PlugSubscriber; -import enedis.lab.io.PortPlugNotifier; - -/** - * Class used to notify when a TIC port has been plugged or unplugged - */ -public class TICPortPlugNotifier extends PortPlugNotifier -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final int DEFAULT_JSON_INDENTATION = 2; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program writing on the output stream when an TIC port has been plugged or unplugged - * - * @param args - * not used - */ - public static void main(String[] args) - { - /* 1. Create notification service */ - TICPortPlugNotifier notifier = new TICPortPlugNotifier(); - /* 2. Create subscriber to print when an TIC port has been plugged or unplugged */ - PlugSubscriber subscriber = new PlugSubscriber() - { - @Override - public void onPlugged(TICPortDescriptor descriptor) - { - JSONObject jsonObject = new JSONObject(); - jsonObject.put(notifier.getClass().getSimpleName() + ".onPlugged", descriptor.toJSON()); - System.out.println(jsonObject.toString(DEFAULT_JSON_INDENTATION) + "\n"); - - } - @Override - public void onUnplugged(TICPortDescriptor descriptor) - { - JSONObject jsonObject = new JSONObject(); - jsonObject.put(notifier.getClass().getSimpleName() + ".onUnplugged", descriptor.toJSON()); - System.out.println(jsonObject.toString(DEFAULT_JSON_INDENTATION) + "\n"); - } - }; - /* 3. Run program printing when an TIC port has been plugged or unplugged until CTRL+C is pressed */ - PortPlugNotifier.main(notifier, subscriber); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor with default parameter - * - * @see #DEFAULT_PERIOD - * @see TICPortFinderBase - */ - public TICPortPlugNotifier() - { - this(DEFAULT_PERIOD,TICPortFinderBase.getInstance()); - } - - /** - * Constructor with all parameters - * - * @param period the period (in milliseconds) used to look for plugged or unplugged TIC port - * @param finder the TIC port finder interface used to find all TIC port descriptors - */ - public TICPortPlugNotifier(long period,TICPortFinder finder) - { - super(period,finder); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Runnable - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +import enedis.lab.io.PlugSubscriber; +import enedis.lab.io.PortPlugNotifier; +import org.json.JSONObject; + +/** Class used to notify when a TIC port has been plugged or unplugged */ +public class TICPortPlugNotifier extends PortPlugNotifier { + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static final int DEFAULT_JSON_INDENTATION = 2; + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Program writing on the output stream when an TIC port has been plugged or unplugged + * + * @param args not used + */ + public static void main(String[] args) { + /* 1. Create notification service */ + TICPortPlugNotifier notifier = new TICPortPlugNotifier(); + /* 2. Create subscriber to print when an TIC port has been plugged or unplugged */ + PlugSubscriber subscriber = + new PlugSubscriber() { + @Override + public void onPlugged(TICPortDescriptor descriptor) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(notifier.getClass().getSimpleName() + ".onPlugged", descriptor.toJSON()); + System.out.println(jsonObject.toString(DEFAULT_JSON_INDENTATION) + "\n"); + } + + @Override + public void onUnplugged(TICPortDescriptor descriptor) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put( + notifier.getClass().getSimpleName() + ".onUnplugged", descriptor.toJSON()); + System.out.println(jsonObject.toString(DEFAULT_JSON_INDENTATION) + "\n"); + } + }; + /* 3. Run program printing when an TIC port has been plugged or unplugged until CTRL+C is pressed */ + PortPlugNotifier.main(notifier, subscriber); + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Constructor with default parameter + * + * @see #DEFAULT_PERIOD + * @see TICPortFinderBase + */ + public TICPortPlugNotifier() { + this(DEFAULT_PERIOD, TICPortFinderBase.getInstance()); + } + + /** + * Constructor with all parameters + * + * @param period the period (in milliseconds) used to look for plugged or unplugged TIC port + * @param finder the TIC port finder interface used to find all TIC port descriptors + */ + public TICPortPlugNotifier(long period, TICPortFinder finder) { + super(period, finder); + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// Runnable + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java b/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java index b3f71fb..eac94dc 100644 --- a/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java +++ b/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java @@ -1,711 +1,682 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.usb; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * USBPortDescriptor class - * - * Generated - */ -public class USBPortDescriptor extends DataDictionaryBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_BCD_DEVICE = "bcdDevice"; - protected static final String KEY_BCD_USB = "bcdUSB"; - protected static final String KEY_B_DESCRIPTOR_TYPE = "bDescriptorType"; - protected static final String KEY_B_DEVICE_CLASS = "bDeviceClass"; - protected static final String KEY_B_DEVICE_PROTOCOL = "bDeviceProtocol"; - protected static final String KEY_B_DEVICE_SUB_CLASS = "bDeviceSubClass"; - protected static final String KEY_B_LENGTH = "bLength"; - protected static final String KEY_B_MAX_PACKET_SIZE0 = "bMaxPacketSize0"; - protected static final String KEY_B_NUM_CONFIGURATIONS = "bNumConfigurations"; - protected static final String KEY_ID_PRODUCT = "idProduct"; - protected static final String KEY_ID_VENDOR = "idVendor"; - protected static final String KEY_I_MANUFACTURER = "iManufacturer"; - protected static final String KEY_I_PRODUCT = "iProduct"; - protected static final String KEY_I_SERIAL_NUMBER = "iSerialNumber"; - protected static final String KEY_MANUFACTURER = "manufacturer"; - protected static final String KEY_PRODUCT = "product"; - protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorNumber kBcdDevice; - protected KeyDescriptorNumber kBcdUSB; - protected KeyDescriptorNumber kBDescriptorType; - protected KeyDescriptorNumber kBDeviceClass; - protected KeyDescriptorNumber kBDeviceProtocol; - protected KeyDescriptorNumber kBDeviceSubClass; - protected KeyDescriptorNumber kBLength; - protected KeyDescriptorNumber kBMaxPacketSize0; - protected KeyDescriptorNumber kBNumConfigurations; - protected KeyDescriptorNumber kIdProduct; - protected KeyDescriptorNumber kIdVendor; - protected KeyDescriptorNumber kIManufacturer; - protected KeyDescriptorNumber kIProduct; - protected KeyDescriptorNumber kISerialNumber; - protected KeyDescriptorString kManufacturer; - protected KeyDescriptorString kProduct; - protected KeyDescriptorString kSerialNumber; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected USBPortDescriptor() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public USBPortDescriptor(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public USBPortDescriptor(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param bcdDevice - * @param bcdUSB - * @param bDescriptorType - * @param bDeviceClass - * @param bDeviceProtocol - * @param bDeviceSubClass - * @param bLength - * @param bMaxPacketSize0 - * @param bNumConfigurations - * @param idProduct - * @param idVendor - * @param iManufacturer - * @param iProduct - * @param iSerialNumber - * @param manufacturer - * @param product - * @param serialNumber - * @throws DataDictionaryException - */ - public USBPortDescriptor(Number bcdDevice, Number bcdUSB, Number bDescriptorType, Number bDeviceClass, Number bDeviceProtocol, Number bDeviceSubClass, Number bLength, - Number bMaxPacketSize0, Number bNumConfigurations, Number idProduct, Number idVendor, Number iManufacturer, Number iProduct, Number iSerialNumber, String manufacturer, - String product, String serialNumber) throws DataDictionaryException - { - this(); - - this.setBcdDevice(bcdDevice); - this.setBcdUSB(bcdUSB); - this.setBDescriptorType(bDescriptorType); - this.setBDeviceClass(bDeviceClass); - this.setBDeviceProtocol(bDeviceProtocol); - this.setBDeviceSubClass(bDeviceSubClass); - this.setBLength(bLength); - this.setBMaxPacketSize0(bMaxPacketSize0); - this.setBNumConfigurations(bNumConfigurations); - this.setIdProduct(idProduct); - this.setIdVendor(idVendor); - this.setIManufacturer(iManufacturer); - this.setIProduct(iProduct); - this.setISerialNumber(iSerialNumber); - this.setManufacturer(manufacturer); - this.setProduct(product); - this.setSerialNumber(serialNumber); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get bcd device - * - * @return the bcd device - */ - public Number getBcdDevice() - { - return (Number) this.data.get(KEY_BCD_DEVICE); - } - - /** - * Get bcd u s b - * - * @return the bcd u s b - */ - public Number getBcdUSB() - { - return (Number) this.data.get(KEY_BCD_USB); - } - - /** - * Get b descriptor type - * - * @return the b descriptor type - */ - public Number getBDescriptorType() - { - return (Number) this.data.get(KEY_B_DESCRIPTOR_TYPE); - } - - /** - * Get b device class - * - * @return the b device class - */ - public Number getBDeviceClass() - { - return (Number) this.data.get(KEY_B_DEVICE_CLASS); - } - - /** - * Get b device protocol - * - * @return the b device protocol - */ - public Number getBDeviceProtocol() - { - return (Number) this.data.get(KEY_B_DEVICE_PROTOCOL); - } - - /** - * Get b device sub class - * - * @return the b device sub class - */ - public Number getBDeviceSubClass() - { - return (Number) this.data.get(KEY_B_DEVICE_SUB_CLASS); - } - - /** - * Get b length - * - * @return the b length - */ - public Number getBLength() - { - return (Number) this.data.get(KEY_B_LENGTH); - } - - /** - * Get b max packet size0 - * - * @return the b max packet size0 - */ - public Number getBMaxPacketSize0() - { - return (Number) this.data.get(KEY_B_MAX_PACKET_SIZE0); - } - - /** - * Get b num configurations - * - * @return the b num configurations - */ - public Number getBNumConfigurations() - { - return (Number) this.data.get(KEY_B_NUM_CONFIGURATIONS); - } - - /** - * Get id product - * - * @return the id product - */ - public Number getIdProduct() - { - return (Number) this.data.get(KEY_ID_PRODUCT); - } - - /** - * Get id vendor - * - * @return the id vendor - */ - public Number getIdVendor() - { - return (Number) this.data.get(KEY_ID_VENDOR); - } - - /** - * Get i manufacturer - * - * @return the i manufacturer - */ - public Number getIManufacturer() - { - return (Number) this.data.get(KEY_I_MANUFACTURER); - } - - /** - * Get i product - * - * @return the i product - */ - public Number getIProduct() - { - return (Number) this.data.get(KEY_I_PRODUCT); - } - - /** - * Get i serial number - * - * @return the i serial number - */ - public Number getISerialNumber() - { - return (Number) this.data.get(KEY_I_SERIAL_NUMBER); - } - - /** - * Get manufacturer - * - * @return the manufacturer - */ - public String getManufacturer() - { - return (String) this.data.get(KEY_MANUFACTURER); - } - - /** - * Get product - * - * @return the product - */ - public String getProduct() - { - return (String) this.data.get(KEY_PRODUCT); - } - - /** - * Get serial number - * - * @return the serial number - */ - public String getSerialNumber() - { - return (String) this.data.get(KEY_SERIAL_NUMBER); - } - - /** - * Set bcd device - * - * @param bcdDevice - * @throws DataDictionaryException - */ - public void setBcdDevice(Number bcdDevice) throws DataDictionaryException - { - this.setBcdDevice((Object) bcdDevice); - } - - /** - * Set bcd u s b - * - * @param bcdUSB - * @throws DataDictionaryException - */ - public void setBcdUSB(Number bcdUSB) throws DataDictionaryException - { - this.setBcdUSB((Object) bcdUSB); - } - - /** - * Set b descriptor type - * - * @param bDescriptorType - * @throws DataDictionaryException - */ - public void setBDescriptorType(Number bDescriptorType) throws DataDictionaryException - { - this.setBDescriptorType((Object) bDescriptorType); - } - - /** - * Set b device class - * - * @param bDeviceClass - * @throws DataDictionaryException - */ - public void setBDeviceClass(Number bDeviceClass) throws DataDictionaryException - { - this.setBDeviceClass((Object) bDeviceClass); - } - - /** - * Set b device protocol - * - * @param bDeviceProtocol - * @throws DataDictionaryException - */ - public void setBDeviceProtocol(Number bDeviceProtocol) throws DataDictionaryException - { - this.setBDeviceProtocol((Object) bDeviceProtocol); - } - - /** - * Set b device sub class - * - * @param bDeviceSubClass - * @throws DataDictionaryException - */ - public void setBDeviceSubClass(Number bDeviceSubClass) throws DataDictionaryException - { - this.setBDeviceSubClass((Object) bDeviceSubClass); - } - - /** - * Set b length - * - * @param bLength - * @throws DataDictionaryException - */ - public void setBLength(Number bLength) throws DataDictionaryException - { - this.setBLength((Object) bLength); - } - - /** - * Set b max packet size0 - * - * @param bMaxPacketSize0 - * @throws DataDictionaryException - */ - public void setBMaxPacketSize0(Number bMaxPacketSize0) throws DataDictionaryException - { - this.setBMaxPacketSize0((Object) bMaxPacketSize0); - } - - /** - * Set b num configurations - * - * @param bNumConfigurations - * @throws DataDictionaryException - */ - public void setBNumConfigurations(Number bNumConfigurations) throws DataDictionaryException - { - this.setBNumConfigurations((Object) bNumConfigurations); - } - - /** - * Set id product - * - * @param idProduct - * @throws DataDictionaryException - */ - public void setIdProduct(Number idProduct) throws DataDictionaryException - { - this.setIdProduct((Object) idProduct); - } - - /** - * Set id vendor - * - * @param idVendor - * @throws DataDictionaryException - */ - public void setIdVendor(Number idVendor) throws DataDictionaryException - { - this.setIdVendor((Object) idVendor); - } - - /** - * Set i manufacturer - * - * @param iManufacturer - * @throws DataDictionaryException - */ - public void setIManufacturer(Number iManufacturer) throws DataDictionaryException - { - this.setIManufacturer((Object) iManufacturer); - } - - /** - * Set i product - * - * @param iProduct - * @throws DataDictionaryException - */ - public void setIProduct(Number iProduct) throws DataDictionaryException - { - this.setIProduct((Object) iProduct); - } - - /** - * Set i serial number - * - * @param iSerialNumber - * @throws DataDictionaryException - */ - public void setISerialNumber(Number iSerialNumber) throws DataDictionaryException - { - this.setISerialNumber((Object) iSerialNumber); - } - - /** - * Set manufacturer - * - * @param manufacturer - * @throws DataDictionaryException - */ - public void setManufacturer(String manufacturer) throws DataDictionaryException - { - this.setManufacturer((Object) manufacturer); - } - - /** - * Set product - * - * @param product - * @throws DataDictionaryException - */ - public void setProduct(String product) throws DataDictionaryException - { - this.setProduct((Object) product); - } - - /** - * Set serial number - * - * @param serialNumber - * @throws DataDictionaryException - */ - public void setSerialNumber(String serialNumber) throws DataDictionaryException - { - this.setSerialNumber((Object) serialNumber); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setBcdDevice(Object bcdDevice) throws DataDictionaryException - { - this.data.put(KEY_BCD_DEVICE, this.kBcdDevice.convert(bcdDevice)); - } - - protected void setBcdUSB(Object bcdUSB) throws DataDictionaryException - { - this.data.put(KEY_BCD_USB, this.kBcdUSB.convert(bcdUSB)); - } - - protected void setBDescriptorType(Object bDescriptorType) throws DataDictionaryException - { - this.data.put(KEY_B_DESCRIPTOR_TYPE, this.kBDescriptorType.convert(bDescriptorType)); - } - - protected void setBDeviceClass(Object bDeviceClass) throws DataDictionaryException - { - this.data.put(KEY_B_DEVICE_CLASS, this.kBDeviceClass.convert(bDeviceClass)); - } - - protected void setBDeviceProtocol(Object bDeviceProtocol) throws DataDictionaryException - { - this.data.put(KEY_B_DEVICE_PROTOCOL, this.kBDeviceProtocol.convert(bDeviceProtocol)); - } - - protected void setBDeviceSubClass(Object bDeviceSubClass) throws DataDictionaryException - { - this.data.put(KEY_B_DEVICE_SUB_CLASS, this.kBDeviceSubClass.convert(bDeviceSubClass)); - } - - protected void setBLength(Object bLength) throws DataDictionaryException - { - this.data.put(KEY_B_LENGTH, this.kBLength.convert(bLength)); - } - - protected void setBMaxPacketSize0(Object bMaxPacketSize0) throws DataDictionaryException - { - this.data.put(KEY_B_MAX_PACKET_SIZE0, this.kBMaxPacketSize0.convert(bMaxPacketSize0)); - } - - protected void setBNumConfigurations(Object bNumConfigurations) throws DataDictionaryException - { - this.data.put(KEY_B_NUM_CONFIGURATIONS, this.kBNumConfigurations.convert(bNumConfigurations)); - } - - protected void setIdProduct(Object idProduct) throws DataDictionaryException - { - this.data.put(KEY_ID_PRODUCT, this.kIdProduct.convert(idProduct)); - } - - protected void setIdVendor(Object idVendor) throws DataDictionaryException - { - this.data.put(KEY_ID_VENDOR, this.kIdVendor.convert(idVendor)); - } - - protected void setIManufacturer(Object iManufacturer) throws DataDictionaryException - { - this.data.put(KEY_I_MANUFACTURER, this.kIManufacturer.convert(iManufacturer)); - } - - protected void setIProduct(Object iProduct) throws DataDictionaryException - { - this.data.put(KEY_I_PRODUCT, this.kIProduct.convert(iProduct)); - } - - protected void setISerialNumber(Object iSerialNumber) throws DataDictionaryException - { - this.data.put(KEY_I_SERIAL_NUMBER, this.kISerialNumber.convert(iSerialNumber)); - } - - protected void setManufacturer(Object manufacturer) throws DataDictionaryException - { - this.data.put(KEY_MANUFACTURER, this.kManufacturer.convert(manufacturer)); - } - - protected void setProduct(Object product) throws DataDictionaryException - { - this.data.put(KEY_PRODUCT, this.kProduct.convert(product)); - } - - protected void setSerialNumber(Object serialNumber) throws DataDictionaryException - { - this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kBcdDevice = new KeyDescriptorNumber(KEY_BCD_DEVICE, true); - this.keys.add(this.kBcdDevice); - - this.kBcdUSB = new KeyDescriptorNumber(KEY_BCD_USB, true); - this.keys.add(this.kBcdUSB); - - this.kBDescriptorType = new KeyDescriptorNumber(KEY_B_DESCRIPTOR_TYPE, true); - this.keys.add(this.kBDescriptorType); - - this.kBDeviceClass = new KeyDescriptorNumber(KEY_B_DEVICE_CLASS, true); - this.keys.add(this.kBDeviceClass); - - this.kBDeviceProtocol = new KeyDescriptorNumber(KEY_B_DEVICE_PROTOCOL, true); - this.keys.add(this.kBDeviceProtocol); - - this.kBDeviceSubClass = new KeyDescriptorNumber(KEY_B_DEVICE_SUB_CLASS, true); - this.keys.add(this.kBDeviceSubClass); - - this.kBLength = new KeyDescriptorNumber(KEY_B_LENGTH, true); - this.keys.add(this.kBLength); - - this.kBMaxPacketSize0 = new KeyDescriptorNumber(KEY_B_MAX_PACKET_SIZE0, true); - this.keys.add(this.kBMaxPacketSize0); - - this.kBNumConfigurations = new KeyDescriptorNumber(KEY_B_NUM_CONFIGURATIONS, true); - this.keys.add(this.kBNumConfigurations); - - this.kIdProduct = new KeyDescriptorNumber(KEY_ID_PRODUCT, true); - this.keys.add(this.kIdProduct); - - this.kIdVendor = new KeyDescriptorNumber(KEY_ID_VENDOR, true); - this.keys.add(this.kIdVendor); - - this.kIManufacturer = new KeyDescriptorNumber(KEY_I_MANUFACTURER, true); - this.keys.add(this.kIManufacturer); - - this.kIProduct = new KeyDescriptorNumber(KEY_I_PRODUCT, true); - this.keys.add(this.kIProduct); - - this.kISerialNumber = new KeyDescriptorNumber(KEY_I_SERIAL_NUMBER, true); - this.keys.add(this.kISerialNumber); - - this.kManufacturer = new KeyDescriptorString(KEY_MANUFACTURER, false, true); - this.keys.add(this.kManufacturer); - - this.kProduct = new KeyDescriptorString(KEY_PRODUCT, false, true); - this.keys.add(this.kProduct); - - this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, true); - this.keys.add(this.kSerialNumber); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.usb; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorNumber; +import enedis.lab.types.datadictionary.KeyDescriptorString; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * USBPortDescriptor class + * + *

    Generated + */ +public class USBPortDescriptor extends DataDictionaryBase { + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected static final String KEY_BCD_DEVICE = "bcdDevice"; + protected static final String KEY_BCD_USB = "bcdUSB"; + protected static final String KEY_B_DESCRIPTOR_TYPE = "bDescriptorType"; + protected static final String KEY_B_DEVICE_CLASS = "bDeviceClass"; + protected static final String KEY_B_DEVICE_PROTOCOL = "bDeviceProtocol"; + protected static final String KEY_B_DEVICE_SUB_CLASS = "bDeviceSubClass"; + protected static final String KEY_B_LENGTH = "bLength"; + protected static final String KEY_B_MAX_PACKET_SIZE0 = "bMaxPacketSize0"; + protected static final String KEY_B_NUM_CONFIGURATIONS = "bNumConfigurations"; + protected static final String KEY_ID_PRODUCT = "idProduct"; + protected static final String KEY_ID_VENDOR = "idVendor"; + protected static final String KEY_I_MANUFACTURER = "iManufacturer"; + protected static final String KEY_I_PRODUCT = "iProduct"; + protected static final String KEY_I_SERIAL_NUMBER = "iSerialNumber"; + protected static final String KEY_MANUFACTURER = "manufacturer"; + protected static final String KEY_PRODUCT = "product"; + protected static final String KEY_SERIAL_NUMBER = "serialNumber"; + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private List> keys = new ArrayList>(); + + protected KeyDescriptorNumber kBcdDevice; + protected KeyDescriptorNumber kBcdUSB; + protected KeyDescriptorNumber kBDescriptorType; + protected KeyDescriptorNumber kBDeviceClass; + protected KeyDescriptorNumber kBDeviceProtocol; + protected KeyDescriptorNumber kBDeviceSubClass; + protected KeyDescriptorNumber kBLength; + protected KeyDescriptorNumber kBMaxPacketSize0; + protected KeyDescriptorNumber kBNumConfigurations; + protected KeyDescriptorNumber kIdProduct; + protected KeyDescriptorNumber kIdVendor; + protected KeyDescriptorNumber kIManufacturer; + protected KeyDescriptorNumber kIProduct; + protected KeyDescriptorNumber kISerialNumber; + protected KeyDescriptorString kManufacturer; + protected KeyDescriptorString kProduct; + protected KeyDescriptorString kSerialNumber; + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected USBPortDescriptor() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public USBPortDescriptor(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public USBPortDescriptor(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param bcdDevice + * @param bcdUSB + * @param bDescriptorType + * @param bDeviceClass + * @param bDeviceProtocol + * @param bDeviceSubClass + * @param bLength + * @param bMaxPacketSize0 + * @param bNumConfigurations + * @param idProduct + * @param idVendor + * @param iManufacturer + * @param iProduct + * @param iSerialNumber + * @param manufacturer + * @param product + * @param serialNumber + * @throws DataDictionaryException + */ + public USBPortDescriptor( + Number bcdDevice, + Number bcdUSB, + Number bDescriptorType, + Number bDeviceClass, + Number bDeviceProtocol, + Number bDeviceSubClass, + Number bLength, + Number bMaxPacketSize0, + Number bNumConfigurations, + Number idProduct, + Number idVendor, + Number iManufacturer, + Number iProduct, + Number iSerialNumber, + String manufacturer, + String product, + String serialNumber) + throws DataDictionaryException { + this(); + + this.setBcdDevice(bcdDevice); + this.setBcdUSB(bcdUSB); + this.setBDescriptorType(bDescriptorType); + this.setBDeviceClass(bDeviceClass); + this.setBDeviceProtocol(bDeviceProtocol); + this.setBDeviceSubClass(bDeviceSubClass); + this.setBLength(bLength); + this.setBMaxPacketSize0(bMaxPacketSize0); + this.setBNumConfigurations(bNumConfigurations); + this.setIdProduct(idProduct); + this.setIdVendor(idVendor); + this.setIManufacturer(iManufacturer); + this.setIProduct(iProduct); + this.setISerialNumber(iSerialNumber); + this.setManufacturer(manufacturer); + this.setProduct(product); + this.setSerialNumber(serialNumber); + + this.checkAndUpdate(); + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// DataDictionaryBase + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + super.updateOptionalParameters(); + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get bcd device + * + * @return the bcd device + */ + public Number getBcdDevice() { + return (Number) this.data.get(KEY_BCD_DEVICE); + } + + /** + * Get bcd u s b + * + * @return the bcd u s b + */ + public Number getBcdUSB() { + return (Number) this.data.get(KEY_BCD_USB); + } + + /** + * Get b descriptor type + * + * @return the b descriptor type + */ + public Number getBDescriptorType() { + return (Number) this.data.get(KEY_B_DESCRIPTOR_TYPE); + } + + /** + * Get b device class + * + * @return the b device class + */ + public Number getBDeviceClass() { + return (Number) this.data.get(KEY_B_DEVICE_CLASS); + } + + /** + * Get b device protocol + * + * @return the b device protocol + */ + public Number getBDeviceProtocol() { + return (Number) this.data.get(KEY_B_DEVICE_PROTOCOL); + } + + /** + * Get b device sub class + * + * @return the b device sub class + */ + public Number getBDeviceSubClass() { + return (Number) this.data.get(KEY_B_DEVICE_SUB_CLASS); + } + + /** + * Get b length + * + * @return the b length + */ + public Number getBLength() { + return (Number) this.data.get(KEY_B_LENGTH); + } + + /** + * Get b max packet size0 + * + * @return the b max packet size0 + */ + public Number getBMaxPacketSize0() { + return (Number) this.data.get(KEY_B_MAX_PACKET_SIZE0); + } + + /** + * Get b num configurations + * + * @return the b num configurations + */ + public Number getBNumConfigurations() { + return (Number) this.data.get(KEY_B_NUM_CONFIGURATIONS); + } + + /** + * Get id product + * + * @return the id product + */ + public Number getIdProduct() { + return (Number) this.data.get(KEY_ID_PRODUCT); + } + + /** + * Get id vendor + * + * @return the id vendor + */ + public Number getIdVendor() { + return (Number) this.data.get(KEY_ID_VENDOR); + } + + /** + * Get i manufacturer + * + * @return the i manufacturer + */ + public Number getIManufacturer() { + return (Number) this.data.get(KEY_I_MANUFACTURER); + } + + /** + * Get i product + * + * @return the i product + */ + public Number getIProduct() { + return (Number) this.data.get(KEY_I_PRODUCT); + } + + /** + * Get i serial number + * + * @return the i serial number + */ + public Number getISerialNumber() { + return (Number) this.data.get(KEY_I_SERIAL_NUMBER); + } + + /** + * Get manufacturer + * + * @return the manufacturer + */ + public String getManufacturer() { + return (String) this.data.get(KEY_MANUFACTURER); + } + + /** + * Get product + * + * @return the product + */ + public String getProduct() { + return (String) this.data.get(KEY_PRODUCT); + } + + /** + * Get serial number + * + * @return the serial number + */ + public String getSerialNumber() { + return (String) this.data.get(KEY_SERIAL_NUMBER); + } + + /** + * Set bcd device + * + * @param bcdDevice + * @throws DataDictionaryException + */ + public void setBcdDevice(Number bcdDevice) throws DataDictionaryException { + this.setBcdDevice((Object) bcdDevice); + } + + /** + * Set bcd u s b + * + * @param bcdUSB + * @throws DataDictionaryException + */ + public void setBcdUSB(Number bcdUSB) throws DataDictionaryException { + this.setBcdUSB((Object) bcdUSB); + } + + /** + * Set b descriptor type + * + * @param bDescriptorType + * @throws DataDictionaryException + */ + public void setBDescriptorType(Number bDescriptorType) throws DataDictionaryException { + this.setBDescriptorType((Object) bDescriptorType); + } + + /** + * Set b device class + * + * @param bDeviceClass + * @throws DataDictionaryException + */ + public void setBDeviceClass(Number bDeviceClass) throws DataDictionaryException { + this.setBDeviceClass((Object) bDeviceClass); + } + + /** + * Set b device protocol + * + * @param bDeviceProtocol + * @throws DataDictionaryException + */ + public void setBDeviceProtocol(Number bDeviceProtocol) throws DataDictionaryException { + this.setBDeviceProtocol((Object) bDeviceProtocol); + } + + /** + * Set b device sub class + * + * @param bDeviceSubClass + * @throws DataDictionaryException + */ + public void setBDeviceSubClass(Number bDeviceSubClass) throws DataDictionaryException { + this.setBDeviceSubClass((Object) bDeviceSubClass); + } + + /** + * Set b length + * + * @param bLength + * @throws DataDictionaryException + */ + public void setBLength(Number bLength) throws DataDictionaryException { + this.setBLength((Object) bLength); + } + + /** + * Set b max packet size0 + * + * @param bMaxPacketSize0 + * @throws DataDictionaryException + */ + public void setBMaxPacketSize0(Number bMaxPacketSize0) throws DataDictionaryException { + this.setBMaxPacketSize0((Object) bMaxPacketSize0); + } + + /** + * Set b num configurations + * + * @param bNumConfigurations + * @throws DataDictionaryException + */ + public void setBNumConfigurations(Number bNumConfigurations) throws DataDictionaryException { + this.setBNumConfigurations((Object) bNumConfigurations); + } + + /** + * Set id product + * + * @param idProduct + * @throws DataDictionaryException + */ + public void setIdProduct(Number idProduct) throws DataDictionaryException { + this.setIdProduct((Object) idProduct); + } + + /** + * Set id vendor + * + * @param idVendor + * @throws DataDictionaryException + */ + public void setIdVendor(Number idVendor) throws DataDictionaryException { + this.setIdVendor((Object) idVendor); + } + + /** + * Set i manufacturer + * + * @param iManufacturer + * @throws DataDictionaryException + */ + public void setIManufacturer(Number iManufacturer) throws DataDictionaryException { + this.setIManufacturer((Object) iManufacturer); + } + + /** + * Set i product + * + * @param iProduct + * @throws DataDictionaryException + */ + public void setIProduct(Number iProduct) throws DataDictionaryException { + this.setIProduct((Object) iProduct); + } + + /** + * Set i serial number + * + * @param iSerialNumber + * @throws DataDictionaryException + */ + public void setISerialNumber(Number iSerialNumber) throws DataDictionaryException { + this.setISerialNumber((Object) iSerialNumber); + } + + /** + * Set manufacturer + * + * @param manufacturer + * @throws DataDictionaryException + */ + public void setManufacturer(String manufacturer) throws DataDictionaryException { + this.setManufacturer((Object) manufacturer); + } + + /** + * Set product + * + * @param product + * @throws DataDictionaryException + */ + public void setProduct(String product) throws DataDictionaryException { + this.setProduct((Object) product); + } + + /** + * Set serial number + * + * @param serialNumber + * @throws DataDictionaryException + */ + public void setSerialNumber(String serialNumber) throws DataDictionaryException { + this.setSerialNumber((Object) serialNumber); + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + protected void setBcdDevice(Object bcdDevice) throws DataDictionaryException { + this.data.put(KEY_BCD_DEVICE, this.kBcdDevice.convert(bcdDevice)); + } + + protected void setBcdUSB(Object bcdUSB) throws DataDictionaryException { + this.data.put(KEY_BCD_USB, this.kBcdUSB.convert(bcdUSB)); + } + + protected void setBDescriptorType(Object bDescriptorType) throws DataDictionaryException { + this.data.put(KEY_B_DESCRIPTOR_TYPE, this.kBDescriptorType.convert(bDescriptorType)); + } + + protected void setBDeviceClass(Object bDeviceClass) throws DataDictionaryException { + this.data.put(KEY_B_DEVICE_CLASS, this.kBDeviceClass.convert(bDeviceClass)); + } + + protected void setBDeviceProtocol(Object bDeviceProtocol) throws DataDictionaryException { + this.data.put(KEY_B_DEVICE_PROTOCOL, this.kBDeviceProtocol.convert(bDeviceProtocol)); + } + + protected void setBDeviceSubClass(Object bDeviceSubClass) throws DataDictionaryException { + this.data.put(KEY_B_DEVICE_SUB_CLASS, this.kBDeviceSubClass.convert(bDeviceSubClass)); + } + + protected void setBLength(Object bLength) throws DataDictionaryException { + this.data.put(KEY_B_LENGTH, this.kBLength.convert(bLength)); + } + + protected void setBMaxPacketSize0(Object bMaxPacketSize0) throws DataDictionaryException { + this.data.put(KEY_B_MAX_PACKET_SIZE0, this.kBMaxPacketSize0.convert(bMaxPacketSize0)); + } + + protected void setBNumConfigurations(Object bNumConfigurations) throws DataDictionaryException { + this.data.put(KEY_B_NUM_CONFIGURATIONS, this.kBNumConfigurations.convert(bNumConfigurations)); + } + + protected void setIdProduct(Object idProduct) throws DataDictionaryException { + this.data.put(KEY_ID_PRODUCT, this.kIdProduct.convert(idProduct)); + } + + protected void setIdVendor(Object idVendor) throws DataDictionaryException { + this.data.put(KEY_ID_VENDOR, this.kIdVendor.convert(idVendor)); + } + + protected void setIManufacturer(Object iManufacturer) throws DataDictionaryException { + this.data.put(KEY_I_MANUFACTURER, this.kIManufacturer.convert(iManufacturer)); + } + + protected void setIProduct(Object iProduct) throws DataDictionaryException { + this.data.put(KEY_I_PRODUCT, this.kIProduct.convert(iProduct)); + } + + protected void setISerialNumber(Object iSerialNumber) throws DataDictionaryException { + this.data.put(KEY_I_SERIAL_NUMBER, this.kISerialNumber.convert(iSerialNumber)); + } + + protected void setManufacturer(Object manufacturer) throws DataDictionaryException { + this.data.put(KEY_MANUFACTURER, this.kManufacturer.convert(manufacturer)); + } + + protected void setProduct(Object product) throws DataDictionaryException { + this.data.put(KEY_PRODUCT, this.kProduct.convert(product)); + } + + protected void setSerialNumber(Object serialNumber) throws DataDictionaryException { + this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadKeyDescriptors() { + try { + this.kBcdDevice = new KeyDescriptorNumber(KEY_BCD_DEVICE, true); + this.keys.add(this.kBcdDevice); + + this.kBcdUSB = new KeyDescriptorNumber(KEY_BCD_USB, true); + this.keys.add(this.kBcdUSB); + + this.kBDescriptorType = new KeyDescriptorNumber(KEY_B_DESCRIPTOR_TYPE, true); + this.keys.add(this.kBDescriptorType); + + this.kBDeviceClass = new KeyDescriptorNumber(KEY_B_DEVICE_CLASS, true); + this.keys.add(this.kBDeviceClass); + + this.kBDeviceProtocol = new KeyDescriptorNumber(KEY_B_DEVICE_PROTOCOL, true); + this.keys.add(this.kBDeviceProtocol); + + this.kBDeviceSubClass = new KeyDescriptorNumber(KEY_B_DEVICE_SUB_CLASS, true); + this.keys.add(this.kBDeviceSubClass); + + this.kBLength = new KeyDescriptorNumber(KEY_B_LENGTH, true); + this.keys.add(this.kBLength); + + this.kBMaxPacketSize0 = new KeyDescriptorNumber(KEY_B_MAX_PACKET_SIZE0, true); + this.keys.add(this.kBMaxPacketSize0); + + this.kBNumConfigurations = new KeyDescriptorNumber(KEY_B_NUM_CONFIGURATIONS, true); + this.keys.add(this.kBNumConfigurations); + + this.kIdProduct = new KeyDescriptorNumber(KEY_ID_PRODUCT, true); + this.keys.add(this.kIdProduct); + + this.kIdVendor = new KeyDescriptorNumber(KEY_ID_VENDOR, true); + this.keys.add(this.kIdVendor); + + this.kIManufacturer = new KeyDescriptorNumber(KEY_I_MANUFACTURER, true); + this.keys.add(this.kIManufacturer); + + this.kIProduct = new KeyDescriptorNumber(KEY_I_PRODUCT, true); + this.keys.add(this.kIProduct); + + this.kISerialNumber = new KeyDescriptorNumber(KEY_I_SERIAL_NUMBER, true); + this.keys.add(this.kISerialNumber); + + this.kManufacturer = new KeyDescriptorString(KEY_MANUFACTURER, false, true); + this.keys.add(this.kManufacturer); + + this.kProduct = new KeyDescriptorString(KEY_PRODUCT, false, true); + this.keys.add(this.kProduct); + + this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, true); + this.keys.add(this.kSerialNumber); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/io/usb/USBPortFinder.java b/src/main/java/enedis/lab/io/usb/USBPortFinder.java index 391e3e2..1f3a0ca 100644 --- a/src/main/java/enedis/lab/io/usb/USBPortFinder.java +++ b/src/main/java/enedis/lab/io/usb/USBPortFinder.java @@ -1,73 +1,71 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.usb; - -import java.util.stream.Collectors; - -import enedis.lab.io.PortFinder; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; - -/** - * Interface used to find all USB port descriptor - */ -public interface USBPortFinder extends PortFinder -{ - /** - * Find USB device with the given product id - * - * @param idProduct the USB product identifier - * @return the USB descriptor data list of all USB device connected with the given product id - */ - public default DataList findByProductId(int idProduct) - { - DataList descriptors = new DataArrayList(); - // @formatter:off - descriptors.addAll(this.findAll().stream() - .filter(d -> d.getIdProduct().intValue() == idProduct) - .collect(Collectors.toList())); - // @formatter:on - return descriptors; - } - - /** - * Find USB device with the given vendor id - * - * @param idVendor the USB vendor identifier - * @return the USB descriptor data list of all USB device connected with the given vendor id - */ - public default DataList findByVendorId(int idVendor) - { - DataList descriptors = new DataArrayList(); - // @formatter:off - descriptors.addAll(this.findAll().stream() - .filter(d -> d.getIdVendor().intValue() == idVendor) - .collect(Collectors.toList())); - // @formatter:on - return descriptors; - } - - /** - * Find USB device with the given product id and vendor id - * - * @param idProduct the USB product identifier - * @param idVendor the USB vendor identifier - * @return the USB descriptor data list of all USB device connected with the given product id and vendor id - */ - public default DataList findByProductIdAndVendorId(int idProduct, int idVendor) - { - DataList descriptors = new DataArrayList(); - // @formatter:off - descriptors.addAll(this.findAll().stream() - .filter(d -> d.getIdProduct().intValue() == idProduct) - .filter(d -> d.getIdVendor().intValue() == idVendor) - .collect(Collectors.toList())); - // @formatter:on - return descriptors; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.usb; + +import enedis.lab.io.PortFinder; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataList; +import java.util.stream.Collectors; + +/** Interface used to find all USB port descriptor */ +public interface USBPortFinder extends PortFinder { + /** + * Find USB device with the given product id + * + * @param idProduct the USB product identifier + * @return the USB descriptor data list of all USB device connected with the given product id + */ + public default DataList findByProductId(int idProduct) { + DataList descriptors = new DataArrayList(); + // @formatter:off + descriptors.addAll( + this.findAll().stream() + .filter(d -> d.getIdProduct().intValue() == idProduct) + .collect(Collectors.toList())); + // @formatter:on + return descriptors; + } + + /** + * Find USB device with the given vendor id + * + * @param idVendor the USB vendor identifier + * @return the USB descriptor data list of all USB device connected with the given vendor id + */ + public default DataList findByVendorId(int idVendor) { + DataList descriptors = new DataArrayList(); + // @formatter:off + descriptors.addAll( + this.findAll().stream() + .filter(d -> d.getIdVendor().intValue() == idVendor) + .collect(Collectors.toList())); + // @formatter:on + return descriptors; + } + + /** + * Find USB device with the given product id and vendor id + * + * @param idProduct the USB product identifier + * @param idVendor the USB vendor identifier + * @return the USB descriptor data list of all USB device connected with the given product id and + * vendor id + */ + public default DataList findByProductIdAndVendorId( + int idProduct, int idVendor) { + DataList descriptors = new DataArrayList(); + // @formatter:off + descriptors.addAll( + this.findAll().stream() + .filter(d -> d.getIdProduct().intValue() == idProduct) + .filter(d -> d.getIdVendor().intValue() == idVendor) + .collect(Collectors.toList())); + // @formatter:on + return descriptors; + } +} diff --git a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java b/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java index 712626c..0b1491a 100644 --- a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java +++ b/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java @@ -1,201 +1,200 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.usb; - -import org.usb4java.Context; -import org.usb4java.Device; -import org.usb4java.DeviceDescriptor; -import org.usb4java.DeviceHandle; -import org.usb4java.DeviceList; -import org.usb4java.LibUsb; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * Class used to find all USB port descriptor - */ -public class USBPortFinderBase implements USBPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program writing the USB port descriptor list (JSON format) on the output stream - * - * @param args - * not used - */ - public static void main(String[] args) - { - DataList descriptors = getInstance().findAll(); - - System.out.println(descriptors.toString(2)); - } - - /** - * Get instance - * - * @return Unique instance - */ - public static USBPortFinderBase getInstance() - { - if (instance == null) - { - instance = new USBPortFinderBase(); - } - - return instance; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static USBPortFinderBase instance; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private USBPortFinderBase() - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// USBPortFinder - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataList findAll() - { - DataList usbPortList = new DataArrayList(); - - Context context = new Context(); - DeviceList deviceList = new DeviceList(); - - // Initialize USB library access - int result = LibUsb.init(context); - if (result != LibUsb.SUCCESS) - { - return usbPortList; - } - - // Get available USB device list - result = LibUsb.getDeviceList(context, deviceList); - if (result < 0) - { - return usbPortList; - } - - // For each USB device - for (Device device : deviceList) - { - DeviceDescriptor descriptor = new DeviceDescriptor(); - - // Get USB device descriptor - result = LibUsb.getDeviceDescriptor(device, descriptor); - if (result != LibUsb.SUCCESS) - { - continue; - } - String productName = null; - String manufacturer = null; - String serialNumber = null; - // If USB device has string descriptors - if (descriptor.idProduct() != 0) - { - // Get string descriptors for USB device - DeviceHandle handle = new DeviceHandle(); - result = LibUsb.open(device, handle); - if (result == LibUsb.SUCCESS) - { - productName = LibUsb.getStringDescriptor(handle, descriptor.iProduct()); - manufacturer = LibUsb.getStringDescriptor(handle, descriptor.iManufacturer()); - serialNumber = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber()); - LibUsb.close(handle); - } - } - - try - { - // @formatter:off - USBPortDescriptor usbPort = new USBPortDescriptor( - descriptor.bcdDevice() & 0xFFFF, - descriptor.bcdUSB() & 0xFFFF, - descriptor.bDescriptorType() & 0xFF, - descriptor.bDeviceClass() & 0xFF, - descriptor.bDeviceProtocol() & 0xFF, - descriptor.bDeviceSubClass() & 0xFF, - descriptor.bLength() & 0xFF, - descriptor.bMaxPacketSize0() & 0xFF, - descriptor.bNumConfigurations() & 0xFF, - descriptor.idProduct() & 0xFFFF, - descriptor.idVendor() & 0xFFFF, - descriptor.iManufacturer() & 0xFF, - descriptor.iProduct() & 0xFF, - descriptor.iSerialNumber() & 0xFF, - manufacturer, - productName, - serialNumber); - // @formatter:on - usbPortList.add(usbPort); - } - catch (DataDictionaryException e) - { - } - } - - // Finalize USB library access - LibUsb.freeDeviceList(deviceList, true); - LibUsb.exit(context); - return usbPortList; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.usb; + +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; +import org.usb4java.Context; +import org.usb4java.Device; +import org.usb4java.DeviceDescriptor; +import org.usb4java.DeviceHandle; +import org.usb4java.DeviceList; +import org.usb4java.LibUsb; + +/** Class used to find all USB port descriptor */ +public class USBPortFinderBase implements USBPortFinder { + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTANTS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// TYPES + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// STATIC METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Program writing the USB port descriptor list (JSON format) on the output stream + * + * @param args not used + */ + public static void main(String[] args) { + DataList descriptors = getInstance().findAll(); + + System.out.println(descriptors.toString(2)); + } + + /** + * Get instance + * + * @return Unique instance + */ + public static USBPortFinderBase getInstance() { + if (instance == null) { + instance = new USBPortFinderBase(); + } + + return instance; + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// ATTRIBUTES + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private static USBPortFinderBase instance; + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// CONSTRUCTORS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private USBPortFinderBase() {} + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// INTERFACE + /// USBPortFinder + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public DataList findAll() { + DataList usbPortList = new DataArrayList(); + + Context context = new Context(); + DeviceList deviceList = new DeviceList(); + + // Initialize USB library access + int result = LibUsb.init(context); + if (result != LibUsb.SUCCESS) { + return usbPortList; + } + + // Get available USB device list + result = LibUsb.getDeviceList(context, deviceList); + if (result < 0) { + return usbPortList; + } + + // For each USB device + for (Device device : deviceList) { + DeviceDescriptor descriptor = new DeviceDescriptor(); + + // Get USB device descriptor + result = LibUsb.getDeviceDescriptor(device, descriptor); + if (result != LibUsb.SUCCESS) { + continue; + } + String productName = null; + String manufacturer = null; + String serialNumber = null; + // If USB device has string descriptors + if (descriptor.idProduct() != 0) { + // Get string descriptors for USB device + DeviceHandle handle = new DeviceHandle(); + result = LibUsb.open(device, handle); + if (result == LibUsb.SUCCESS) { + productName = LibUsb.getStringDescriptor(handle, descriptor.iProduct()); + manufacturer = LibUsb.getStringDescriptor(handle, descriptor.iManufacturer()); + serialNumber = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber()); + LibUsb.close(handle); + } + } + + try { + // @formatter:off + USBPortDescriptor usbPort = + new USBPortDescriptor( + descriptor.bcdDevice() & 0xFFFF, + descriptor.bcdUSB() & 0xFFFF, + descriptor.bDescriptorType() & 0xFF, + descriptor.bDeviceClass() & 0xFF, + descriptor.bDeviceProtocol() & 0xFF, + descriptor.bDeviceSubClass() & 0xFF, + descriptor.bLength() & 0xFF, + descriptor.bMaxPacketSize0() & 0xFF, + descriptor.bNumConfigurations() & 0xFF, + descriptor.idProduct() & 0xFFFF, + descriptor.idVendor() & 0xFFFF, + descriptor.iManufacturer() & 0xFF, + descriptor.iProduct() & 0xFF, + descriptor.iSerialNumber() & 0xFF, + manufacturer, + productName, + serialNumber); + // @formatter:on + usbPortList.add(usbPort); + } catch (DataDictionaryException e) { + } + } + + // Finalize USB library access + LibUsb.freeDeviceList(deviceList, true); + LibUsb.exit(context); + return usbPortList; + } + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PUBLIC METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PROTECTED METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// PRIVATE METHODS + /// + /// + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} From 07a1a2493b69db2affc1e95f0b45b389529cd832 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Wed, 1 Oct 2025 12:10:06 +0200 Subject: [PATCH 18/59] chore: add spotless to pom.xml --- .gitignore | 1 + pom.xml | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 64ae858..8758e00 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ # # SPDX-License-Identifier: Apache-2.0 +.vscode/ src/main/resources/TIC2WebSocket.properties target/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5efdc3e..dacfd47 100644 --- a/pom.xml +++ b/pom.xml @@ -318,6 +318,11 @@ SPDX-License-Identifier: Apache-2.0 2.43.0 + + src/main/java/enedis/lab/io/*.java + src/main/java/enedis/lab/io/*/*.java + src/main/java/enedis/lab/io/channels/serialport/*.java + From 6d81028c56d85e8b8d1d75c9733737ca65456eec Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Fri, 3 Oct 2025 14:50:24 +0200 Subject: [PATCH 19/59] chore: remove bloc comment sections --- .../java/enedis/lab/types/BytesArray.java | 55 - .../java/enedis/lab/types/DataArrayList.java | 729 ++++----- .../lab/types/DataDictionaryException.java | 195 +-- .../java/enedis/lab/types/ExceptionBase.java | 209 +-- .../types/configuration/Configuration.java | 53 - .../configuration/ConfigurationBase.java | 317 ++-- .../configuration/ConfigurationException.java | 289 ++-- .../datadictionary/DataDictionaryBase.java | 1391 ++++++++--------- .../datadictionary/KeyDescriptorBase.java | 365 ++--- .../KeyDescriptorDataDictionary.java | 250 ++- .../datadictionary/KeyDescriptorEnum.java | 229 ++- .../datadictionary/KeyDescriptorList.java | 411 +++-- .../KeyDescriptorListMinMaxSize.java | 285 ++-- .../KeyDescriptorLocalDateTime.java | 258 ++- .../datadictionary/KeyDescriptorNumber.java | 223 +-- .../KeyDescriptorNumberMinMax.java | 273 ++-- .../datadictionary/KeyDescriptorString.java | 225 +-- .../java/enedis/lab/util/MinMaxChecker.java | 315 ++-- .../java/enedis/lab/util/SystemError.java | 185 +-- .../java/enedis/lab/util/message/Event.java | 323 ++-- .../java/enedis/lab/util/message/Message.java | 377 ++--- .../java/enedis/lab/util/message/Request.java | 255 ++- .../enedis/lab/util/message/Response.java | 463 +++--- .../enedis/lab/util/message/ResponseBase.java | 323 ++-- .../message/exception/MessageException.java | 169 +- .../MessageInvalidContentException.java | 169 +- .../MessageInvalidFormatException.java | 169 +- .../MessageInvalidTypeException.java | 169 +- .../MessageKeyNameDoesntExistException.java | 169 +- .../MessageKeyTypeDoesntExistException.java | 169 +- .../UnsupportedMessageException.java | 169 +- .../factory/AbstractMessageFactory.java | 249 ++- .../util/message/factory/MessageFactory.java | 299 ++-- .../lab/util/task/FilteredNotifierBase.java | 55 - .../enedis/lab/util/task/NotifierBase.java | 55 - .../java/enedis/lab/util/task/TaskBase.java | 356 ++--- .../enedis/lab/util/task/TaskPeriodic.java | 247 ++- .../task/TaskPeriodicWithSubscribers.java | 189 +-- src/main/java/enedis/lab/util/time/Time.java | 271 ++-- .../tic/core/ReadNextFrameSubscriber.java | 153 +- .../java/enedis/tic/core/TICCoreBase.java | 1086 ++++++------- .../java/enedis/tic/core/TICCoreError.java | 55 - .../enedis/tic/core/TICCoreException.java | 97 +- .../java/enedis/tic/core/TICCoreFrame.java | 55 - .../enedis/tic/core/TICCoreStreamBase.java | 680 ++++---- .../java/enedis/tic/core/TICIdentifier.java | 55 - .../tic/service/TIC2WebSocketApplication.java | 55 - .../tic/service/TIC2WebSocketCommandLine.java | 55 - .../service/client/TIC2WebSocketClient.java | 55 - .../client/TIC2WebSocketClientPoolBase.java | 55 - .../config/TIC2WebSocketConfiguration.java | 48 - .../TIC2WebSocketEndPointException.java | 55 - .../tic/service/message/EventOnError.java | 329 ++-- .../tic/service/message/EventOnTICData.java | 329 ++-- .../message/RequestGetAvailableTICs.java | 202 +-- .../service/message/RequestGetModemsInfo.java | 192 +-- .../tic/service/message/RequestReadTIC.java | 323 ++-- .../service/message/RequestSubscribeTIC.java | 325 ++-- .../message/RequestUnsubscribeTIC.java | 325 ++-- .../message/ResponseGetAvailableTICs.java | 339 ++-- .../message/ResponseGetModemsInfo.java | 339 ++-- .../tic/service/message/ResponseReadTIC.java | 337 ++-- .../service/message/ResponseSubscribeTIC.java | 265 ++-- .../message/ResponseUnsubscribeTIC.java | 265 ++-- .../TIC2WebSocketChannelInitializer.java | 53 - .../service/netty/TIC2WebSocketHandler.java | 55 - .../service/netty/TIC2WebSocketServer.java | 53 - .../TIC2WebSocketRequestHandlerBase.java | 55 - 68 files changed, 6710 insertions(+), 10482 deletions(-) diff --git a/src/main/java/enedis/lab/types/BytesArray.java b/src/main/java/enedis/lab/types/BytesArray.java index fb1cd50..067d224 100644 --- a/src/main/java/enedis/lab/types/BytesArray.java +++ b/src/main/java/enedis/lab/types/BytesArray.java @@ -18,12 +18,6 @@ */ public class BytesArray implements List { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** Default option */ public static final int DEFAULT_OPTIONS = 0x0000; /** Greedy option */ @@ -33,18 +27,6 @@ public class BytesArray implements List /** Remove patterns option */ public static final int REMOVE_PATTERNS = 0x0004; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Convert int to byte[] * @@ -103,20 +85,8 @@ private static int countBytes(int value) return count; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected byte[] buffer; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Default constructor */ @@ -135,13 +105,6 @@ public BytesArray(byte[] bytesArray) this.buffer = bytesArray.clone(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// List - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public int size() { @@ -587,12 +550,6 @@ else if ((toIndex < 0) || (toIndex >= this.buffer.length)) return subBytesArray; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Get bytes * @@ -997,18 +954,6 @@ public Integer checksum32() return checksum; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * * @param beginPattern diff --git a/src/main/java/enedis/lab/types/DataArrayList.java b/src/main/java/enedis/lab/types/DataArrayList.java index a6b267c..1f1348a 100644 --- a/src/main/java/enedis/lab/types/DataArrayList.java +++ b/src/main/java/enedis/lab/types/DataArrayList.java @@ -1,392 +1,337 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.RandomAccess; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Resizable-array implementation of the {@code DataList} interface. - * - * @param the type of elements in this data list - */ -public class DataArrayList extends ArrayList implements DataList -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = 3116080161929203526L; - - private static final int JSON_INDENT_FACTOR = 0; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Returns a fixed-size data list backed by the specified array. (Changes to - * the returned data list "write through" to the array.) This method acts - * as bridge between array-based and collection-based APIs, in - * combination with {@link Collection#toArray}. The returned list is - * serializable and implements {@link RandomAccess}. - * - *

    This method also provides a convenient way to create a fixed-size - * data list initialized to contain several elements: - *

    -     *     DataList<String> stooges = DataArrayList.asList("Larry", "Moe", "Curly");
    -     * 
    - * - * @param the class of the objects in the array - * @param items the array by which the data list will be backed - * @return a data list view of the specified array - */ - @SafeVarargs - public static DataList asList(E... items) - { - DataList dataList = new DataArrayList(); - - for(E item : items) - { - dataList.add(item); - } - - return dataList; - } - - /** - * Instantiate a data list from a File - * - * @param the class of the objects in the list - * @param file - * @param clazz - * - * @return a data list - * @throws JSONException - * @throws IOException - */ - public static DataList fromFile(File file,Class clazz) throws JSONException, IOException - { - InputStream stream = new FileInputStream(file); - - return fromStream(stream,clazz); - } - - /** - * Instantiate a data list from a Stream - * - * @param the class of the objects in the list - * @param stream - * @param clazz - * - * @return a data list - * @throws JSONException - * @throws IOException - */ - public static DataList fromStream(InputStream stream,Class clazz) throws JSONException, IOException - { - byte[] buffer = new byte[stream.available()]; - stream.read(buffer); - stream.close(); - String text = new String(buffer); - - return fromString(text,clazz); - } - - /** - * Instantiate a data list from a String - * - * @param the class of the objects in the list - * @param text - * @param clazz - * - * @return a data list - * @throws JSONException - */ - public static DataList fromString(String text,Class clazz) throws JSONException - { - if (text == null) - { - return null; - } - - JSONArray jsonArray = new JSONArray(text); - - return fromJSON(jsonArray,clazz); - } - - /** - * Instantiate a data list from a JSONObject - * - * @param the class of the objects in the list - * @param jsonArray - * @param clazz - * - * @return a data list - * @throws JSONException - */ - public static DataList fromJSON(JSONArray jsonArray, Class clazz) throws JSONException - { - if(jsonArray == null) - { - return null; - } - DataList dataList = new DataArrayList(); - for(int i = 0; i < jsonArray.length(); i++) - { - Object jsonItem = jsonArray.get(i); - E dataListItem; - try - { - if(JSONObject.NULL.equals(jsonItem)) - { - dataListItem = null; - } - else if(clazz.isAssignableFrom(jsonItem.getClass())) - { - dataListItem = clazz.cast(jsonItem); - } - else if(Enum.class.isAssignableFrom(clazz)) - { - dataListItem = convertToEnum(jsonItem,clazz); - } - else if(LocalDateTime.class.isAssignableFrom(clazz)) - { - dataListItem = convertToLocalDateTime(jsonItem,clazz); - } - else if(List.class.isAssignableFrom(clazz)) - { - dataListItem = convertToDataList(jsonItem,clazz); - } - else if(DataDictionary.class.isAssignableFrom(clazz)) - { - dataListItem = convertToDataDictionary(jsonItem,clazz); - } - else - { - throw new IllegalArgumentException("Cannot convert " + jsonItem.getClass().getName() + " to " + clazz.getName()); - } - } - catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) - { - throw new IllegalArgumentException("Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getMessage() + ")",e); - } - catch (InvocationTargetException e) - { - throw new IllegalArgumentException("Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getTargetException().getMessage() + ")",e); - } - dataList.add(dataListItem); - } - - return dataList; - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructs an empty data list with an initial capacity of ten. - */ - public DataArrayList() - { - super(); - } - - /** - * Constructs a data list containing the elements of the specified - * collection, in the order they are returned by the collection's - * iterator. - * - * @param collection the collection whose elements are to be placed into this data list - * @throws NullPointerException if the specified collection is null - */ - public DataArrayList(Collection collection) - { - super(collection); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public JSONArray toJSON() - { - JSONArray jsonArray = new JSONArray(); - - for(E item : this) - { - Object objectItem; - if(item instanceof DataDictionary) - { - DataDictionary dictionaryItem = (DataDictionary)item; - objectItem = dictionaryItem.toJSON(); - } - else - { - objectItem = item; - } - jsonArray.put(objectItem); - } - - return jsonArray; - } - - @Override - public void toFile(File file, int indentFactor) throws IOException - { - OutputStream stream = new FileOutputStream(file); - - this.toStream(stream, indentFactor); - } - - @Override - public void toStream(OutputStream stream, int indentFactor) throws IOException - { - String text = this.toString(indentFactor); - - stream.write(text.getBytes()); - } - - @Override - public String toString() - { - return this.toString(JSON_INDENT_FACTOR); - } - - @Override - public String toString(int identFactor) - { - return this.toJSON().toString(identFactor); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @SuppressWarnings("unchecked") - private static E convertToDataDictionary(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException - { - E result; - - if(value instanceof JSONObject) - { - Method method = clazz.getMethod("fromJSON",JSONObject.class,Class.class); - result = (E) method.invoke(null,value,clazz); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToDataList(Object value,Class clazz) - { - E result; - - if(value instanceof JSONArray) - { - result = (E) DataArrayList.fromJSON((JSONArray)value, Object.class); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToEnum(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException - { - E result; - - if(value instanceof String) - { - Method method = clazz.getDeclaredMethod("valueOf", String.class); - result = (E) method.invoke(null, (String)value); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToLocalDateTime(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException - { - E result; - - if(value instanceof String) - { - Method method = clazz.getDeclaredMethod("parse", CharSequence.class); - result = (E) method.invoke(null, (String)value); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.RandomAccess; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Resizable-array implementation of the {@code DataList} interface. + * + * @param the type of elements in this data list + */ +public class DataArrayList extends ArrayList implements DataList +{ + private static final long serialVersionUID = 3116080161929203526L; + + private static final int JSON_INDENT_FACTOR = 0; + + /** + * Returns a fixed-size data list backed by the specified array. (Changes to + * the returned data list "write through" to the array.) This method acts + * as bridge between array-based and collection-based APIs, in + * combination with {@link Collection#toArray}. The returned list is + * serializable and implements {@link RandomAccess}. + * + *

    This method also provides a convenient way to create a fixed-size + * data list initialized to contain several elements: + *

    +     *     DataList<String> stooges = DataArrayList.asList("Larry", "Moe", "Curly");
    +     * 
    + * + * @param the class of the objects in the array + * @param items the array by which the data list will be backed + * @return a data list view of the specified array + */ + @SafeVarargs + public static DataList asList(E... items) + { + DataList dataList = new DataArrayList(); + + for(E item : items) + { + dataList.add(item); + } + + return dataList; + } + + /** + * Instantiate a data list from a File + * + * @param the class of the objects in the list + * @param file + * @param clazz + * + * @return a data list + * @throws JSONException + * @throws IOException + */ + public static DataList fromFile(File file,Class clazz) throws JSONException, IOException + { + InputStream stream = new FileInputStream(file); + + return fromStream(stream,clazz); + } + + /** + * Instantiate a data list from a Stream + * + * @param the class of the objects in the list + * @param stream + * @param clazz + * + * @return a data list + * @throws JSONException + * @throws IOException + */ + public static DataList fromStream(InputStream stream,Class clazz) throws JSONException, IOException + { + byte[] buffer = new byte[stream.available()]; + stream.read(buffer); + stream.close(); + String text = new String(buffer); + + return fromString(text,clazz); + } + + /** + * Instantiate a data list from a String + * + * @param the class of the objects in the list + * @param text + * @param clazz + * + * @return a data list + * @throws JSONException + */ + public static DataList fromString(String text,Class clazz) throws JSONException + { + if (text == null) + { + return null; + } + + JSONArray jsonArray = new JSONArray(text); + + return fromJSON(jsonArray,clazz); + } + + /** + * Instantiate a data list from a JSONObject + * + * @param the class of the objects in the list + * @param jsonArray + * @param clazz + * + * @return a data list + * @throws JSONException + */ + public static DataList fromJSON(JSONArray jsonArray, Class clazz) throws JSONException + { + if(jsonArray == null) + { + return null; + } + DataList dataList = new DataArrayList(); + for(int i = 0; i < jsonArray.length(); i++) + { + Object jsonItem = jsonArray.get(i); + E dataListItem; + try + { + if(JSONObject.NULL.equals(jsonItem)) + { + dataListItem = null; + } + else if(clazz.isAssignableFrom(jsonItem.getClass())) + { + dataListItem = clazz.cast(jsonItem); + } + else if(Enum.class.isAssignableFrom(clazz)) + { + dataListItem = convertToEnum(jsonItem,clazz); + } + else if(LocalDateTime.class.isAssignableFrom(clazz)) + { + dataListItem = convertToLocalDateTime(jsonItem,clazz); + } + else if(List.class.isAssignableFrom(clazz)) + { + dataListItem = convertToDataList(jsonItem,clazz); + } + else if(DataDictionary.class.isAssignableFrom(clazz)) + { + dataListItem = convertToDataDictionary(jsonItem,clazz); + } + else + { + throw new IllegalArgumentException("Cannot convert " + jsonItem.getClass().getName() + " to " + clazz.getName()); + } + } + catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) + { + throw new IllegalArgumentException("Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getMessage() + ")",e); + } + catch (InvocationTargetException e) + { + throw new IllegalArgumentException("Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getTargetException().getMessage() + ")",e); + } + dataList.add(dataListItem); + } + + return dataList; + } + /** + * Constructs an empty data list with an initial capacity of ten. + */ + public DataArrayList() + { + super(); + } + + /** + * Constructs a data list containing the elements of the specified + * collection, in the order they are returned by the collection's + * iterator. + * + * @param collection the collection whose elements are to be placed into this data list + * @throws NullPointerException if the specified collection is null + */ + public DataArrayList(Collection collection) + { + super(collection); + } + + @Override + public JSONArray toJSON() + { + JSONArray jsonArray = new JSONArray(); + + for(E item : this) + { + Object objectItem; + if(item instanceof DataDictionary) + { + DataDictionary dictionaryItem = (DataDictionary)item; + objectItem = dictionaryItem.toJSON(); + } + else + { + objectItem = item; + } + jsonArray.put(objectItem); + } + + return jsonArray; + } + + @Override + public void toFile(File file, int indentFactor) throws IOException + { + OutputStream stream = new FileOutputStream(file); + + this.toStream(stream, indentFactor); + } + + @Override + public void toStream(OutputStream stream, int indentFactor) throws IOException + { + String text = this.toString(indentFactor); + + stream.write(text.getBytes()); + } + + @Override + public String toString() + { + return this.toString(JSON_INDENT_FACTOR); + } + + @Override + public String toString(int identFactor) + { + return this.toJSON().toString(identFactor); + } + + @SuppressWarnings("unchecked") + private static E convertToDataDictionary(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + E result; + + if(value instanceof JSONObject) + { + Method method = clazz.getMethod("fromJSON",JSONObject.class,Class.class); + result = (E) method.invoke(null,value,clazz); + } + else + { + throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } + + @SuppressWarnings("unchecked") + private static E convertToDataList(Object value,Class clazz) + { + E result; + + if(value instanceof JSONArray) + { + result = (E) DataArrayList.fromJSON((JSONArray)value, Object.class); + } + else + { + throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } + + @SuppressWarnings("unchecked") + private static E convertToEnum(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + E result; + + if(value instanceof String) + { + Method method = clazz.getDeclaredMethod("valueOf", String.class); + result = (E) method.invoke(null, (String)value); + } + else + { + throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } + + @SuppressWarnings("unchecked") + private static E convertToLocalDateTime(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + E result; + + if(value instanceof String) + { + Method method = clazz.getDeclaredMethod("parse", CharSequence.class); + result = (E) method.invoke(null, (String)value); + } + else + { + throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } +} diff --git a/src/main/java/enedis/lab/types/DataDictionaryException.java b/src/main/java/enedis/lab/types/DataDictionaryException.java index 0d2f088..516f154 100644 --- a/src/main/java/enedis/lab/types/DataDictionaryException.java +++ b/src/main/java/enedis/lab/types/DataDictionaryException.java @@ -1,125 +1,70 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -/** - * Data dictionary exception - */ -public class DataDictionaryException extends Exception -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -7967428428453584771L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public DataDictionaryException() - { - super(); - } - - /** - * Constructor using message, cause, enable suppression flag and writable stack trace flag - * - * @param message - * @param cause - * @param enableSuppression - * @param writableStackTrace - */ - public DataDictionaryException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) - { - super(message, cause, enableSuppression, writableStackTrace); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public DataDictionaryException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public DataDictionaryException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public DataDictionaryException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +/** + * Data dictionary exception + */ +public class DataDictionaryException extends Exception +{ + + private static final long serialVersionUID = -7967428428453584771L; + + /** + * Default constructor + */ + public DataDictionaryException() + { + super(); + } + + /** + * Constructor using message, cause, enable suppression flag and writable stack trace flag + * + * @param message + * @param cause + * @param enableSuppression + * @param writableStackTrace + */ + public DataDictionaryException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) + { + super(message, cause, enableSuppression, writableStackTrace); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public DataDictionaryException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public DataDictionaryException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public DataDictionaryException(Throwable cause) + { + super(cause); + } + +} diff --git a/src/main/java/enedis/lab/types/ExceptionBase.java b/src/main/java/enedis/lab/types/ExceptionBase.java index 99e3e7f..b871cc9 100644 --- a/src/main/java/enedis/lab/types/ExceptionBase.java +++ b/src/main/java/enedis/lab/types/ExceptionBase.java @@ -1,132 +1,77 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -/** - * Exception base - */ -@SuppressWarnings("serial") -public class ExceptionBase extends Exception -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected int code; - protected String info; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param code - * @param info - */ - public ExceptionBase(int code, String info) - { - this.setErrorCode(code); - this.setErrorInfo(info); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Exception - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public String getMessage() - { - return this.info + " (" + this.code + ")"; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get error code - * - * @return error code - */ - public int getErrorCode() - { - return this.code; - } - - /** - * Set error code - * - * @param errorCode - */ - public void setErrorCode(int errorCode) - { - this.code = errorCode; - } - - /** - * Get error info - * - * @return error info - */ - public String getErrorInfo() - { - return this.info; - } - - /** - * Set error info - * - * @param errorInfo - */ - public void setErrorInfo(String errorInfo) - { - this.info = errorInfo; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +/** + * Exception base + */ +@SuppressWarnings("serial") +public class ExceptionBase extends Exception +{ + protected int code; + protected String info; + + /** + * Constructor + * + * @param code + * @param info + */ + public ExceptionBase(int code, String info) + { + this.setErrorCode(code); + this.setErrorInfo(info); + } + + @Override + public String getMessage() + { + return this.info + " (" + this.code + ")"; + } + + /** + * Get error code + * + * @return error code + */ + public int getErrorCode() + { + return this.code; + } + + /** + * Set error code + * + * @param errorCode + */ + public void setErrorCode(int errorCode) + { + this.code = errorCode; + } + + /** + * Get error info + * + * @return error info + */ + public String getErrorInfo() + { + return this.info; + } + + /** + * Set error info + * + * @param errorInfo + */ + public void setErrorInfo(String errorInfo) + { + this.info = errorInfo; + } + +} diff --git a/src/main/java/enedis/lab/types/configuration/Configuration.java b/src/main/java/enedis/lab/types/configuration/Configuration.java index 46939e9..e483dbb 100644 --- a/src/main/java/enedis/lab/types/configuration/Configuration.java +++ b/src/main/java/enedis/lab/types/configuration/Configuration.java @@ -16,48 +16,6 @@ */ public interface Configuration extends DataDictionary { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Load configuration from file @@ -89,16 +47,5 @@ public interface Configuration extends DataDictionary */ public File getConfigFile(); - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java b/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java index be58331..4e25ff4 100644 --- a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java +++ b/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java @@ -1,186 +1,131 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.configuration; - -import java.io.File; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; - -/** - * Basic implementation of Configuration - */ -public class ConfigurationBase extends DataDictionaryBase implements Configuration -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private String name = null; - private File file = null; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ConfigurationBase() - { - super(); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ConfigurationBase(DataDictionary other) throws DataDictionaryException - { - super(other); - } - - /** - * Constructor using Map - * - * @param map - * @throws DataDictionaryException - */ - public ConfigurationBase(Map map) throws DataDictionaryException - { - super(map); - } - - /** - * Constructor using name and file - * - * @param name - * @param file - */ - public ConfigurationBase(String name, File file) - { - super(); - this.init(name, file); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Configuration - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void load() throws ConfigurationException - { - DataDictionary configuration; - - try - { - configuration = DataDictionaryBase.fromFile(this.file, this.getClass()); - } - catch (Exception exception) - { - throw new ConfigurationException("Cannot load configuration '" + ((this.name == null) ? "" : this.name) + "' from file '" + ((this.file == null) ? "" : this.file) - + "' (" + exception.getMessage() + ") " + exception.getClass().getSimpleName()); - } - try - { - this.copy(configuration); - } - catch (Exception exception) - { - throw new ConfigurationException("Cannot copy configuration '" + ((this.name == null) ? "" : this.name) + "' from file '" + ((this.file == null) ? "" : this.file) - + "' (" + exception.getMessage() + ")"); - } - } - - @Override - public void save() throws ConfigurationException - { - try - { - this.toFile(this.file, 2); - } - catch (Exception exception) - { - throw new ConfigurationException("Cannot save configuration '" + ((this.name == null) ? "" : this.name) + "' to file '" + ((this.file == null) ? "" : this.file) + "' (" - + exception.getMessage() + ")"); - } - } - - @Override - public String getConfigName() - { - return this.name; - } - - @Override - public File getConfigFile() - { - return this.file; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Set config name - * - * @param configName - */ - public void setConfigName(String configName) - { - this.name = configName; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void init(String name, File file) - { - this.name = name; - this.file = file; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.configuration; + +import java.io.File; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; + +/** + * Basic implementation of Configuration + */ +public class ConfigurationBase extends DataDictionaryBase implements Configuration +{ + private String name = null; + private File file = null; + + protected ConfigurationBase() + { + super(); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ConfigurationBase(DataDictionary other) throws DataDictionaryException + { + super(other); + } + + /** + * Constructor using Map + * + * @param map + * @throws DataDictionaryException + */ + public ConfigurationBase(Map map) throws DataDictionaryException + { + super(map); + } + + /** + * Constructor using name and file + * + * @param name + * @param file + */ + public ConfigurationBase(String name, File file) + { + super(); + this.init(name, file); + } + + @Override + public void load() throws ConfigurationException + { + DataDictionary configuration; + + try + { + configuration = DataDictionaryBase.fromFile(this.file, this.getClass()); + } + catch (Exception exception) + { + throw new ConfigurationException("Cannot load configuration '" + ((this.name == null) ? "" : this.name) + "' from file '" + ((this.file == null) ? "" : this.file) + + "' (" + exception.getMessage() + ") " + exception.getClass().getSimpleName()); + } + try + { + this.copy(configuration); + } + catch (Exception exception) + { + throw new ConfigurationException("Cannot copy configuration '" + ((this.name == null) ? "" : this.name) + "' from file '" + ((this.file == null) ? "" : this.file) + + "' (" + exception.getMessage() + ")"); + } + } + + @Override + public void save() throws ConfigurationException + { + try + { + this.toFile(this.file, 2); + } + catch (Exception exception) + { + throw new ConfigurationException("Cannot save configuration '" + ((this.name == null) ? "" : this.name) + "' to file '" + ((this.file == null) ? "" : this.file) + "' (" + + exception.getMessage() + ")"); + } + } + + @Override + public String getConfigName() + { + return this.name; + } + + @Override + public File getConfigFile() + { + return this.file; + } + + /** + * Set config name + * + * @param configName + */ + public void setConfigName(String configName) + { + this.name = configName; + } + + protected void init(String name, File file) + { + this.name = name; + this.file = file; + } + +} diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java b/src/main/java/enedis/lab/types/configuration/ConfigurationException.java index 3e435e2..cfdf7c3 100644 --- a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java +++ b/src/main/java/enedis/lab/types/configuration/ConfigurationException.java @@ -1,172 +1,117 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.configuration; - -import enedis.lab.types.DataDictionaryException; - -/** - * Configuration exception - */ -public class ConfigurationException extends DataDictionaryException -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = 8090072693974075297L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Create missing parameter exception - * - * @param parameter - * @return configurationException - */ - public static ConfigurationException createMissingParameterException(String parameter) - { - return new ConfigurationException("Parameter \'" + parameter + "\' is missing"); - } - - /** - * Create unknown parameter exception - * - * @param parameter - * @return configurationException - */ - public static ConfigurationException createUnknownParameterException(String parameter) - { - return new ConfigurationException("Parameter \'" + parameter + "\' is unknown"); - } - - /** - * Create invalid parameter value exception - * - * @param parameter - * @param info - * @return configurationException - */ - public static ConfigurationException createInvalidParameterValueException(String parameter, String info) - { - return new ConfigurationException("Parameter \'" + parameter + "\' has an invalid value : " + info); - } - - /** - * Create invalid paramter type exception - * - * @param parameter - * @param type - * @return configurationException - */ - public static ConfigurationException createInvalidParameterTypeException(String parameter, Class type) - { - return new ConfigurationException("Parameter \'" + parameter + "\' has an invalid type : " + type.getName()); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public ConfigurationException() - { - super(); - } - - /** - * Constructor using message, cause, enable suppression flag and writable stack trace flag - * - * @param message - * @param cause - * @param enableSuppression - * @param writableStackTrace - */ - public ConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) - { - super(message, cause, enableSuppression, writableStackTrace); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public ConfigurationException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public ConfigurationException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public ConfigurationException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.configuration; + +import enedis.lab.types.DataDictionaryException; + +/** + * Configuration exception + */ +public class ConfigurationException extends DataDictionaryException +{ + private static final long serialVersionUID = 8090072693974075297L; + + /** + * Create missing parameter exception + * + * @param parameter + * @return configurationException + */ + public static ConfigurationException createMissingParameterException(String parameter) + { + return new ConfigurationException("Parameter \'" + parameter + "\' is missing"); + } + + /** + * Create unknown parameter exception + * + * @param parameter + * @return configurationException + */ + public static ConfigurationException createUnknownParameterException(String parameter) + { + return new ConfigurationException("Parameter \'" + parameter + "\' is unknown"); + } + + /** + * Create invalid parameter value exception + * + * @param parameter + * @param info + * @return configurationException + */ + public static ConfigurationException createInvalidParameterValueException(String parameter, String info) + { + return new ConfigurationException("Parameter \'" + parameter + "\' has an invalid value : " + info); + } + + /** + * Create invalid paramter type exception + * + * @param parameter + * @param type + * @return configurationException + */ + public static ConfigurationException createInvalidParameterTypeException(String parameter, Class type) + { + return new ConfigurationException("Parameter \'" + parameter + "\' has an invalid type : " + type.getName()); + } + + /** + * Default constructor + */ + public ConfigurationException() + { + super(); + } + + /** + * Constructor using message, cause, enable suppression flag and writable stack trace flag + * + * @param message + * @param cause + * @param enableSuppression + * @param writableStackTrace + */ + public ConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) + { + super(message, cause, enableSuppression, writableStackTrace); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public ConfigurationException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public ConfigurationException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public ConfigurationException(Throwable cause) + { + super(cause); + } + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java b/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java index 02ca337..fb4cf55 100644 --- a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java +++ b/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java @@ -1,723 +1,668 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * DataDictionary basic implementation - */ -public class DataDictionaryBase implements DataDictionary -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final int JSON_INDENT_FACTOR = 0; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Instantiate a datadictionary from a File - * - * @param file - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - * @throws IOException - */ - public static DataDictionary fromFile(File file, Class clazz) throws JSONException, DataDictionaryException, IOException - { - if (file == null) - { - throw new DataDictionaryException("Can't load file from null"); - } - - InputStream stream = new FileInputStream(file); - - return DataDictionaryBase.fromStream(stream, clazz); - } - - /** - * Instantiate a datadictionary from a Stream - * - * @param stream - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - * @throws IOException - */ - public static DataDictionary fromStream(InputStream stream, Class clazz) throws JSONException, DataDictionaryException, IOException - { - if (stream == null) - { - return null; - } - - byte[] buffer = new byte[stream.available()]; - stream.read(buffer); - stream.close(); - String text = new String(buffer); - - return DataDictionaryBase.fromString(text, clazz); - } - - /** - * Instantiate a datadictionary from a String - * - * @param text - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromString(String text, Class clazz) throws JSONException, DataDictionaryException - { - if (text == null) - { - return null; - } - - JSONObject jsonObject = new JSONObject(text); - - return DataDictionaryBase.fromJSON(jsonObject, clazz); - } - - /** - * Instantiate a datadictionary from a JSONObject - * - * @param jsonObject - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromJSON(JSONObject jsonObject, Class clazz) throws JSONException, DataDictionaryException - { - if (jsonObject == null) - { - return null; - } - - return fromMap(jsonObject.toMap(), clazz); - } - - /** - * Instantiate a datadictionary from a Map - * - * @param map - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromMap(Map map, Class clazz) throws DataDictionaryException - { - if (map == null) - { - return null; - } - if (clazz == null) - { - return fromMap(map); - } - DataDictionary dataDictionnary; - try - { - Constructor constructor = clazz.getConstructor(Map.class); - dataDictionnary = constructor.newInstance(map); - } - catch (InvocationTargetException exception) - { - throw new DataDictionaryException("Cannot create instance of " + clazz.getName() + " : " + exception.getTargetException().getMessage()); - } - catch (Exception exception) - { - throw new DataDictionaryException("Cannot create instance of " + clazz.getName() + " : " + exception.getMessage()); - } - - return dataDictionnary; - } - - /** - * Instantiate a datadictionary from a Map - * - * @param map - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromMap(Map map) throws DataDictionaryException - { - if (map == null) - { - return null; - } - DataDictionaryBase dataDictionnary = new DataDictionaryBase(); - for (String key : map.keySet()) - { - Object value = map.get(key); - if (value instanceof JSONObject) - { - JSONObject jsonObjectValue = (JSONObject) value; - DataDictionary dataDictionaryValue = fromMap(jsonObjectValue.toMap()); - dataDictionnary.set(key, dataDictionaryValue); - } - else if (value instanceof JSONArray) - { - JSONArray jsonArrayValue = (JSONArray) value; - DataList listValue = new DataArrayList(jsonArrayValue.toList()); - dataDictionnary.set(key, listValue); - } - else - { - dataDictionnary.set(key, value); - } - } - - return dataDictionnary; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keyDescriptors; - protected HashMap data; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public DataDictionaryBase() - { - this.init(); - } - - /** - * Constructor using map and setting key not defined allowed flag - * - * @param map - * @throws DataDictionaryException - */ - public DataDictionaryBase(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using dataDictionary and setting key not defined allowed flag - * - * @param other - * @throws DataDictionaryException - */ - public DataDictionaryBase(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionnary - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public final boolean exists(String key) - { - return this.data.containsKey(key); - } - - @Override - public final boolean existsInKeys(String key) - { - // @formatter:off - return this.keyDescriptors.stream() - .filter(k -> k.getName().equals(key)) - .findAny() - .isPresent(); - // @formatter:on - } - - @Override - public final String[] keys() - { - Set setKeys = this.data.keySet(); - String[] keys = new String[setKeys.size()]; - - setKeys.toArray(keys); - - return keys; - } - - @Override - public final void addKey(String key) - { - if (key != null) - { - this.data.putIfAbsent(key, null); - } - } - - @Override - public final void removeKey(String key) - { - if (key != null) - { - this.data.remove(key); - } - } - - @Override - public final Object get(String key) - { - return this.data.get(key); - } - - @Override - public final void set(String key, Object value) throws DataDictionaryException - { - if (key == null) - { - throw new DataDictionaryException("Key null not allowed"); - } - - if (this.keyDescriptors.isEmpty()) - { - this.data.put(key, value); - } - else - { - Optional> keyDescriptor = this.getKeyDescriptor(key); - if (keyDescriptor.isPresent()) - { - try - { - String setterName = this.getSetterName(key); - // @formatter:off - List methodNameList = Arrays.asList(this.getClass().getDeclaredMethods()) - .stream() - .map(m -> m.getName()) - .collect(Collectors.toList()); - // @formatter:on - if (methodNameList.contains(setterName)) - { - Method method = this.getClass().getDeclaredMethod(setterName, Object.class); - method.setAccessible(true); - method.invoke(this, value); - } - else - { - this.data.put(key, keyDescriptor.get().convert(value)); - } - } - catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) - { - throw new DataDictionaryException("Set key " + key + " failed : " + e.getClass().getSimpleName() + " -> " + e.getMessage()); - } - catch (InvocationTargetException e) - { - throw new DataDictionaryException( - "Set key " + key + " failed : " + e.getTargetException().getClass().getSimpleName() + " -> " + e.getTargetException().getMessage()); - } - - } - else - { - throw new DataDictionaryException("Key " + key + " not allowed"); - } - } - } - - @Override - public void copy(DataDictionary other) throws DataDictionaryException - { - String[] otherKeys = other.keys(); - - this.clear(); - for (int i = 0; i < otherKeys.length; i++) - { - this.set(otherKeys[i], other.get(otherKeys[i])); - } - - this.checkAndUpdate(); - } - - @Override - public final void clear() - { - this.data.clear(); - } - - @Override - public final int hashCode() - { - final int prime = 31; - int result = 1; - result = prime * result + ((this.data == null) ? 0 : this.data.hashCode()); - return result; - } - - @Override - public final boolean equals(Object object) - { - if (object == null) - { - return false; - } - if (this == object) - { - return true; - } - if (!(object instanceof DataDictionary)) - { - return false; - } - DataDictionaryBase other = (DataDictionaryBase) object; - - if (this.data == null) - { - if (other.data != null) - { - return false; - } - } - else - { - if (!this.data.keySet().equals(other.data.keySet())) - { - return false; - } - for (String key : this.data.keySet()) - { - Object thisValue = this.data.get(key); - Object otherValue = other.data.get(key); - - if (thisValue == null) - { - if (otherValue != null) - { - return false; - } - else - { - continue; - } - } - else if (otherValue == null) - { - return false; - } - if (!thisValue.getClass().equals(otherValue.getClass())) - { - if ((thisValue instanceof Number) && (otherValue instanceof Number)) - { - double thisDoubleValue = ((Number) thisValue).doubleValue(); - double otherDoubleValue = ((Number) otherValue).doubleValue(); - return thisDoubleValue == otherDoubleValue; - } - else - { - return false; - } - } - if (thisValue.getClass().isArray()) - { - if (thisValue instanceof boolean[]) - { - if (!Arrays.equals((boolean[]) thisValue, (boolean[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof byte[]) - { - if (!Arrays.equals((byte[]) thisValue, (byte[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof char[]) - { - if (!Arrays.equals((char[]) thisValue, (char[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof short[]) - { - if (!Arrays.equals((short[]) thisValue, (short[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof int[]) - { - if (!Arrays.equals((int[]) thisValue, (int[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof long[]) - { - if (!Arrays.equals((long[]) thisValue, (long[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof float[]) - { - if (!Arrays.equals((float[]) thisValue, (float[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof double[]) - { - if (!Arrays.equals((double[]) thisValue, (double[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof Object[]) - { - if (!Arrays.equals((Object[]) thisValue, (Object[]) otherValue)) - { - return false; - } - } - } - else - { - if (!thisValue.equals(otherValue)) - { - return false; - } - } - } - } - - return true; - } - - @Override - public final void toFile(File file, int indentFactor) throws IOException - { - OutputStream stream = new FileOutputStream(file); - - this.toStream(stream, indentFactor); - } - - @Override - public final void toStream(OutputStream stream, int indentFactor) throws IOException - { - String text = this.toString(indentFactor); - - stream.write(text.getBytes()); - } - - @Override - public JSONObject toJSON() - { - JSONObject jsonObject = new JSONObject(); - String[] keys = this.keys(); - - for (int i = 0; i < keys.length; i++) - { - Object value = this.get(keys[i]); - if (value instanceof DataDictionaryBase) - { - DataDictionaryBase dataDictionary = (DataDictionaryBase) value; - jsonObject.put(keys[i], dataDictionary.toJSON()); - } - else if (value instanceof LocalDateTime) - { - String textValue = ((LocalDateTime) value).format(KeyDescriptorLocalDateTime.DEFAULT_FORMATTER); - jsonObject.put(keys[i], textValue); - } - else - { - jsonObject.put(keys[i], value); - } - } - - return jsonObject; - } - - @SuppressWarnings("unchecked") - @Override - public final Map toMap() - { - return (Map) this.data.clone(); - } - - @Override - public String toString() - { - return this.toString(DataDictionaryBase.JSON_INDENT_FACTOR); - } - - @Override - public String toString(int identFactor) - { - return this.toJSON().toString(identFactor); - } - - @Override - public DataDictionaryBase clone() - { - try - { - Constructor constructor = this.getClass().getConstructor(DataDictionary.class); - return this.getClass().cast(constructor.newInstance(this)); - } - catch (Exception e) - { - throw new RuntimeException(e.getMessage(), e); - } - } - - @Override - public void print() - { - System.out.println(this.toString(2)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected final void checkAndUpdate() throws DataDictionaryException - { - this.updateOptionalParameters(); - this.checkMandatoryParameters(); - } - - protected void checkMandatoryParameters() throws DataDictionaryException - { - for (KeyDescriptor key : this.keyDescriptors) - { - if (key.isMandatory() && !this.exists(key.getName())) - { - throw new DataDictionaryException("Mandatory key " + key.getName() + " not defined"); - } - } - } - - protected void updateOptionalParameters() throws DataDictionaryException - { - } - - protected void addKeyDescriptor(KeyDescriptor keyDescriptor) throws DataDictionaryException - { - if (this.existsInKeys(keyDescriptor.getName())) - { - throw new DataDictionaryException("Key " + keyDescriptor.getName() + "already exists"); - } - this.keyDescriptors.add(keyDescriptor); - } - - protected void addAllKeyDescriptor(List> keyDescriptor) throws DataDictionaryException - { - for (KeyDescriptor key : keyDescriptor) - { - this.addKeyDescriptor(key); - } - } - - protected Optional> getKeyDescriptor(String name) - { - // @formatter:off - return this.keyDescriptors.stream() - .filter(k -> k.getName().equals(name)) - .findFirst(); - // @formatter:on - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private String getSetterName(String key) - { - return "set" + key.substring(0, 1).toUpperCase() + key.substring(1); - } - - private void init() - { - this.keyDescriptors = new ArrayList>(); - this.data = new HashMap(); - } - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; + +/** + * DataDictionary basic implementation + */ +public class DataDictionaryBase implements DataDictionary +{ + private static final int JSON_INDENT_FACTOR = 0; + + /** + * Instantiate a datadictionary from a File + * + * @param file + * @param clazz + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + * @throws IOException + */ + public static DataDictionary fromFile(File file, Class clazz) throws JSONException, DataDictionaryException, IOException + { + if (file == null) + { + throw new DataDictionaryException("Can't load file from null"); + } + + InputStream stream = new FileInputStream(file); + + return DataDictionaryBase.fromStream(stream, clazz); + } + + /** + * Instantiate a datadictionary from a Stream + * + * @param stream + * @param clazz + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + * @throws IOException + */ + public static DataDictionary fromStream(InputStream stream, Class clazz) throws JSONException, DataDictionaryException, IOException + { + if (stream == null) + { + return null; + } + + byte[] buffer = new byte[stream.available()]; + stream.read(buffer); + stream.close(); + String text = new String(buffer); + + return DataDictionaryBase.fromString(text, clazz); + } + + /** + * Instantiate a datadictionary from a String + * + * @param text + * @param clazz + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromString(String text, Class clazz) throws JSONException, DataDictionaryException + { + if (text == null) + { + return null; + } + + JSONObject jsonObject = new JSONObject(text); + + return DataDictionaryBase.fromJSON(jsonObject, clazz); + } + + /** + * Instantiate a datadictionary from a JSONObject + * + * @param jsonObject + * @param clazz + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromJSON(JSONObject jsonObject, Class clazz) throws JSONException, DataDictionaryException + { + if (jsonObject == null) + { + return null; + } + + return fromMap(jsonObject.toMap(), clazz); + } + + /** + * Instantiate a datadictionary from a Map + * + * @param map + * @param clazz + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromMap(Map map, Class clazz) throws DataDictionaryException + { + if (map == null) + { + return null; + } + if (clazz == null) + { + return fromMap(map); + } + DataDictionary dataDictionnary; + try + { + Constructor constructor = clazz.getConstructor(Map.class); + dataDictionnary = constructor.newInstance(map); + } + catch (InvocationTargetException exception) + { + throw new DataDictionaryException("Cannot create instance of " + clazz.getName() + " : " + exception.getTargetException().getMessage()); + } + catch (Exception exception) + { + throw new DataDictionaryException("Cannot create instance of " + clazz.getName() + " : " + exception.getMessage()); + } + + return dataDictionnary; + } + + /** + * Instantiate a datadictionary from a Map + * + * @param map + * + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromMap(Map map) throws DataDictionaryException + { + if (map == null) + { + return null; + } + DataDictionaryBase dataDictionnary = new DataDictionaryBase(); + for (String key : map.keySet()) + { + Object value = map.get(key); + if (value instanceof JSONObject) + { + JSONObject jsonObjectValue = (JSONObject) value; + DataDictionary dataDictionaryValue = fromMap(jsonObjectValue.toMap()); + dataDictionnary.set(key, dataDictionaryValue); + } + else if (value instanceof JSONArray) + { + JSONArray jsonArrayValue = (JSONArray) value; + DataList listValue = new DataArrayList(jsonArrayValue.toList()); + dataDictionnary.set(key, listValue); + } + else + { + dataDictionnary.set(key, value); + } + } + + return dataDictionnary; + } + + private List> keyDescriptors; + protected HashMap data; + + /** + * Default constructor + */ + public DataDictionaryBase() + { + this.init(); + } + + /** + * Constructor using map and setting key not defined allowed flag + * + * @param map + * @throws DataDictionaryException + */ + public DataDictionaryBase(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using dataDictionary and setting key not defined allowed flag + * + * @param other + * @throws DataDictionaryException + */ + public DataDictionaryBase(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + @Override + public final boolean exists(String key) + { + return this.data.containsKey(key); + } + + @Override + public final boolean existsInKeys(String key) + { + // @formatter:off + return this.keyDescriptors.stream() + .filter(k -> k.getName().equals(key)) + .findAny() + .isPresent(); + // @formatter:on + } + + @Override + public final String[] keys() + { + Set setKeys = this.data.keySet(); + String[] keys = new String[setKeys.size()]; + + setKeys.toArray(keys); + + return keys; + } + + @Override + public final void addKey(String key) + { + if (key != null) + { + this.data.putIfAbsent(key, null); + } + } + + @Override + public final void removeKey(String key) + { + if (key != null) + { + this.data.remove(key); + } + } + + @Override + public final Object get(String key) + { + return this.data.get(key); + } + + @Override + public final void set(String key, Object value) throws DataDictionaryException + { + if (key == null) + { + throw new DataDictionaryException("Key null not allowed"); + } + + if (this.keyDescriptors.isEmpty()) + { + this.data.put(key, value); + } + else + { + Optional> keyDescriptor = this.getKeyDescriptor(key); + if (keyDescriptor.isPresent()) + { + try + { + String setterName = this.getSetterName(key); + // @formatter:off + List methodNameList = Arrays.asList(this.getClass().getDeclaredMethods()) + .stream() + .map(m -> m.getName()) + .collect(Collectors.toList()); + // @formatter:on + if (methodNameList.contains(setterName)) + { + Method method = this.getClass().getDeclaredMethod(setterName, Object.class); + method.setAccessible(true); + method.invoke(this, value); + } + else + { + this.data.put(key, keyDescriptor.get().convert(value)); + } + } + catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) + { + throw new DataDictionaryException("Set key " + key + " failed : " + e.getClass().getSimpleName() + " -> " + e.getMessage()); + } + catch (InvocationTargetException e) + { + throw new DataDictionaryException( + "Set key " + key + " failed : " + e.getTargetException().getClass().getSimpleName() + " -> " + e.getTargetException().getMessage()); + } + + } + else + { + throw new DataDictionaryException("Key " + key + " not allowed"); + } + } + } + + @Override + public void copy(DataDictionary other) throws DataDictionaryException + { + String[] otherKeys = other.keys(); + + this.clear(); + for (int i = 0; i < otherKeys.length; i++) + { + this.set(otherKeys[i], other.get(otherKeys[i])); + } + + this.checkAndUpdate(); + } + + @Override + public final void clear() + { + this.data.clear(); + } + + @Override + public final int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.data == null) ? 0 : this.data.hashCode()); + return result; + } + + @Override + public final boolean equals(Object object) + { + if (object == null) + { + return false; + } + if (this == object) + { + return true; + } + if (!(object instanceof DataDictionary)) + { + return false; + } + DataDictionaryBase other = (DataDictionaryBase) object; + + if (this.data == null) + { + if (other.data != null) + { + return false; + } + } + else + { + if (!this.data.keySet().equals(other.data.keySet())) + { + return false; + } + for (String key : this.data.keySet()) + { + Object thisValue = this.data.get(key); + Object otherValue = other.data.get(key); + + if (thisValue == null) + { + if (otherValue != null) + { + return false; + } + else + { + continue; + } + } + else if (otherValue == null) + { + return false; + } + if (!thisValue.getClass().equals(otherValue.getClass())) + { + if ((thisValue instanceof Number) && (otherValue instanceof Number)) + { + double thisDoubleValue = ((Number) thisValue).doubleValue(); + double otherDoubleValue = ((Number) otherValue).doubleValue(); + return thisDoubleValue == otherDoubleValue; + } + else + { + return false; + } + } + if (thisValue.getClass().isArray()) + { + if (thisValue instanceof boolean[]) + { + if (!Arrays.equals((boolean[]) thisValue, (boolean[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof byte[]) + { + if (!Arrays.equals((byte[]) thisValue, (byte[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof char[]) + { + if (!Arrays.equals((char[]) thisValue, (char[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof short[]) + { + if (!Arrays.equals((short[]) thisValue, (short[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof int[]) + { + if (!Arrays.equals((int[]) thisValue, (int[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof long[]) + { + if (!Arrays.equals((long[]) thisValue, (long[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof float[]) + { + if (!Arrays.equals((float[]) thisValue, (float[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof double[]) + { + if (!Arrays.equals((double[]) thisValue, (double[]) otherValue)) + { + return false; + } + } + else if (thisValue instanceof Object[]) + { + if (!Arrays.equals((Object[]) thisValue, (Object[]) otherValue)) + { + return false; + } + } + } + else + { + if (!thisValue.equals(otherValue)) + { + return false; + } + } + } + } + + return true; + } + + @Override + public final void toFile(File file, int indentFactor) throws IOException + { + OutputStream stream = new FileOutputStream(file); + + this.toStream(stream, indentFactor); + } + + @Override + public final void toStream(OutputStream stream, int indentFactor) throws IOException + { + String text = this.toString(indentFactor); + + stream.write(text.getBytes()); + } + + @Override + public JSONObject toJSON() + { + JSONObject jsonObject = new JSONObject(); + String[] keys = this.keys(); + + for (int i = 0; i < keys.length; i++) + { + Object value = this.get(keys[i]); + if (value instanceof DataDictionaryBase) + { + DataDictionaryBase dataDictionary = (DataDictionaryBase) value; + jsonObject.put(keys[i], dataDictionary.toJSON()); + } + else if (value instanceof LocalDateTime) + { + String textValue = ((LocalDateTime) value).format(KeyDescriptorLocalDateTime.DEFAULT_FORMATTER); + jsonObject.put(keys[i], textValue); + } + else + { + jsonObject.put(keys[i], value); + } + } + + return jsonObject; + } + + @SuppressWarnings("unchecked") + @Override + public final Map toMap() + { + return (Map) this.data.clone(); + } + + @Override + public String toString() + { + return this.toString(DataDictionaryBase.JSON_INDENT_FACTOR); + } + + @Override + public String toString(int identFactor) + { + return this.toJSON().toString(identFactor); + } + + @Override + public DataDictionaryBase clone() + { + try + { + Constructor constructor = this.getClass().getConstructor(DataDictionary.class); + return this.getClass().cast(constructor.newInstance(this)); + } + catch (Exception e) + { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public void print() + { + System.out.println(this.toString(2)); + } + + protected final void checkAndUpdate() throws DataDictionaryException + { + this.updateOptionalParameters(); + this.checkMandatoryParameters(); + } + + protected void checkMandatoryParameters() throws DataDictionaryException + { + for (KeyDescriptor key : this.keyDescriptors) + { + if (key.isMandatory() && !this.exists(key.getName())) + { + throw new DataDictionaryException("Mandatory key " + key.getName() + " not defined"); + } + } + } + + protected void updateOptionalParameters() throws DataDictionaryException + { + } + + protected void addKeyDescriptor(KeyDescriptor keyDescriptor) throws DataDictionaryException + { + if (this.existsInKeys(keyDescriptor.getName())) + { + throw new DataDictionaryException("Key " + keyDescriptor.getName() + "already exists"); + } + this.keyDescriptors.add(keyDescriptor); + } + + protected void addAllKeyDescriptor(List> keyDescriptor) throws DataDictionaryException + { + for (KeyDescriptor key : keyDescriptor) + { + this.addKeyDescriptor(key); + } + } + + protected Optional> getKeyDescriptor(String name) + { + // @formatter:off + return this.keyDescriptors.stream() + .filter(k -> k.getName().equals(name)) + .findFirst(); + // @formatter:on + } + + private String getSetterName(String key) + { + return "set" + key.substring(0, 1).toUpperCase() + key.substring(1); + } + + private void init() + { + this.keyDescriptors = new ArrayList>(); + this.data = new HashMap(); + } + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java index cac21c7..6809abd 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java @@ -1,210 +1,155 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.util.Arrays; -import java.util.List; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor base - * - * @param - */ -public abstract class KeyDescriptorBase implements KeyDescriptor -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private String name; - private boolean mandatory; - protected List acceptedValues; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor setting all attributes - * - * @param name - * @param mandatory - */ - public KeyDescriptorBase(String name, boolean mandatory) - { - super(); - this.name = name; - this.mandatory = mandatory; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// KeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public final T convert(Object value) throws DataDictionaryException - { - T convertedValue = null; - - if (value == null) - { - this.handleNullValue(); - return null; - } - - convertedValue = this.convertValue(value); - - this.checkAcceptedValues(convertedValue); - - return convertedValue; - } - - @Override - public String toString(T value) - { - if (value == null) - { - return null; - } - - return value.toString(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get key name - * - * @return key name - */ - @Override - public String getName() - { - return this.name; - } - - /** - * Set key name - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Get mandatory flag - * - * @return mandatory flag - */ - @Override - public boolean isMandatory() - { - return this.mandatory; - } - - /** - * Set mandatory flag - * - * @param mandatory - */ - @Override - public void setMandatory(boolean mandatory) - { - this.mandatory = mandatory; - } - - /** - * Set mandatory value - * - * @param acceptedValues - */ - @SuppressWarnings("unchecked") - @Override - public void setAcceptedValues(T... acceptedValues) - { - this.acceptedValues = Arrays.asList(acceptedValues); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected abstract T convertValue(Object value) throws DataDictionaryException; - - protected final void handleNullValue() throws DataDictionaryException - { - if (this.isMandatory()) - { - throw new DataDictionaryException("Cannot set null " + this.getName()); - } - } - - protected void checkAcceptedValues(T value) throws DataDictionaryException - { - if (this.acceptedValues != null) - { - boolean accepted = false; - - for (T v : this.acceptedValues) - { - if (v.equals(value)) - { - accepted = true; - break; - } - } - - if (!accepted) - { - throw new DataDictionaryException("Key " + this.getName() + " doesn't respect mandatory value"); - } - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.util.Arrays; +import java.util.List; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor base + * + * @param + */ +public abstract class KeyDescriptorBase implements KeyDescriptor +{ + private String name; + private boolean mandatory; + protected List acceptedValues; + + /** + * Constructor setting all attributes + * + * @param name + * @param mandatory + */ + public KeyDescriptorBase(String name, boolean mandatory) + { + super(); + this.name = name; + this.mandatory = mandatory; + } + + @Override + public final T convert(Object value) throws DataDictionaryException + { + T convertedValue = null; + + if (value == null) + { + this.handleNullValue(); + return null; + } + + convertedValue = this.convertValue(value); + + this.checkAcceptedValues(convertedValue); + + return convertedValue; + } + + @Override + public String toString(T value) + { + if (value == null) + { + return null; + } + + return value.toString(); + } + + /** + * Get key name + * + * @return key name + */ + @Override + public String getName() + { + return this.name; + } + + /** + * Set key name + * + * @param name + */ + public void setName(String name) + { + this.name = name; + } + + /** + * Get mandatory flag + * + * @return mandatory flag + */ + @Override + public boolean isMandatory() + { + return this.mandatory; + } + + /** + * Set mandatory flag + * + * @param mandatory + */ + @Override + public void setMandatory(boolean mandatory) + { + this.mandatory = mandatory; + } + + /** + * Set mandatory value + * + * @param acceptedValues + */ + @SuppressWarnings("unchecked") + @Override + public void setAcceptedValues(T... acceptedValues) + { + this.acceptedValues = Arrays.asList(acceptedValues); + } + + protected abstract T convertValue(Object value) throws DataDictionaryException; + + protected final void handleNullValue() throws DataDictionaryException + { + if (this.isMandatory()) + { + throw new DataDictionaryException("Cannot set null " + this.getName()); + } + } + + protected void checkAcceptedValues(T value) throws DataDictionaryException + { + if (this.acceptedValues != null) + { + boolean accepted = false; + + for (T v : this.acceptedValues) + { + if (v.equals(value)) + { + accepted = true; + break; + } + } + + if (!accepted) + { + throw new DataDictionaryException("Key " + this.getName() + " doesn't respect mandatory value"); + } + } + } + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java index 397464d..f5fec56 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java @@ -1,152 +1,98 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor DataDictionary - * - * @param - */ -public class KeyDescriptorDataDictionary extends KeyDescriptorBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Class dataDictionaryClass; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param dataDictionaryClass - */ - public KeyDescriptorDataDictionary(String name, boolean mandatory, Class dataDictionaryClass) - { - super(name, mandatory); - this.dataDictionaryClass = dataDictionaryClass; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @SuppressWarnings("unchecked") - @Override - public T convertValue(Object value) throws DataDictionaryException - { - T convertedValue = null; - - if (this.dataDictionaryClass.isAssignableFrom(value.getClass())) - { - convertedValue = this.dataDictionaryClass.cast(value); - } - else if (value instanceof String) - { - convertedValue = (T) DataDictionaryBase.fromString((String) value, this.dataDictionaryClass); - } - else if (value instanceof DataDictionary) - { - try - { - Constructor constructor = this.dataDictionaryClass.getConstructor(DataDictionary.class); - convertedValue = constructor.newInstance((DataDictionary) value); - } - catch (InvocationTargetException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getTargetException().getMessage()); - } - catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getMessage()); - } - } - else if (value instanceof Map) - { - try - { - Constructor constructor = this.dataDictionaryClass.getConstructor(Map.class); - convertedValue = constructor.newInstance((Map) value); - } - catch (InvocationTargetException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getTargetException().getMessage()); - } - catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getMessage()); - } - } - else - { - throw new DataDictionaryException( - "Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + this.dataDictionaryClass.getSimpleName()); - } - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor DataDictionary + * + * @param + */ +public class KeyDescriptorDataDictionary extends KeyDescriptorBase +{ + private Class dataDictionaryClass; + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param dataDictionaryClass + */ + public KeyDescriptorDataDictionary(String name, boolean mandatory, Class dataDictionaryClass) + { + super(name, mandatory); + this.dataDictionaryClass = dataDictionaryClass; + } + + @SuppressWarnings("unchecked") + @Override + public T convertValue(Object value) throws DataDictionaryException + { + T convertedValue = null; + + if (this.dataDictionaryClass.isAssignableFrom(value.getClass())) + { + convertedValue = this.dataDictionaryClass.cast(value); + } + else if (value instanceof String) + { + convertedValue = (T) DataDictionaryBase.fromString((String) value, this.dataDictionaryClass); + } + else if (value instanceof DataDictionary) + { + try + { + Constructor constructor = this.dataDictionaryClass.getConstructor(DataDictionary.class); + convertedValue = constructor.newInstance((DataDictionary) value); + } + catch (InvocationTargetException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + + this.dataDictionaryClass.getSimpleName() + " : " + e.getTargetException().getMessage()); + } + catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + + this.dataDictionaryClass.getSimpleName() + " : " + e.getMessage()); + } + } + else if (value instanceof Map) + { + try + { + Constructor constructor = this.dataDictionaryClass.getConstructor(Map.class); + convertedValue = constructor.newInstance((Map) value); + } + catch (InvocationTargetException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + + this.dataDictionaryClass.getSimpleName() + " : " + e.getTargetException().getMessage()); + } + catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + + this.dataDictionaryClass.getSimpleName() + " : " + e.getMessage()); + } + } + else + { + throw new DataDictionaryException( + "Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + this.dataDictionaryClass.getSimpleName()); + } + + return convertedValue; + } + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java index aeffc8f..9e523f3 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java @@ -1,142 +1,87 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor Enum - * - * @param - */ -@SuppressWarnings("rawtypes") -public class KeyDescriptorEnum extends KeyDescriptorBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Class enumClass; - private String prefix; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param enumClass - */ - public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass) - { - this(name, mandatory, enumClass, ""); - } - - /** - * Constructor setting all parameters - * - * @param name - * @param mandatory - * @param enumClass - * @param prefix - */ - public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass, String prefix) - { - super(name, mandatory); - this.enumClass = enumClass; - this.prefix = prefix; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public T convertValue(Object value) throws DataDictionaryException - { - T convertedValue = null; - - if (this.enumClass.isAssignableFrom(value.getClass())) - { - convertedValue = this.enumClass.cast(value); - } - else if (value instanceof String) - { - convertedValue = this.toEnum((String) value); - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + this.enumClass.getSimpleName()); - } - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @SuppressWarnings("unchecked") - protected T toEnum(String value) throws DataDictionaryException - { - T convertedValue = null; - try - { - String preparedValue = this.prefix + value.toUpperCase().replace(".", "_").replace("-", "_"); - convertedValue = (T) Enum.valueOf(this.enumClass, preparedValue); - } - catch (IllegalArgumentException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": " + this.enumClass.getSimpleName() + " doesn't contain " + value); - } - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor Enum + * + * @param + */ +@SuppressWarnings("rawtypes") +public class KeyDescriptorEnum extends KeyDescriptorBase +{ + private Class enumClass; + private String prefix; + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param enumClass + */ + public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass) + { + this(name, mandatory, enumClass, ""); + } + + /** + * Constructor setting all parameters + * + * @param name + * @param mandatory + * @param enumClass + * @param prefix + */ + public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass, String prefix) + { + super(name, mandatory); + this.enumClass = enumClass; + this.prefix = prefix; + } + + @Override + public T convertValue(Object value) throws DataDictionaryException + { + T convertedValue = null; + + if (this.enumClass.isAssignableFrom(value.getClass())) + { + convertedValue = this.enumClass.cast(value); + } + else if (value instanceof String) + { + convertedValue = this.toEnum((String) value); + } + else + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + this.enumClass.getSimpleName()); + } + + return convertedValue; + } + + @SuppressWarnings("unchecked") + protected T toEnum(String value) throws DataDictionaryException + { + T convertedValue = null; + try + { + String preparedValue = this.prefix + value.toUpperCase().replace(".", "_").replace("-", "_"); + convertedValue = (T) Enum.valueOf(this.enumClass, preparedValue); + } + catch (IllegalArgumentException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": " + this.enumClass.getSimpleName() + " doesn't contain " + value); + } + return convertedValue; + } + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java index 464997d..758a03a 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java @@ -1,233 +1,178 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.json.JSONObject; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor List - * - * @param - */ -public class KeyDescriptorList extends KeyDescriptorBase> -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Class itemClass; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param itemClass - */ - public KeyDescriptorList(String name, boolean mandatory, Class itemClass) - { - super(name, mandatory); - this.itemClass = itemClass; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// KeyDescriptor> - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public List convertValue(Object value) throws DataDictionaryException - { - List convertedValue = new ArrayList(); - - if (this.itemClass.isAssignableFrom(value.getClass())) - { - T convertedItem = this.convertItem(value); - convertedValue.add(convertedItem); - } - else if (value instanceof HashMap) - { - T convertedItem = this.convertItem(value); - convertedValue.add(convertedItem); - } - else if (value instanceof List) - { - for (Object item : (List) value) - { - T convertedItem = this.convertItem(item); - convertedValue.add(convertedItem); - } - } - else if (value instanceof Object[]) - { - for (Object item : (Object[]) value) - { - T convertedItem = this.convertItem(item); - convertedValue.add(convertedItem); - } - } - else - { - throw new DataDictionaryException( - "Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to List<" + this.itemClass.getSimpleName() + ">"); - } - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private T convertItem(Object item) throws DataDictionaryException - { - T convertedItem = null; - if (this.itemClass.isAssignableFrom(item.getClass())) - { - convertedItem = this.itemClass.cast(item); - } - else if (DataDictionary.class.isAssignableFrom(this.itemClass)) - { - convertedItem = this.convertDataDictionaryItem(item); - } - else if (Enum.class.isAssignableFrom(this.itemClass)) - { - convertedItem = this.convertEnumItem(item); - } - else - { - throw new DataDictionaryException( - "Key " + this.getName() + ": at least on item isn't a " + this.itemClass.getSimpleName() + ", type received :" + item.getClass().getSimpleName()); - } - return convertedItem; - } - - private T convertDataDictionaryItem(Object item) throws DataDictionaryException - { - T convertedItem = null; - if (item instanceof JSONObject) - { - JSONObject itemJsonObject = (JSONObject) item; - try - { - Constructor constructor = this.itemClass.getConstructor(Map.class); - convertedItem = (T) constructor.newInstance(itemJsonObject.toMap()); - } - catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) - { - e.printStackTrace(); - } - catch (InvocationTargetException e) - { - e.printStackTrace(); - } - } - else if (item instanceof Map) - { - try - { - Constructor constructor = this.itemClass.getConstructor(Map.class); - convertedItem = (T) constructor.newInstance((Map) item); - } - catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) - { - e.printStackTrace(); - } - catch (InvocationTargetException e) - { - e.printStackTrace(); - } - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": at least on item isn't a JSONObject, type received :" + item.getClass().getSimpleName()); - } - return convertedItem; - } - - @SuppressWarnings("unchecked") - private T convertEnumItem(Object item) throws DataDictionaryException - { - T convertedItem = null; - if (item instanceof String) - { - String itemString = (String) item; - try - { - Method valueOf = this.itemClass.getMethod("valueOf", String.class); - convertedItem = (T) valueOf.invoke(null, itemString.toUpperCase()); - } - catch (SecurityException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) - { - e.printStackTrace(); - } - catch (InvocationTargetException e) - { - e.printStackTrace(); - } - - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": at least on item isn't a JSONObject, type received :" + item.getClass().getSimpleName()); - } - return convertedItem; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.json.JSONObject; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor List + * + * @param + */ +public class KeyDescriptorList extends KeyDescriptorBase> +{ + private Class itemClass; + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param itemClass + */ + public KeyDescriptorList(String name, boolean mandatory, Class itemClass) + { + super(name, mandatory); + this.itemClass = itemClass; + } + + @Override + public List convertValue(Object value) throws DataDictionaryException + { + List convertedValue = new ArrayList(); + + if (this.itemClass.isAssignableFrom(value.getClass())) + { + T convertedItem = this.convertItem(value); + convertedValue.add(convertedItem); + } + else if (value instanceof HashMap) + { + T convertedItem = this.convertItem(value); + convertedValue.add(convertedItem); + } + else if (value instanceof List) + { + for (Object item : (List) value) + { + T convertedItem = this.convertItem(item); + convertedValue.add(convertedItem); + } + } + else if (value instanceof Object[]) + { + for (Object item : (Object[]) value) + { + T convertedItem = this.convertItem(item); + convertedValue.add(convertedItem); + } + } + else + { + throw new DataDictionaryException( + "Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to List<" + this.itemClass.getSimpleName() + ">"); + } + return convertedValue; + } + + private T convertItem(Object item) throws DataDictionaryException + { + T convertedItem = null; + if (this.itemClass.isAssignableFrom(item.getClass())) + { + convertedItem = this.itemClass.cast(item); + } + else if (DataDictionary.class.isAssignableFrom(this.itemClass)) + { + convertedItem = this.convertDataDictionaryItem(item); + } + else if (Enum.class.isAssignableFrom(this.itemClass)) + { + convertedItem = this.convertEnumItem(item); + } + else + { + throw new DataDictionaryException( + "Key " + this.getName() + ": at least on item isn't a " + this.itemClass.getSimpleName() + ", type received :" + item.getClass().getSimpleName()); + } + return convertedItem; + } + + private T convertDataDictionaryItem(Object item) throws DataDictionaryException + { + T convertedItem = null; + if (item instanceof JSONObject) + { + JSONObject itemJsonObject = (JSONObject) item; + try + { + Constructor constructor = this.itemClass.getConstructor(Map.class); + convertedItem = (T) constructor.newInstance(itemJsonObject.toMap()); + } + catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) + { + e.printStackTrace(); + } + catch (InvocationTargetException e) + { + e.printStackTrace(); + } + } + else if (item instanceof Map) + { + try + { + Constructor constructor = this.itemClass.getConstructor(Map.class); + convertedItem = (T) constructor.newInstance((Map) item); + } + catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) + { + e.printStackTrace(); + } + catch (InvocationTargetException e) + { + e.printStackTrace(); + } + } + else + { + throw new DataDictionaryException("Key " + this.getName() + ": at least on item isn't a JSONObject, type received :" + item.getClass().getSimpleName()); + } + return convertedItem; + } + + @SuppressWarnings("unchecked") + private T convertEnumItem(Object item) throws DataDictionaryException + { + T convertedItem = null; + if (item instanceof String) + { + String itemString = (String) item; + try + { + Method valueOf = this.itemClass.getMethod("valueOf", String.class); + convertedItem = (T) valueOf.invoke(null, itemString.toUpperCase()); + } + catch (SecurityException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) + { + e.printStackTrace(); + } + catch (InvocationTargetException e) + { + e.printStackTrace(); + } + + } + else + { + throw new DataDictionaryException("Key " + this.getName() + ": at least on item isn't a JSONObject, type received :" + item.getClass().getSimpleName()); + } + return convertedItem; + } +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java index 92b47e7..b8b3695 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java @@ -1,170 +1,115 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.util.List; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.MinMaxChecker; - -/** - * DataDictionary key descriptor Number min max - * - * @param - */ -public class KeyDescriptorListMinMaxSize extends KeyDescriptorList -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private MinMaxChecker minMaxChecker; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param itemClass - */ - public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass) - { - this(name, mandatory, itemClass, null, null); - } - - /** - * Constructor setting all attributes - * - * @param name - * @param mandatory - * @param itemClass - * @param min - * @param max - */ - public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass, Integer min, Integer max) - { - super(name, mandatory, itemClass); - this.minMaxChecker = new MinMaxChecker(min, max); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public List convertValue(Object value) throws DataDictionaryException - { - List convertedValue = super.convertValue(value); - - this.check(convertedValue); - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get min - * - * @return min - */ - public Integer getMin() - { - return this.minMaxChecker.getMin().intValue(); - } - - /** - * Set min - * - * @param min - * @throws IllegalArgumentException - * if min is greater than max - */ - public void setMin(Integer min) - { - this.minMaxChecker.setMin(min); - } - - /** - * Get max - * - * @return max - */ - public Integer getMax() - { - return this.minMaxChecker.getMax().intValue(); - } - - /** - * Set max - * - * @param max - * @throws IllegalArgumentException - * if max is smaller than min - */ - public void setMax(Integer max) - { - this.minMaxChecker.setMax(max); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void check(List list) throws DataDictionaryException - { - if (list != null) - { - if (!this.minMaxChecker.check(list.size())) - { - throw new DataDictionaryException("Key " + this.getName() + ": value size (" + list.size() + ") out of bound"); - } - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.util.List; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.MinMaxChecker; + +/** + * DataDictionary key descriptor Number min max + * + * @param + */ +public class KeyDescriptorListMinMaxSize extends KeyDescriptorList +{ + private MinMaxChecker minMaxChecker; + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param itemClass + */ + public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass) + { + this(name, mandatory, itemClass, null, null); + } + + /** + * Constructor setting all attributes + * + * @param name + * @param mandatory + * @param itemClass + * @param min + * @param max + */ + public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass, Integer min, Integer max) + { + super(name, mandatory, itemClass); + this.minMaxChecker = new MinMaxChecker(min, max); + } + + @Override + public List convertValue(Object value) throws DataDictionaryException + { + List convertedValue = super.convertValue(value); + + this.check(convertedValue); + + return convertedValue; + } + + /** + * Get min + * + * @return min + */ + public Integer getMin() + { + return this.minMaxChecker.getMin().intValue(); + } + + /** + * Set min + * + * @param min + * @throws IllegalArgumentException + * if min is greater than max + */ + public void setMin(Integer min) + { + this.minMaxChecker.setMin(min); + } + + /** + * Get max + * + * @return max + */ + public Integer getMax() + { + return this.minMaxChecker.getMax().intValue(); + } + + /** + * Set max + * + * @param max + * @throws IllegalArgumentException + * if max is smaller than min + */ + public void setMax(Integer max) + { + this.minMaxChecker.setMax(max); + } + + private void check(List list) throws DataDictionaryException + { + if (list != null) + { + if (!this.minMaxChecker.check(list.size())) + { + throw new DataDictionaryException("Key " + this.getName() + ": value size (" + list.size() + ") out of bound"); + } + } + } +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java index 7f868dd..e083324 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java @@ -1,160 +1,98 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor LocalDateTime - */ -public class KeyDescriptorLocalDateTime extends KeyDescriptorBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Default pattern used */ - public static final String DEFAULT_PATTERN = "dd/MM/yyyy HH:mm:ss"; - /** Default formatter used */ - public static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_PATTERN); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private DateTimeFormatter formatter; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorLocalDateTime(String name, boolean mandatory) - { - this(name, mandatory, DEFAULT_PATTERN); - } - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param formatterPattern - */ - public KeyDescriptorLocalDateTime(String name, boolean mandatory, String formatterPattern) - { - super(name, mandatory); - this.formatter = DateTimeFormatter.ofPattern(formatterPattern); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// KeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public String toString(LocalDateTime value) - { - if (value == null) - { - return null; - } - - return this.formatter.format(value); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// KeyDescriptorBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public LocalDateTime convertValue(Object value) throws DataDictionaryException - { - LocalDateTime convertedValue = null; - - if (value instanceof LocalDateTime) - { - convertedValue = (LocalDateTime) value; - } - else if (value instanceof String) - { - convertedValue = this.toLocalDateTime((String) value); - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to LocalDateTime"); - } - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private LocalDateTime toLocalDateTime(String value) throws DataDictionaryException - { - LocalDateTime convertedValue = null; - try - { - convertedValue = LocalDateTime.parse(value, this.formatter); - } - catch (DateTimeParseException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": string " + value + " cannot be converted to LocalDateTime"); - } - return convertedValue; - } - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor LocalDateTime + */ +public class KeyDescriptorLocalDateTime extends KeyDescriptorBase +{ + /** Default pattern used */ + public static final String DEFAULT_PATTERN = "dd/MM/yyyy HH:mm:ss"; + /** Default formatter used */ + public static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_PATTERN); + + private DateTimeFormatter formatter; + + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorLocalDateTime(String name, boolean mandatory) + { + this(name, mandatory, DEFAULT_PATTERN); + } + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param formatterPattern + */ + public KeyDescriptorLocalDateTime(String name, boolean mandatory, String formatterPattern) + { + super(name, mandatory); + this.formatter = DateTimeFormatter.ofPattern(formatterPattern); + } + + @Override + public String toString(LocalDateTime value) + { + if (value == null) + { + return null; + } + + return this.formatter.format(value); + } + + @Override + public LocalDateTime convertValue(Object value) throws DataDictionaryException + { + LocalDateTime convertedValue = null; + + if (value instanceof LocalDateTime) + { + convertedValue = (LocalDateTime) value; + } + else if (value instanceof String) + { + convertedValue = this.toLocalDateTime((String) value); + } + else + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to LocalDateTime"); + } + + return convertedValue; + } + + private LocalDateTime toLocalDateTime(String value) throws DataDictionaryException + { + LocalDateTime convertedValue = null; + try + { + convertedValue = LocalDateTime.parse(value, this.formatter); + } + catch (DateTimeParseException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": string " + value + " cannot be converted to LocalDateTime"); + } + return convertedValue; + } + +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java index c455a86..24e44e4 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java @@ -1,139 +1,84 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor Number - */ -public class KeyDescriptorNumber extends KeyDescriptorBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorNumber(String name, boolean mandatory) - { - super(name, mandatory); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Number convertValue(Object value) throws DataDictionaryException - { - Number convertedValue = null; - - if (value instanceof Number) - { - convertedValue = (Number) value; - } - else if (value instanceof String) - { - convertedValue = this.toNumber((String) value); - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to Number"); - } - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void checkAcceptedValues(Number value) throws DataDictionaryException - { - if (this.acceptedValues != null) - { - boolean accepted = false; - - for(Number v : this.acceptedValues) - { - if(v.doubleValue() == value.doubleValue()) - { - accepted = true; - break; - } - } - - if(!accepted) - { - throw new DataDictionaryException("Key " + this.getName() + " doesn't respect mandatory value"); - } - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Number toNumber(String value) throws DataDictionaryException - { - Number out = null; - try - { - out = Double.valueOf((String) value); - } - catch (NumberFormatException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to Number"); - } - return out; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor Number + */ +public class KeyDescriptorNumber extends KeyDescriptorBase +{ + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorNumber(String name, boolean mandatory) + { + super(name, mandatory); + } + + @Override + public Number convertValue(Object value) throws DataDictionaryException + { + Number convertedValue = null; + + if (value instanceof Number) + { + convertedValue = (Number) value; + } + else if (value instanceof String) + { + convertedValue = this.toNumber((String) value); + } + else + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to Number"); + } + + return convertedValue; + } + + protected void checkAcceptedValues(Number value) throws DataDictionaryException + { + if (this.acceptedValues != null) + { + boolean accepted = false; + + for(Number v : this.acceptedValues) + { + if(v.doubleValue() == value.doubleValue()) + { + accepted = true; + break; + } + } + + if(!accepted) + { + throw new DataDictionaryException("Key " + this.getName() + " doesn't respect mandatory value"); + } + } + } + + private Number toNumber(String value) throws DataDictionaryException + { + Number out = null; + try + { + out = Double.valueOf((String) value); + } + catch (NumberFormatException e) + { + throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to Number"); + } + return out; + } +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java index ce295f3..041efc0 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java @@ -1,164 +1,109 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.MinMaxChecker; - -/** - * DataDictionary key descriptor Number min max - */ -public class KeyDescriptorNumberMinMax extends KeyDescriptorNumber -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private MinMaxChecker minMaxChecker; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorNumberMinMax(String name, boolean mandatory) - { - this(name, mandatory, null, null); - } - - /** - * Constructor setting all attributes - * - * @param name - * @param mandatory - * @param min - * @param max - */ - public KeyDescriptorNumberMinMax(String name, boolean mandatory, Number min, Number max) - { - super(name, mandatory); - this.minMaxChecker = new MinMaxChecker(min, max); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Number convertValue(Object value) throws DataDictionaryException - { - Number convertedValue = super.convertValue(value); - - this.check(convertedValue); - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get min - * - * @return min - */ - public Number getMin() - { - return this.minMaxChecker.getMin(); - } - - /** - * Set min - * - * @param min - * @throws IllegalArgumentException - * if min is greater than max - */ - public void setMin(Number min) - { - this.minMaxChecker.setMin(min); - } - - /** - * Get max - * - * @return max - */ - public Number getMax() - { - return this.minMaxChecker.getMax(); - } - - /** - * Set max - * - * @param max - * @throws IllegalArgumentException - * if max is smaller than min - */ - public void setMax(Number max) - { - this.minMaxChecker.setMax(max); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void check(Number convertedValue) throws DataDictionaryException - { - if (convertedValue != null) - { - if (!this.minMaxChecker.check(convertedValue)) - { - throw new DataDictionaryException("Key " + this.getName() + ": value (" + convertedValue + ") out of bound"); - } - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.MinMaxChecker; + +/** + * DataDictionary key descriptor Number min max + */ +public class KeyDescriptorNumberMinMax extends KeyDescriptorNumber +{ + private MinMaxChecker minMaxChecker; + + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorNumberMinMax(String name, boolean mandatory) + { + this(name, mandatory, null, null); + } + + /** + * Constructor setting all attributes + * + * @param name + * @param mandatory + * @param min + * @param max + */ + public KeyDescriptorNumberMinMax(String name, boolean mandatory, Number min, Number max) + { + super(name, mandatory); + this.minMaxChecker = new MinMaxChecker(min, max); + } + + @Override + public Number convertValue(Object value) throws DataDictionaryException + { + Number convertedValue = super.convertValue(value); + + this.check(convertedValue); + + return convertedValue; + } + + /** + * Get min + * + * @return min + */ + public Number getMin() + { + return this.minMaxChecker.getMin(); + } + + /** + * Set min + * + * @param min + * @throws IllegalArgumentException + * if min is greater than max + */ + public void setMin(Number min) + { + this.minMaxChecker.setMin(min); + } + + /** + * Get max + * + * @return max + */ + public Number getMax() + { + return this.minMaxChecker.getMax(); + } + + /** + * Set max + * + * @param max + * @throws IllegalArgumentException + * if max is smaller than min + */ + public void setMax(Number max) + { + this.minMaxChecker.setMax(max); + } + + private void check(Number convertedValue) throws DataDictionaryException + { + if (convertedValue != null) + { + if (!this.minMaxChecker.check(convertedValue)) + { + throw new DataDictionaryException("Key " + this.getName() + ": value (" + convertedValue + ") out of bound"); + } + } + } +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java index a7ca434..0de5b9f 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java @@ -1,140 +1,85 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor String - */ -public class KeyDescriptorString extends KeyDescriptorBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final boolean DEFAULT_EMPTY_ALLOW_FLAG = true; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private boolean emptyAllow; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorString(String name, boolean mandatory) - { - this(name, mandatory, DEFAULT_EMPTY_ALLOW_FLAG); - } - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param emptyAllow - */ - public KeyDescriptorString(String name, boolean mandatory, boolean emptyAllow) - { - super(name, mandatory); - this.emptyAllow = emptyAllow; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public String convertValue(Object value) throws DataDictionaryException - { - String convertedValue = null; - - convertedValue = value.toString(); - - this.check(convertedValue); - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get empty allow flag - * - * @return empty allow flag - */ - public boolean isEmptyAllow() - { - return this.emptyAllow; - } - - /** - * Set empty allow flag - * - * @param emptyAllow - */ - public void setEmptyAllow(boolean emptyAllow) - { - this.emptyAllow = emptyAllow; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void check(String value) throws DataDictionaryException - { - if (!this.emptyAllow && value.trim().isEmpty()) - { - throw new DataDictionaryException("Key " + this.getName() + ": value can't be empty"); - } - } - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import enedis.lab.types.DataDictionaryException; + +/** + * DataDictionary key descriptor String + */ +public class KeyDescriptorString extends KeyDescriptorBase +{ + private static final boolean DEFAULT_EMPTY_ALLOW_FLAG = true; + + private boolean emptyAllow; + + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorString(String name, boolean mandatory) + { + this(name, mandatory, DEFAULT_EMPTY_ALLOW_FLAG); + } + + /** + * Default constructor + * + * @param name + * @param mandatory + * @param emptyAllow + */ + public KeyDescriptorString(String name, boolean mandatory, boolean emptyAllow) + { + super(name, mandatory); + this.emptyAllow = emptyAllow; + } + + @Override + public String convertValue(Object value) throws DataDictionaryException + { + String convertedValue = null; + + convertedValue = value.toString(); + + this.check(convertedValue); + + return convertedValue; + } + + /** + * Get empty allow flag + * + * @return empty allow flag + */ + public boolean isEmptyAllow() + { + return this.emptyAllow; + } + + /** + * Set empty allow flag + * + * @param emptyAllow + */ + public void setEmptyAllow(boolean emptyAllow) + { + this.emptyAllow = emptyAllow; + } + + private void check(String value) throws DataDictionaryException + { + if (!this.emptyAllow && value.trim().isEmpty()) + { + throw new DataDictionaryException("Key " + this.getName() + ": value can't be empty"); + } + } + +} diff --git a/src/main/java/enedis/lab/util/MinMaxChecker.java b/src/main/java/enedis/lab/util/MinMaxChecker.java index 12ad83c..6354bcd 100644 --- a/src/main/java/enedis/lab/util/MinMaxChecker.java +++ b/src/main/java/enedis/lab/util/MinMaxChecker.java @@ -1,185 +1,130 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util; - -/** - * Min max class - */ -public class MinMaxChecker -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Number min; - private Number max; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MinMaxChecker() - { - super(); - } - - /** - * Constructor setting parameters - * - * @param min - * @param max - */ - public MinMaxChecker(Number min, Number max) - { - this(); - this.setMin(min); - this.setMax(max); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get min - * - * @return min - */ - public Number getMin() - { - return this.min; - } - - /** - * Set min - * - * @param min - * @throws IllegalArgumentException - * if min is greater than max - */ - public void setMin(Number min) - { - if (min != null) - { - if (this.max != null && min.doubleValue() > this.max.doubleValue()) - { - throw new IllegalArgumentException("min can't be greater than max"); - } - } - this.min = min; - } - - /** - * Get max - * - * @return max - */ - public Number getMax() - { - return this.max; - } - - /** - * Set max - * - * @param max - * @throws IllegalArgumentException - * if max is smaller than min - */ - public void setMax(Number max) - { - if (max != null) - { - if (this.min != null && max.doubleValue() < this.min.doubleValue()) - { - throw new IllegalArgumentException("max can't be smaller than min"); - } - } - this.max = max; - } - - /** - * Check the given value - * - * @param value - * @return true if the value is in [min, max] - */ - public boolean check(Number value) - { - if (value != null) - { - if (this.min != null) - { - if (value.doubleValue() < this.min.doubleValue()) - { - return false; - } - } - if (this.max != null) - { - if (value.doubleValue() > this.max.doubleValue()) - { - return false; - } - } - return true; - } - else - { - return false; - } - - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util; + +/** + * Min max class + */ +public class MinMaxChecker +{ + private Number min; + private Number max; + + /** + * Default constructor + */ + public MinMaxChecker() + { + super(); + } + + /** + * Constructor setting parameters + * + * @param min + * @param max + */ + public MinMaxChecker(Number min, Number max) + { + this(); + this.setMin(min); + this.setMax(max); + } + + /** + * Get min + * + * @return min + */ + public Number getMin() + { + return this.min; + } + + /** + * Set min + * + * @param min + * @throws IllegalArgumentException + * if min is greater than max + */ + public void setMin(Number min) + { + if (min != null) + { + if (this.max != null && min.doubleValue() > this.max.doubleValue()) + { + throw new IllegalArgumentException("min can't be greater than max"); + } + } + this.min = min; + } + + /** + * Get max + * + * @return max + */ + public Number getMax() + { + return this.max; + } + + /** + * Set max + * + * @param max + * @throws IllegalArgumentException + * if max is smaller than min + */ + public void setMax(Number max) + { + if (max != null) + { + if (this.min != null && max.doubleValue() < this.min.doubleValue()) + { + throw new IllegalArgumentException("max can't be smaller than min"); + } + } + this.max = max; + } + + /** + * Check the given value + * + * @param value + * @return true if the value is in [min, max] + */ + public boolean check(Number value) + { + if (value != null) + { + if (this.min != null) + { + if (value.doubleValue() < this.min.doubleValue()) + { + return false; + } + } + if (this.max != null) + { + if (value.doubleValue() > this.max.doubleValue()) + { + return false; + } + } + return true; + } + else + { + return false; + } + + } + +} diff --git a/src/main/java/enedis/lab/util/SystemError.java b/src/main/java/enedis/lab/util/SystemError.java index 90b48c1..d348446 100644 --- a/src/main/java/enedis/lab/util/SystemError.java +++ b/src/main/java/enedis/lab/util/SystemError.java @@ -1,120 +1,65 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util; - -import org.apache.commons.lang3.SystemUtils; - -import com.sun.jna.Native; -import com.sun.jna.platform.win32.Kernel32Util; - -/** - * Class for system error - * - */ -public class SystemError -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get system last error code - * - * @return System last error code - */ - static public int getCode() - { - return Native.getLastError(); - } - - /** - * Get system last error message - * - * @return System last error message - */ - static public String getMessage() - { - return getMessage(getCode()); - } - - /** - * Get system error message associated with code - * - * @param code - * the system error code - * - * @return System error message - */ - static public String getMessage(int code) - { - String message = null; - - if (SystemUtils.IS_OS_WINDOWS) - { - message = Kernel32Util.formatMessage(code); - } - else if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_UNIX || SystemUtils.IS_OS_MAC_OSX) - { - message = SystemLibC.INSTANCE.strerror(code); - } - - return message; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util; + +import org.apache.commons.lang3.SystemUtils; + +import com.sun.jna.Native; +import com.sun.jna.platform.win32.Kernel32Util; + +/** + * Class for system error + * + */ +public class SystemError +{ + /** + * Get system last error code + * + * @return System last error code + */ + static public int getCode() + { + return Native.getLastError(); + } + + /** + * Get system last error message + * + * @return System last error message + */ + static public String getMessage() + { + return getMessage(getCode()); + } + + /** + * Get system error message associated with code + * + * @param code + * the system error code + * + * @return System error message + */ + static public String getMessage(int code) + { + String message = null; + + if (SystemUtils.IS_OS_WINDOWS) + { + message = Kernel32Util.formatMessage(code); + } + else if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_UNIX || SystemUtils.IS_OS_MAC_OSX) + { + message = SystemLibC.INSTANCE.strerror(code); + } + + return message; + } + +} diff --git a/src/main/java/enedis/lab/util/message/Event.java b/src/main/java/enedis/lab/util/message/Event.java index 1b23b0a..6d07741 100644 --- a/src/main/java/enedis/lab/util/message/Event.java +++ b/src/main/java/enedis/lab/util/message/Event.java @@ -1,189 +1,134 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; - -/** - * Event class - * - * Generated - */ -public abstract class Event extends Message -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATE_TIME = "dateTime"; - - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.EVENT; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorLocalDateTime kDateTime; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Event() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Event(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Event(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param dateTime - * @throws DataDictionaryException - */ - public Event(String name, LocalDateTime dateTime) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setDateTime(dateTime); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Message - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get date time - * - * @return the date time - */ - public LocalDateTime getDateTime() - { - return (LocalDateTime) this.data.get(KEY_DATE_TIME); - } - - /** - * Set date time - * - * @param dateTime - * @throws DataDictionaryException - */ - public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException - { - this.setDateTime((Object) dateTime); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setDateTime(Object dateTime) throws DataDictionaryException - { - this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); - this.keys.add(this.kDateTime); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; + +/** + * Event class + * + * Generated + */ +public abstract class Event extends Message +{ + protected static final String KEY_DATE_TIME = "dateTime"; + + private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.EVENT; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorLocalDateTime kDateTime; + + protected Event() + { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Event(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Event(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param dateTime + * @throws DataDictionaryException + */ + public Event(String name, LocalDateTime dateTime) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setDateTime(dateTime); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_TYPE)) + { + this.setType(TYPE_ACCEPTED_VALUE); + } + super.updateOptionalParameters(); + } + + /** + * Get date time + * + * @return the date time + */ + public LocalDateTime getDateTime() + { + return (LocalDateTime) this.data.get(KEY_DATE_TIME); + } + + /** + * Set date time + * + * @param dateTime + * @throws DataDictionaryException + */ + public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException + { + this.setDateTime((Object) dateTime); + } + + protected void setDateTime(Object dateTime) throws DataDictionaryException + { + this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); + } + + private void loadKeyDescriptors() + { + try + { + this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); + this.keys.add(this.kDateTime); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/util/message/Message.java b/src/main/java/enedis/lab/util/message/Message.java index efec77a..74ebb9e 100644 --- a/src/main/java/enedis/lab/util/message/Message.java +++ b/src/main/java/enedis/lab/util/message/Message.java @@ -1,216 +1,161 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * Message class - * - * Generated - */ -public class Message extends DataDictionaryBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Key type */ - public static final String KEY_TYPE = "type"; - /** Key name */ - public static final String KEY_NAME = "name"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kType; - protected KeyDescriptorString kName; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Message() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Message(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Message(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @throws DataDictionaryException - */ - public Message(MessageType type, String name) throws DataDictionaryException - { - this(); - - this.setType(type); - this.setName(name); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get type - * - * @return the type - */ - public MessageType getType() - { - return (MessageType) this.data.get(KEY_TYPE); - } - - /** - * Get name - * - * @return the name - */ - public String getName() - { - return (String) this.data.get(KEY_NAME); - } - - /** - * Set type - * - * @param type - * @throws DataDictionaryException - */ - public void setType(MessageType type) throws DataDictionaryException - { - this.setType((Object) type); - } - - /** - * Set name - * - * @param name - * @throws DataDictionaryException - */ - public void setName(String name) throws DataDictionaryException - { - this.setName((Object) name); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setType(Object type) throws DataDictionaryException - { - this.data.put(KEY_TYPE, this.kType.convert(type)); - } - - protected void setName(Object name) throws DataDictionaryException - { - this.data.put(KEY_NAME, this.kName.convert(name)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kType = new KeyDescriptorEnum(KEY_TYPE, true, MessageType.class); - this.keys.add(this.kType); - - this.kName = new KeyDescriptorString(KEY_NAME, true, false); - this.keys.add(this.kName); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * Message class + * + * Generated + */ +public class Message extends DataDictionaryBase +{ + /** Key type */ + public static final String KEY_TYPE = "type"; + /** Key name */ + public static final String KEY_NAME = "name"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kType; + protected KeyDescriptorString kName; + + protected Message() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Message(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Message(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param type + * @param name + * @throws DataDictionaryException + */ + public Message(MessageType type, String name) throws DataDictionaryException + { + this(); + + this.setType(type); + this.setName(name); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + super.updateOptionalParameters(); + } + + /** + * Get type + * + * @return the type + */ + public MessageType getType() + { + return (MessageType) this.data.get(KEY_TYPE); + } + + /** + * Get name + * + * @return the name + */ + public String getName() + { + return (String) this.data.get(KEY_NAME); + } + + /** + * Set type + * + * @param type + * @throws DataDictionaryException + */ + public void setType(MessageType type) throws DataDictionaryException + { + this.setType((Object) type); + } + + /** + * Set name + * + * @param name + * @throws DataDictionaryException + */ + public void setName(String name) throws DataDictionaryException + { + this.setName((Object) name); + } + + protected void setType(Object type) throws DataDictionaryException + { + this.data.put(KEY_TYPE, this.kType.convert(type)); + } + + protected void setName(Object name) throws DataDictionaryException + { + this.data.put(KEY_NAME, this.kName.convert(name)); + } + + private void loadKeyDescriptors() + { + try + { + this.kType = new KeyDescriptorEnum(KEY_TYPE, true, MessageType.class); + this.keys.add(this.kType); + + this.kName = new KeyDescriptorString(KEY_NAME, true, false); + this.keys.add(this.kName); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/util/message/Request.java b/src/main/java/enedis/lab/util/message/Request.java index 7ad542b..b27b774 100644 --- a/src/main/java/enedis/lab/util/message/Request.java +++ b/src/main/java/enedis/lab/util/message/Request.java @@ -1,155 +1,100 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; - -/** - * Request class - * - * Generated - */ -public abstract class Request extends Message -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.REQUEST; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Request() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Request(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Request(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @throws DataDictionaryException - */ - public Request(MessageType type, String name) throws DataDictionaryException - { - this(); - - this.setType(type); - this.setName(name); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Message - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; + +/** + * Request class + * + * Generated + */ +public abstract class Request extends Message +{ + private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.REQUEST; + + private List> keys = new ArrayList>(); + + protected Request() + { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Request(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Request(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param type + * @param name + * @throws DataDictionaryException + */ + public Request(MessageType type, String name) throws DataDictionaryException + { + this(); + + this.setType(type); + this.setName(name); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_TYPE)) + { + this.setType(TYPE_ACCEPTED_VALUE); + } + super.updateOptionalParameters(); + } + + private void loadKeyDescriptors() + { + try + { + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/util/message/Response.java b/src/main/java/enedis/lab/util/message/Response.java index 772c4b0..67e4ec8 100644 --- a/src/main/java/enedis/lab/util/message/Response.java +++ b/src/main/java/enedis/lab/util/message/Response.java @@ -1,259 +1,204 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * Response class - * - * Generated - */ -public abstract class Response extends Message -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATE_TIME = "dateTime"; - protected static final String KEY_ERROR_CODE = "errorCode"; - protected static final String KEY_ERROR_MESSAGE = "errorMessage"; - - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.RESPONSE; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorLocalDateTime kDateTime; - protected KeyDescriptorNumber kErrorCode; - protected KeyDescriptorString kErrorMessage; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Response() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Response(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Response(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public Response(MessageType type, String name, LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(); - - this.setType(type); - this.setName(name); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Message - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get date time - * - * @return the date time - */ - public LocalDateTime getDateTime() - { - return (LocalDateTime) this.data.get(KEY_DATE_TIME); - } - - /** - * Get error code - * - * @return the error code - */ - public Number getErrorCode() - { - return (Number) this.data.get(KEY_ERROR_CODE); - } - - /** - * Get error message - * - * @return the error message - */ - public String getErrorMessage() - { - return (String) this.data.get(KEY_ERROR_MESSAGE); - } - - /** - * Set date time - * - * @param dateTime - * @throws DataDictionaryException - */ - public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException - { - this.setDateTime((Object) dateTime); - } - - /** - * Set error code - * - * @param errorCode - * @throws DataDictionaryException - */ - public void setErrorCode(Number errorCode) throws DataDictionaryException - { - this.setErrorCode((Object) errorCode); - } - - /** - * Set error message - * - * @param errorMessage - * @throws DataDictionaryException - */ - public void setErrorMessage(String errorMessage) throws DataDictionaryException - { - this.setErrorMessage((Object) errorMessage); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setDateTime(Object dateTime) throws DataDictionaryException - { - this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); - } - - protected void setErrorCode(Object errorCode) throws DataDictionaryException - { - this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); - } - - protected void setErrorMessage(Object errorMessage) throws DataDictionaryException - { - this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); - this.keys.add(this.kDateTime); - - this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); - this.keys.add(this.kErrorCode); - - this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, false, true); - this.keys.add(this.kErrorMessage); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; +import enedis.lab.types.datadictionary.KeyDescriptorNumber; +import enedis.lab.types.datadictionary.KeyDescriptorString; + +/** + * Response class + * + * Generated + */ +public abstract class Response extends Message +{ + protected static final String KEY_DATE_TIME = "dateTime"; + protected static final String KEY_ERROR_CODE = "errorCode"; + protected static final String KEY_ERROR_MESSAGE = "errorMessage"; + + private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.RESPONSE; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorLocalDateTime kDateTime; + protected KeyDescriptorNumber kErrorCode; + protected KeyDescriptorString kErrorMessage; + + protected Response() + { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Response(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Response(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param type + * @param name + * @param dateTime + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public Response(MessageType type, String name, LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException + { + this(); + + this.setType(type); + this.setName(name); + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_TYPE)) + { + this.setType(TYPE_ACCEPTED_VALUE); + } + super.updateOptionalParameters(); + } + + /** + * Get date time + * + * @return the date time + */ + public LocalDateTime getDateTime() + { + return (LocalDateTime) this.data.get(KEY_DATE_TIME); + } + + /** + * Get error code + * + * @return the error code + */ + public Number getErrorCode() + { + return (Number) this.data.get(KEY_ERROR_CODE); + } + + /** + * Get error message + * + * @return the error message + */ + public String getErrorMessage() + { + return (String) this.data.get(KEY_ERROR_MESSAGE); + } + + /** + * Set date time + * + * @param dateTime + * @throws DataDictionaryException + */ + public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException + { + this.setDateTime((Object) dateTime); + } + + /** + * Set error code + * + * @param errorCode + * @throws DataDictionaryException + */ + public void setErrorCode(Number errorCode) throws DataDictionaryException + { + this.setErrorCode((Object) errorCode); + } + + /** + * Set error message + * + * @param errorMessage + * @throws DataDictionaryException + */ + public void setErrorMessage(String errorMessage) throws DataDictionaryException + { + this.setErrorMessage((Object) errorMessage); + } + + protected void setDateTime(Object dateTime) throws DataDictionaryException + { + this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); + } + + protected void setErrorCode(Object errorCode) throws DataDictionaryException + { + this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); + } + + protected void setErrorMessage(Object errorMessage) throws DataDictionaryException + { + this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); + } + + private void loadKeyDescriptors() + { + try + { + this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); + this.keys.add(this.kDateTime); + + this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); + this.keys.add(this.kErrorCode); + + this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, false, true); + this.keys.add(this.kErrorMessage); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/util/message/ResponseBase.java b/src/main/java/enedis/lab/util/message/ResponseBase.java index bd44949..cd253e9 100644 --- a/src/main/java/enedis/lab/util/message/ResponseBase.java +++ b/src/main/java/enedis/lab/util/message/ResponseBase.java @@ -1,189 +1,134 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; - -/** - * ResponseBase class - * - * Generated - */ -public class ResponseBase extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseBase() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseBase(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseBase(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseBase(String name, LocalDateTime dateTime, Number errorCode, String errorMessage, DataDictionaryBase data) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - public DataDictionaryBase getData() - { - return (DataDictionaryBase) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(DataDictionaryBase data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, DataDictionaryBase.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; + +/** + * ResponseBase class + * + * Generated + */ +public class ResponseBase extends Response +{ + protected static final String KEY_DATA = "data"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + protected ResponseBase() + { + super(); + this.loadKeyDescriptors(); + + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseBase(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseBase(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseBase(String name, LocalDateTime dateTime, Number errorCode, String errorMessage, DataDictionaryBase data) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + public DataDictionaryBase getData() + { + return (DataDictionaryBase) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(DataDictionaryBase data) throws DataDictionaryException + { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, DataDictionaryBase.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageException.java b/src/main/java/enedis/lab/util/message/exception/MessageException.java index 217fe8e..985289f 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageException.java @@ -1,112 +1,57 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Message exception - */ -public class MessageException extends Exception -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Message exception + */ +public class MessageException extends Exception +{ + + private static final long serialVersionUID = -2263755971102386572L; + + /** + * Default constructor + */ + public MessageException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageException(Throwable cause) + { + super(cause); + } + +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java index 27847b0..a8e2d62 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java @@ -1,112 +1,57 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class MessageInvalidContentException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageInvalidContentException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageInvalidContentException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageInvalidContentException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageInvalidContentException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class MessageInvalidContentException extends MessageException +{ + + private static final long serialVersionUID = -2263755971102386572L; + + /** + * Default constructor + */ + public MessageInvalidContentException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageInvalidContentException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageInvalidContentException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageInvalidContentException(Throwable cause) + { + super(cause); + } + +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java index c1e04fb..1e17c57 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java @@ -1,112 +1,57 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class MessageInvalidFormatException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageInvalidFormatException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageInvalidFormatException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageInvalidFormatException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageInvalidFormatException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class MessageInvalidFormatException extends MessageException +{ + + private static final long serialVersionUID = -2263755971102386572L; + + /** + * Default constructor + */ + public MessageInvalidFormatException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageInvalidFormatException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageInvalidFormatException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageInvalidFormatException(Throwable cause) + { + super(cause); + } + +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java index 0eee2df..a351317 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java @@ -1,112 +1,57 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class MessageInvalidTypeException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageInvalidTypeException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageInvalidTypeException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageInvalidTypeException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageInvalidTypeException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class MessageInvalidTypeException extends MessageException +{ + + private static final long serialVersionUID = -2263755971102386572L; + + /** + * Default constructor + */ + public MessageInvalidTypeException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageInvalidTypeException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageInvalidTypeException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageInvalidTypeException(Throwable cause) + { + super(cause); + } + +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java index 326eb13..faed525 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java @@ -1,112 +1,57 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class MessageKeyNameDoesntExistException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageKeyNameDoesntExistException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageKeyNameDoesntExistException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageKeyNameDoesntExistException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageKeyNameDoesntExistException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class MessageKeyNameDoesntExistException extends MessageException +{ + + private static final long serialVersionUID = -2263755971102386572L; + + /** + * Default constructor + */ + public MessageKeyNameDoesntExistException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageKeyNameDoesntExistException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageKeyNameDoesntExistException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageKeyNameDoesntExistException(Throwable cause) + { + super(cause); + } + +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java index 747843b..ea030a7 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java @@ -1,112 +1,57 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class MessageKeyTypeDoesntExistException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageKeyTypeDoesntExistException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageKeyTypeDoesntExistException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageKeyTypeDoesntExistException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageKeyTypeDoesntExistException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class MessageKeyTypeDoesntExistException extends MessageException +{ + + private static final long serialVersionUID = -2263755971102386572L; + + /** + * Default constructor + */ + public MessageKeyTypeDoesntExistException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageKeyTypeDoesntExistException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageKeyTypeDoesntExistException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageKeyTypeDoesntExistException(Throwable cause) + { + super(cause); + } + +} diff --git a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java index e1f0277..62cb26f 100644 --- a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java +++ b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java @@ -1,112 +1,57 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class UnsupportedMessageException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public UnsupportedMessageException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public UnsupportedMessageException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public UnsupportedMessageException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public UnsupportedMessageException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.exception; + +/** + * Unvalid message format exception + */ +public class UnsupportedMessageException extends MessageException +{ + + private static final long serialVersionUID = -2263755971102386572L; + + /** + * Default constructor + */ + public UnsupportedMessageException() + { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public UnsupportedMessageException(String message, Throwable cause) + { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public UnsupportedMessageException(String message) + { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public UnsupportedMessageException(Throwable cause) + { + super(cause); + } + +} diff --git a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java index 9ecbf5b..f041ece 100644 --- a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java @@ -1,153 +1,98 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import java.util.HashMap; -import java.util.Map; - -import org.json.JSONException; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.util.message.Message; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.UnsupportedMessageException; - -/** - * Message factory - * - * @param - */ -public class AbstractMessageFactory -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Class clazz; - private Map> messageClasses; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param clazz - */ - public AbstractMessageFactory(Class clazz) - { - this.clazz = clazz; - this.messageClasses = new HashMap>(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get message from text - * - * @param text - * @param name - * @return message - * @throws UnsupportedMessageException - * @throws MessageInvalidFormatException - * @throws MessageInvalidContentException - */ - public final T getMessage(String text, String name) throws UnsupportedMessageException, MessageInvalidFormatException, MessageInvalidContentException - { - T message = null; - - try - { - Class messageClazz = this.messageClasses.get(name); - - if (messageClazz != null) - { - message = messageClazz.cast(DataDictionaryBase.fromString(text, messageClazz)); - } - else - { - throw new UnsupportedMessageException("Unsupported " + this.clazz.getSimpleName() + " : " + name); - } - } - catch (JSONException e) - { - throw new MessageInvalidFormatException("Invalid " + this.clazz.getSimpleName() + " " + name + " format, it should be JSON : " + e.getMessage(), e); - } - catch (DataDictionaryException e) - { - throw new MessageInvalidContentException("Invalid " + this.clazz.getSimpleName() + " " + name + " content : " + e.getMessage(), e); - } - - return message; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Add a Message class to decode message with the given name - * - * @param name - * @param messageClazz - */ - public final void addMessageClass(String name, Class messageClazz) - { - if (name == null || messageClazz == null) - { - throw new IllegalArgumentException("Name and " + this.clazz.getSimpleName() + " class can't be null"); - } - - this.messageClasses.put(name, messageClazz); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import java.util.HashMap; +import java.util.Map; + +import org.json.JSONException; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.util.message.Message; +import enedis.lab.util.message.exception.MessageInvalidContentException; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.UnsupportedMessageException; + +/** + * Message factory + * + * @param + */ +public class AbstractMessageFactory +{ + private Class clazz; + private Map> messageClasses; + + /** + * Default constructor + * + * @param clazz + */ + public AbstractMessageFactory(Class clazz) + { + this.clazz = clazz; + this.messageClasses = new HashMap>(); + } + + /** + * Get message from text + * + * @param text + * @param name + * @return message + * @throws UnsupportedMessageException + * @throws MessageInvalidFormatException + * @throws MessageInvalidContentException + */ + public final T getMessage(String text, String name) throws UnsupportedMessageException, MessageInvalidFormatException, MessageInvalidContentException + { + T message = null; + + try + { + Class messageClazz = this.messageClasses.get(name); + + if (messageClazz != null) + { + message = messageClazz.cast(DataDictionaryBase.fromString(text, messageClazz)); + } + else + { + throw new UnsupportedMessageException("Unsupported " + this.clazz.getSimpleName() + " : " + name); + } + } + catch (JSONException e) + { + throw new MessageInvalidFormatException("Invalid " + this.clazz.getSimpleName() + " " + name + " format, it should be JSON : " + e.getMessage(), e); + } + catch (DataDictionaryException e) + { + throw new MessageInvalidContentException("Invalid " + this.clazz.getSimpleName() + " " + name + " content : " + e.getMessage(), e); + } + + return message; + } + + /** + * Add a Message class to decode message with the given name + * + * @param name + * @param messageClazz + */ + public final void addMessageClass(String name, Class messageClazz) + { + if (name == null || messageClazz == null) + { + throw new IllegalArgumentException("Name and " + this.clazz.getSimpleName() + " class can't be null"); + } + + this.messageClasses.put(name, messageClazz); + } + } \ No newline at end of file diff --git a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java index daa4df0..1e6f606 100644 --- a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java @@ -1,177 +1,122 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Message; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; - -/** - * Message factory - */ -public class MessageFactory -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private RequestFactory requestFactory; - private ResponseFactory responseFactory; - private EventFactory eventFactory; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageFactory() - { - super(); - } - - /** - * Constructor using field - * - * @param requestFactory - * @param responseFactory - * @param eventFactory - */ - public MessageFactory(RequestFactory requestFactory, ResponseFactory responseFactory, EventFactory eventFactory) - { - super(); - this.requestFactory = requestFactory; - this.responseFactory = responseFactory; - this.eventFactory = eventFactory; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get message from String - * - * @param text - * @return message - * @throws MessageInvalidTypeException - * @throws MessageKeyNameDoesntExistException - * @throws MessageKeyTypeDoesntExistException - * @throws MessageInvalidFormatException - * @throws MessageInvalidContentException - * @throws UnsupportedMessageException - */ - public Message getMessage(String text) throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - this.checkSubFactoryReferences(); - - Message genericMessage = BasicMessageFactory.getMessage(text); - Message message = null; - - // @formatter:off - switch (genericMessage.getType()) - { - case REQUEST: message = this.requestFactory.getMessage(text, genericMessage.getName()); break; - case RESPONSE: message = this.responseFactory.getMessage(text, genericMessage.getName()); break; - case EVENT: message = this.eventFactory.getMessage(text, genericMessage.getName()); break; - default: - break; - } - // @formatter:on - - return message; - } - - /** - * Set request factory - * - * @param requestFactory - */ - public void setRequestFactory(RequestFactory requestFactory) - { - this.requestFactory = requestFactory; - } - - /** - * Set response factory - * - * @param responseFactory - */ - public void setResponseFactory(ResponseFactory responseFactory) - { - this.responseFactory = responseFactory; - } - - /** - * Set event factory - * - * @param eventFactory - */ - public void setEventFactory(EventFactory eventFactory) - { - this.eventFactory = eventFactory; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void checkSubFactoryReferences() - { - if (this.requestFactory == null || this.responseFactory == null || this.eventFactory == null) - { - throw new IllegalStateException("requestFactory, responseFactory and eventFactory have to be set"); - } - } - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import enedis.lab.util.message.Message; +import enedis.lab.util.message.exception.MessageInvalidContentException; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.MessageInvalidTypeException; +import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; +import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; +import enedis.lab.util.message.exception.UnsupportedMessageException; + +/** + * Message factory + */ +public class MessageFactory +{ + private RequestFactory requestFactory; + private ResponseFactory responseFactory; + private EventFactory eventFactory; + + /** + * Default constructor + */ + public MessageFactory() + { + super(); + } + + /** + * Constructor using field + * + * @param requestFactory + * @param responseFactory + * @param eventFactory + */ + public MessageFactory(RequestFactory requestFactory, ResponseFactory responseFactory, EventFactory eventFactory) + { + super(); + this.requestFactory = requestFactory; + this.responseFactory = responseFactory; + this.eventFactory = eventFactory; + } + + /** + * Get message from String + * + * @param text + * @return message + * @throws MessageInvalidTypeException + * @throws MessageKeyNameDoesntExistException + * @throws MessageKeyTypeDoesntExistException + * @throws MessageInvalidFormatException + * @throws MessageInvalidContentException + * @throws UnsupportedMessageException + */ + public Message getMessage(String text) throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, + MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException + { + this.checkSubFactoryReferences(); + + Message genericMessage = BasicMessageFactory.getMessage(text); + Message message = null; + + // @formatter:off + switch (genericMessage.getType()) + { + case REQUEST: message = this.requestFactory.getMessage(text, genericMessage.getName()); break; + case RESPONSE: message = this.responseFactory.getMessage(text, genericMessage.getName()); break; + case EVENT: message = this.eventFactory.getMessage(text, genericMessage.getName()); break; + default: + break; + } + // @formatter:on + + return message; + } + + /** + * Set request factory + * + * @param requestFactory + */ + public void setRequestFactory(RequestFactory requestFactory) + { + this.requestFactory = requestFactory; + } + + /** + * Set response factory + * + * @param responseFactory + */ + public void setResponseFactory(ResponseFactory responseFactory) + { + this.responseFactory = responseFactory; + } + + /** + * Set event factory + * + * @param eventFactory + */ + public void setEventFactory(EventFactory eventFactory) + { + this.eventFactory = eventFactory; + } + + private void checkSubFactoryReferences() + { + if (this.requestFactory == null || this.responseFactory == null || this.eventFactory == null) + { + throw new IllegalStateException("requestFactory, responseFactory and eventFactory have to be set"); + } + } + +} diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java index 5b772d7..0bda613 100644 --- a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java +++ b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java @@ -26,41 +26,11 @@ */ public class FilteredNotifierBase implements FilteredNotifier { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected Map> subscribersFiltered; private Lock subscribersFilteredLock = new ReentrantLock(); protected Collection subscribersUnfiltered; private Lock subscribersUnfilteredLock = new ReentrantLock(); - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Default constructor */ @@ -71,13 +41,6 @@ public FilteredNotifierBase() this.subscribersUnfiltered = new CopyOnWriteArraySet(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Listenable - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public void subscribe(T listener) { @@ -281,22 +244,4 @@ public Collection getFilters(T listener) return filters; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - } \ No newline at end of file diff --git a/src/main/java/enedis/lab/util/task/NotifierBase.java b/src/main/java/enedis/lab/util/task/NotifierBase.java index d0a9979..a1e95ae 100644 --- a/src/main/java/enedis/lab/util/task/NotifierBase.java +++ b/src/main/java/enedis/lab/util/task/NotifierBase.java @@ -19,38 +19,8 @@ */ public class NotifierBase implements Notifier { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected Set subscribers; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Default constructor */ @@ -60,13 +30,6 @@ public NotifierBase() this.subscribers = new CopyOnWriteArraySet(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Listenable - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public void subscribe(T listener) { @@ -97,22 +60,4 @@ public Collection getSubscribers() return this.subscribers; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - } \ No newline at end of file diff --git a/src/main/java/enedis/lab/util/task/TaskBase.java b/src/main/java/enedis/lab/util/task/TaskBase.java index 15b94d8..4a96367 100644 --- a/src/main/java/enedis/lab/util/task/TaskBase.java +++ b/src/main/java/enedis/lab/util/task/TaskBase.java @@ -1,205 +1,151 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.concurrent.atomic.AtomicBoolean; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Task basic implementation - */ -public abstract class TaskBase implements Task, Runnable -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private AtomicBoolean stopRequired; - private Thread task; - protected static Logger logger = LogManager.getLogger(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TaskBase() - { - super(); - this.stopRequired = new AtomicBoolean(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Task - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void start() - { - if (this.task == null || !this.task.isAlive()) - { - this.stopRequired.set(false); - this.task = new Thread(this); - this.task.start(); - } - } - - @Override - public void stop() - { - try - { - if (this.task != null && this.task.isAlive()) - { - this.stopRequired.set(true); - if (this.task != Thread.currentThread()) - { - this.task.join(); - } - } - } - catch (InterruptedException exception) - { - logger.error("Stop task interrupted", exception); - } - } - - @Override - public final boolean isRunning() - { - return this.task == null ? false : this.task.isAlive(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void run() - { - this.runOnStart(); - this.runProcess(); - this.runOnTerminate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected final boolean isStopRequired() - { - return this.stopRequired.get(); - } - - protected final void runOnStart() - { - try - { - this.onStart(); - } - catch (Exception exception) - { - logger.error("Task on start aborted", exception); - this.runOnError(exception); - } - } - - protected final void runProcess() - { - try - { - this.process(); - } - catch (Exception exception) - { - logger.error("Task process aborted", exception); - this.runOnError(exception); - } - } - - protected final void runOnTerminate() - { - try - { - this.onTerminate(); - } - catch (Exception exception) - { - logger.error("Task on terminate aborted", exception); - this.runOnError(exception); - } - } - - protected final void runOnError(Exception exception) - { - try - { - this.onError(exception); - } - catch (Exception onErrorException) - { - logger.error("Task on error aborted", exception); - } - } - - /** - * Task core process method - */ - protected abstract void process(); - - protected void onStart() - { - } - - protected void onTerminate() - { - } - - protected void onError(Exception exception) - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Task basic implementation + */ +public abstract class TaskBase implements Task, Runnable +{ + private AtomicBoolean stopRequired; + private Thread task; + protected static Logger logger = LogManager.getLogger(); + + /** + * Default constructor + */ + public TaskBase() + { + super(); + this.stopRequired = new AtomicBoolean(); + } + + @Override + public void start() + { + if (this.task == null || !this.task.isAlive()) + { + this.stopRequired.set(false); + this.task = new Thread(this); + this.task.start(); + } + } + + @Override + public void stop() + { + try + { + if (this.task != null && this.task.isAlive()) + { + this.stopRequired.set(true); + if (this.task != Thread.currentThread()) + { + this.task.join(); + } + } + } + catch (InterruptedException exception) + { + logger.error("Stop task interrupted", exception); + } + } + + @Override + public final boolean isRunning() + { + return this.task == null ? false : this.task.isAlive(); + } + + @Override + public void run() + { + this.runOnStart(); + this.runProcess(); + this.runOnTerminate(); + } + + protected final boolean isStopRequired() + { + return this.stopRequired.get(); + } + + protected final void runOnStart() + { + try + { + this.onStart(); + } + catch (Exception exception) + { + logger.error("Task on start aborted", exception); + this.runOnError(exception); + } + } + + protected final void runProcess() + { + try + { + this.process(); + } + catch (Exception exception) + { + logger.error("Task process aborted", exception); + this.runOnError(exception); + } + } + + protected final void runOnTerminate() + { + try + { + this.onTerminate(); + } + catch (Exception exception) + { + logger.error("Task on terminate aborted", exception); + this.runOnError(exception); + } + } + + protected final void runOnError(Exception exception) + { + try + { + this.onError(exception); + } + catch (Exception onErrorException) + { + logger.error("Task on error aborted", exception); + } + } + + /** + * Task core process method + */ + protected abstract void process(); + + protected void onStart() + { + } + + protected void onTerminate() + { + } + + protected void onError(Exception exception) + { + } + +} diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodic.java b/src/main/java/enedis/lab/util/task/TaskPeriodic.java index 84f279d..e404755 100644 --- a/src/main/java/enedis/lab/util/task/TaskPeriodic.java +++ b/src/main/java/enedis/lab/util/task/TaskPeriodic.java @@ -1,151 +1,96 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.concurrent.atomic.AtomicLong; - -import enedis.lab.util.time.Time; - -/** - * Periodic task with configurable period - */ -public abstract class TaskPeriodic extends TaskBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default period (in milliseconds) used to execute process - */ - public static final long DEFAULT_PERIOD = 1000; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private AtomicLong period = new AtomicLong(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor with default parameter - * - * @see #DEFAULT_PERIOD - */ - public TaskPeriodic() - { - super(); - this.setPeriod(DEFAULT_PERIOD); - } - - /** - * Constructor with polling period - * - * @param period - * the period (in milliseconds) used to execute process - */ - public TaskPeriodic(long period) - { - super(); - this.setPeriod(period); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Runnable - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void run() - { - this.runOnStart(); - while (!this.isStopRequired()) - { - this.runProcess(); - if (this.isStopRequired()) - { - break; - } - this.waitPeriod(); - } - this.runOnTerminate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get polling period - * - * @return The period (in milliseconds) used to execute process - */ - public long getPeriod() - { - return this.period.get(); - } - - /** - * Set the polling period - * - * @param period - * the period (in milliseconds) used to execute process - * @throws IllegalArgumentException - * if period <= 0 - */ - public void setPeriod(long period) - { - if (period <= 0) - { - throw new IllegalArgumentException("Cannot set period " + period + " : must be > 0"); - } - this.period.set(period); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void waitPeriod() - { - Time.sleep(this.getPeriod()); - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.concurrent.atomic.AtomicLong; + +import enedis.lab.util.time.Time; + +/** + * Periodic task with configurable period + */ +public abstract class TaskPeriodic extends TaskBase +{ + /** + * Default period (in milliseconds) used to execute process + */ + public static final long DEFAULT_PERIOD = 1000; + + private AtomicLong period = new AtomicLong(); + + /** + * Constructor with default parameter + * + * @see #DEFAULT_PERIOD + */ + public TaskPeriodic() + { + super(); + this.setPeriod(DEFAULT_PERIOD); + } + + /** + * Constructor with polling period + * + * @param period + * the period (in milliseconds) used to execute process + */ + public TaskPeriodic(long period) + { + super(); + this.setPeriod(period); + } + + @Override + public void run() + { + this.runOnStart(); + while (!this.isStopRequired()) + { + this.runProcess(); + if (this.isStopRequired()) + { + break; + } + this.waitPeriod(); + } + this.runOnTerminate(); + } + + /** + * Get polling period + * + * @return The period (in milliseconds) used to execute process + */ + public long getPeriod() + { + return this.period.get(); + } + + /** + * Set the polling period + * + * @param period + * the period (in milliseconds) used to execute process + * @throws IllegalArgumentException + * if period <= 0 + */ + public void setPeriod(long period) + { + if (period <= 0) + { + throw new IllegalArgumentException("Cannot set period " + period + " : must be > 0"); + } + this.period.set(period); + } + + private void waitPeriod() + { + Time.sleep(this.getPeriod()); + } +} diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java index bf34fb8..048c14d 100644 --- a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java +++ b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java @@ -1,122 +1,67 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.Collection; - -/** - * Periodic task with subscribers basic implementation - * - * @param - * the subscriber type - */ -public abstract class TaskPeriodicWithSubscribers extends TaskPeriodic implements Notifier -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Notifier notifier; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TaskPeriodicWithSubscribers() - { - super(); - this.notifier = new NotifierBase(); - } - - /** - * Constructor setting period - * - * @param period - * the period in milliseconds - */ - public TaskPeriodicWithSubscribers(long period) - { - super(period); - this.notifier = new NotifierBase(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void subscribe(T listener) - { - this.notifier.subscribe(listener); - } - - @Override - public void unsubscribe(T listener) - { - this.notifier.unsubscribe(listener); - } - - @Override - public boolean hasSubscriber(T listener) - { - return this.notifier.hasSubscriber(listener); - } - - @Override - public Collection getSubscribers() - { - return this.notifier.getSubscribers(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.Collection; + +/** + * Periodic task with subscribers basic implementation + * + * @param + * the subscriber type + */ +public abstract class TaskPeriodicWithSubscribers extends TaskPeriodic implements Notifier +{ + protected Notifier notifier; + + /** + * Default constructor + */ + public TaskPeriodicWithSubscribers() + { + super(); + this.notifier = new NotifierBase(); + } + + /** + * Constructor setting period + * + * @param period + * the period in milliseconds + */ + public TaskPeriodicWithSubscribers(long period) + { + super(period); + this.notifier = new NotifierBase(); + } + + @Override + public void subscribe(T listener) + { + this.notifier.subscribe(listener); + } + + @Override + public void unsubscribe(T listener) + { + this.notifier.unsubscribe(listener); + } + + @Override + public boolean hasSubscriber(T listener) + { + return this.notifier.hasSubscriber(listener); + } + + @Override + public Collection getSubscribers() + { + return this.notifier.getSubscribers(); + } + +} diff --git a/src/main/java/enedis/lab/util/time/Time.java b/src/main/java/enedis/lab/util/time/Time.java index 25999f0..134a26a 100644 --- a/src/main/java/enedis/lab/util/time/Time.java +++ b/src/main/java/enedis/lab/util/time/Time.java @@ -1,163 +1,108 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.time; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; - -/** - * Time implementation - */ -public abstract class Time -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Millisecond */ - public final static int MILLISECOND = 1; - /** Second */ - public final static int SECOND = 1000 * MILLISECOND; - /** Minute */ - public final static int MINUTE = 60 * SECOND; - /** Hour */ - public final static int HOUR = 60 * MINUTE; - /** Day */ - public final static int DAY = 24 * HOUR; - - /* Default format for displaying the date/time */ - private static final String DFLT_DATETIME_FORMAT = "yyyy/MM/dd HH:mm:ss.SSS"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Wait the given duration in millisecond - * - * @param duration - */ - public static void sleep(long duration) - { - if (duration <= 0L) - { - return; - } - try - { - Thread.sleep(duration); - } - catch (InterruptedException e) - { - e.printStackTrace(); - } - } - - /** - * Convert date/time from long number (since an origin) to a string in the default format - * - * @param timestamp - * a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) - * @return the time/date string in the default format - */ - public static String timestampToDateTimeStr(long timestamp) - { - return timestampToDateTimeStr(timestamp, DFLT_DATETIME_FORMAT); - } - - /** - * Convert date/time from long number to a string in the specified format - * - * @param timestamp - * a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) - * @param dateTimeFormatStr - * the format specified - * @return the time/date string in the specified format - */ - public static String timestampToDateTimeStr(long timestamp, String dateTimeFormatStr) - { - DateFormat dateFormat = new SimpleDateFormat(dateTimeFormatStr); - Date date = new Date(timestamp); - return dateFormat.format(date); - } - - /** - * Timestamp a message with the current DateTime - * - * @param msg - * @return the message decorated with current DateTime - */ - public static String timestamp(String msg) - { - return timestamp(msg, DFLT_DATETIME_FORMAT); - } - - /** - * Timestamp a message with the current DateTime - * - * @param msg - * @param dateTimeFormat - * @return the message decorated with current DateTime - */ - public static String timestamp(String msg, String dateTimeFormat) - { - DateFormat dateFormat = new SimpleDateFormat(dateTimeFormat); - Date date = new Date(); - String text = dateFormat.format(date) + " : " + msg; - return text; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.time; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Time implementation + */ +public abstract class Time +{ + /** Millisecond */ + public final static int MILLISECOND = 1; + /** Second */ + public final static int SECOND = 1000 * MILLISECOND; + /** Minute */ + public final static int MINUTE = 60 * SECOND; + /** Hour */ + public final static int HOUR = 60 * MINUTE; + /** Day */ + public final static int DAY = 24 * HOUR; + + /* Default format for displaying the date/time */ + private static final String DFLT_DATETIME_FORMAT = "yyyy/MM/dd HH:mm:ss.SSS"; + + /** + * Wait the given duration in millisecond + * + * @param duration + */ + public static void sleep(long duration) + { + if (duration <= 0L) + { + return; + } + try + { + Thread.sleep(duration); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + + /** + * Convert date/time from long number (since an origin) to a string in the default format + * + * @param timestamp + * a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) + * @return the time/date string in the default format + */ + public static String timestampToDateTimeStr(long timestamp) + { + return timestampToDateTimeStr(timestamp, DFLT_DATETIME_FORMAT); + } + + /** + * Convert date/time from long number to a string in the specified format + * + * @param timestamp + * a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) + * @param dateTimeFormatStr + * the format specified + * @return the time/date string in the specified format + */ + public static String timestampToDateTimeStr(long timestamp, String dateTimeFormatStr) + { + DateFormat dateFormat = new SimpleDateFormat(dateTimeFormatStr); + Date date = new Date(timestamp); + return dateFormat.format(date); + } + + /** + * Timestamp a message with the current DateTime + * + * @param msg + * @return the message decorated with current DateTime + */ + public static String timestamp(String msg) + { + return timestamp(msg, DFLT_DATETIME_FORMAT); + } + + /** + * Timestamp a message with the current DateTime + * + * @param msg + * @param dateTimeFormat + * @return the message decorated with current DateTime + */ + public static String timestamp(String msg, String dateTimeFormat) + { + DateFormat dateFormat = new SimpleDateFormat(dateTimeFormat); + Date date = new Date(); + String text = dateFormat.format(date) + " : " + msg; + return text; + } + +} diff --git a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java index 1cbce3d..1db1314 100644 --- a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java +++ b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java @@ -1,104 +1,49 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -public class ReadNextFrameSubscriber implements TICCoreSubscriber -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICCoreFrame frame; - private TICCoreError error; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public ReadNextFrameSubscriber() - { - super(); - this.clear(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onData(TICCoreFrame frame) - { - this.frame = frame; - } - - @Override - public void onError(TICCoreError error) - { - this.error = error; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreFrame getData() - { - return this.frame; - } - - public TICCoreError getError() - { - return this.error; - } - - public void clear() - { - this.frame = null; - this.error = null; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +public class ReadNextFrameSubscriber implements TICCoreSubscriber +{ + private TICCoreFrame frame; + private TICCoreError error; + + public ReadNextFrameSubscriber() + { + super(); + this.clear(); + } + + @Override + public void onData(TICCoreFrame frame) + { + this.frame = frame; + } + + @Override + public void onError(TICCoreError error) + { + this.error = error; + } + + public TICCoreFrame getData() + { + return this.frame; + } + + public TICCoreError getError() + { + return this.error; + } + + public void clear() + { + this.frame = null; + this.error = null; + } + +} diff --git a/src/main/java/enedis/tic/core/TICCoreBase.java b/src/main/java/enedis/tic/core/TICCoreBase.java index 206c705..82ff323 100644 --- a/src/main/java/enedis/tic/core/TICCoreBase.java +++ b/src/main/java/enedis/tic/core/TICCoreBase.java @@ -1,581 +1,505 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.function.Predicate; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.io.PlugSubscriber; -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.io.tic.TICPortFinder; -import enedis.lab.io.tic.TICPortFinderBase; -import enedis.lab.io.tic.TICPortPlugNotifier; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.time.Time; -import enedis.lab.util.task.FilteredNotifier; -import enedis.lab.util.task.FilteredNotifierBase; -import enedis.lab.util.task.Task; -import enedis.lab.util.task.TaskBase; - -/** - * TICCore implementation - */ -public class TICCoreBase implements TICCore, Task, TICCoreSubscriber, PlugSubscriber -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final int PLUG_NOTIFIER_POLLING_PERIOD = 100; - private static final int READ_NEXT_FRAME_TIMEOUT = 30000; - private static final int READ_NEXT_FRAME_POLLING_PERIOD = 100; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICPortFinder portFinder; - private TICPortPlugNotifier plugNotifier; - private long plugNotifierPeriod; - private Constructor streamConstructor; - private TICMode streamMode; - private List nativePortNamesOnStart; - private Collection streamList; - private FilteredNotifier eventNotifier; - private static Logger logger = LogManager.getLogger(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreBase() - { - this(TICMode.AUTO, null); - } - - public TICCoreBase(TICMode streamMode, List nativePortNamesStart) - { - this(TICPortFinderBase.getInstance(), PLUG_NOTIFIER_POLLING_PERIOD, TICCoreStreamBase.class, streamMode, nativePortNamesStart); - } - - public TICCoreBase(TICPortFinder ticPortFinder, long plugNotifierPeriod, Class streamClass, TICMode streamMode, List nativePortNamesOnStart) - { - super(); - this.portFinder = ticPortFinder; - this.plugNotifierPeriod = plugNotifierPeriod; - try - { - this.streamConstructor = streamClass.getConstructor(String.class, String.class, TICMode.class); - } - catch (NoSuchMethodException e) - { - throw new IllegalArgumentException("Class " + streamClass.getName() + " has no valid constructor : " + e.getMessage()); - } - if (streamMode == null) - { - throw new IllegalArgumentException("TICMode should be defined"); - } - this.streamMode = streamMode; - this.nativePortNamesOnStart = nativePortNamesOnStart; - this.streamList = Collections.synchronizedSet(new HashSet()); - this.eventNotifier = new FilteredNotifierBase(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCore - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public List getAvailableTICs() - { - List identifiers = new DataArrayList(); - - for (TICCoreStream stream : this.streamList) - { - identifiers.add(stream.getIdentifier()); - } - - return identifiers; - } - - @Override - public List getModemsInfo() - { - List descriptors = new DataArrayList(); - - for (TICCoreStream stream : this.streamList) - { - TICPortDescriptor descriptor = this.portFinder.findByPortName(stream.getIdentifier().getPortName()); - descriptors.add(descriptor); - } - - return descriptors; - } - - @Override - public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException - { - return this.readNextFrame(identifier, READ_NEXT_FRAME_TIMEOUT); - } - - @Override - public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException - { - TICCoreFrame frame = null; - TICPortDescriptor descriptor = null; - TICCoreStream stream = this.findStream(identifier); - - if (stream == null) - { - descriptor = this.portFinder.findNative(identifier.getPortName()); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - stream = this.startNewStream(descriptor); - } - ReadNextFrameSubscriber subscriber = new ReadNextFrameSubscriber(); - stream.subscribe(subscriber); - - long begin = System.nanoTime(); - long elapsed = 0; - while (elapsed < timeout) - { - if (subscriber.getError() != null) - { - TICCoreException exception = new TICCoreException(subscriber.getError().getErrorCode().intValue(), subscriber.getError().getErrorMessage()); - logger.error(exception.getMessage(), exception); - stream.unsubscribe(subscriber); - this.closeNativeStream(identifier, stream, subscriber); - throw exception; - } - if (subscriber.getData() != null) - { - frame = subscriber.getData(); - break; - } - Time.sleep(READ_NEXT_FRAME_POLLING_PERIOD); - elapsed = (System.nanoTime() - begin) / 1000000; - } - stream.unsubscribe(subscriber); - this.closeNativeStream(identifier, stream, subscriber); - if (elapsed >= timeout) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), "Stream " + identifier + " data read timeout !"); - logger.error(exception.getMessage(), exception); - throw exception; - } - return frame; - } - - @Override - public void subscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException - { - TICCoreStream stream = this.findStream(identifier); - - if (stream == null) - { - TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - this.startNewStream(descriptor); - } - try - { - this.eventNotifier.subscribe(identifier, subscriber); - } - catch (Exception e) - { - logger.error(e.getMessage(), e); - } - } - - @Override - public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException - { - TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); - if (descriptor != null) - { - if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) - { - Collection subscribers = this.findSubscribers(identifier, false); - if (subscribers.contains(subscriber) && subscribers.size() == 1) - { - this.stopStream(descriptor); - } - } - } - try - { - this.eventNotifier.unsubscribe(identifier, subscriber); - } - catch (Exception e) - { - logger.error(e.getMessage(), e); - } - } - - @Override - public void subscribe(TICCoreSubscriber subscriber) - { - this.eventNotifier.subscribe(subscriber); - } - - @Override - public void unsubscribe(TICCoreSubscriber subscriber) - { - this.eventNotifier.unsubscribe(subscriber); - } - - @Override - public List getIndentifiers(TICCoreSubscriber listener) - { - List identifiers = new ArrayList(); - - if (this.eventNotifier.getSubscribers(true, false).contains(listener)) - { - identifiers.addAll(this.getAvailableTICs()); - } - else if (this.eventNotifier.getSubscribers(false, true).contains(listener)) - { - identifiers.addAll(this.eventNotifier.getFilters(listener)); - } - - return identifiers; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCoreSubscriber - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onData(TICCoreFrame frame) - { - logger.trace("TICCore frame:\n" + frame.toString(2)); - Collection subscriberList = this.findSubscribers(frame.getIdentifier(), true); - Task task = new TaskBase() - { - @Override - public void process() - { - TICCoreBase.this.notifyOnData(frame, subscriberList); - } - }; - task.start(); - } - - @Override - public void onError(TICCoreError error) - { - logger.error("TICCore error:\n" + error.toString(2)); - Collection subscriberList = this.findSubscribers(error.getIdentifier(), true); - Task task = new TaskBase() - { - @Override - public void process() - { - TICCoreBase.this.notifyOnError(error, subscriberList); - } - }; - task.start(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// PortPlugSubscriber - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onPlugged(TICPortDescriptor descriptor) - { - logger.info("TICCore modem plugged:\n" + descriptor.toString(2)); - this.startNewStream(descriptor); - } - - @Override - public void onUnplugged(TICPortDescriptor descriptor) - { - logger.info("TICCore modem unplugged:\n" + descriptor.toString(2)); - TICIdentifier identifier = this.stopStream(descriptor); - Task task = new TaskBase() - { - @Override - public void process() - { - TICCoreBase.this.notifyOnUnpluggedAndUnsubscribe(identifier); - } - }; - task.start(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Task - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void start() - { - if (!this.isRunning()) - { - logger.info("Starting TICCore"); - logger.debug("Starting TIC port plug notifier"); - this.plugNotifier = new TICPortPlugNotifier(this.plugNotifierPeriod, this.portFinder); - this.plugNotifier.subscribe(this); - this.plugNotifier.start(); - logger.debug("TIC port plug notifier started"); - if (this.nativePortNamesOnStart != null && this.nativePortNamesOnStart.size() > 0) - { - logger.debug("Starting natives TIC port: " + Arrays.toString(this.nativePortNamesOnStart.toArray())); - for (String portName : this.nativePortNamesOnStart) - { - TICPortDescriptor descriptor = this.portFinder.findNative(portName); - logger.debug("Starting native TIC port " + portName + " : " + descriptor); - if (descriptor != null && this.findStream(descriptor) == null) - { - this.startNewStream(descriptor); - } - } - } - } - } - - @Override - public void stop() - { - logger.info("Stopping TICCore"); - logger.debug("Stopping TIC port plug notifier"); - this.plugNotifier.unsubscribe(this); - this.plugNotifier.stop(); - logger.debug("Stopping all streams"); - for (TICCoreStream stream : this.streamList) - { - stream.unsubscribe(this); - stream.stop(); - } - this.streamList.clear(); - logger.debug("Removing all subscribers"); - this.unsubscribe(this.eventNotifier.getSubscribers()); - } - - @Override - public boolean isRunning() - { - return (this.plugNotifier != null) ? this.plugNotifier.isRunning() : false; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICCoreStream findStream(TICIdentifier identifier) - { - for (TICCoreStream stream : this.streamList) - { - if (stream.getIdentifier().matches(identifier)) - { - return stream; - } - } - - return null; - } - - private TICCoreStream findStream(TICPortDescriptor descriptor) - { - for (TICCoreStream stream : this.streamList) - { - TICIdentifier identifier = stream.getIdentifier(); - if (descriptor.getPortId() != null && identifier.getPortId() != null) - { - if (descriptor.getPortId().equals(identifier.getPortId())) - { - return stream; - } - } - if (descriptor.getPortName() != null && identifier.getPortName() != null) - { - if (descriptor.getPortName().equals(identifier.getPortName())) - { - return stream; - } - } - } - - return null; - } - - private TICCoreStream startNewStream(TICPortDescriptor descriptor) - { - TICCoreStream stream = null; - logger.debug("TICCore starting new stream : " + descriptor.toString()); - try - { - stream = this.streamConstructor.newInstance(descriptor.getPortId(), descriptor.getPortName(), this.streamMode); - - stream.subscribe(this); - - stream.start(); - this.streamList.add(stream); - logger.debug("TICCore started new stream : " + descriptor.toString()); - } - catch (Exception e) - { - logger.error(e.getMessage(), e); - } - return stream; - } - - private TICIdentifier stopStream(TICPortDescriptor descriptor) - { - TICIdentifier identifier = null; - TICCoreStream stream = this.findStream(descriptor); - logger.debug("TICCore stopping new stream : " + descriptor.toString()); - if (stream != null) - { - identifier = stream.getIdentifier(); - stream.unsubscribe(this); - stream.stop(); - this.streamList.remove(stream); - logger.debug("TICCore stopped new stream : " + descriptor.toString()); - } - - return identifier; - } - - private void closeNativeStream(TICIdentifier identifier, TICCoreStream stream, ReadNextFrameSubscriber subscriber) - { - TICPortDescriptor nativeDescriptor = this.portFinder.findNative(identifier.getPortName()); - if (nativeDescriptor != null) - { - if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) - { - Collection streamSubscribers = stream.getSubscribers(); - Collection ticCoreSubscribers = this.findSubscribers(identifier, false); - if (!streamSubscribers.contains(subscriber) && streamSubscribers.contains(this) && streamSubscribers.size() == 1 && ticCoreSubscribers.size() == 0) - { - this.stopStream(nativeDescriptor); - } - } - } - } - - private Collection findSubscribers(TICIdentifier sourceIdentifier, boolean globalSubscribers) - { - Predicate predicate = new Predicate() - { - @Override - public boolean test(TICIdentifier identifier) - { - return sourceIdentifier.matches(identifier); - } - }; - - return this.eventNotifier.getSubscribers(predicate, globalSubscribers); - } - - private void notifyOnUnpluggedAndUnsubscribe(TICIdentifier identifier) - { - try - { - TICCoreError error = new TICCoreError(identifier, TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), - "TICCore stream " + identifier + " has been unplugged (unsubscribe has been forced)"); - Collection subscriberList = this.findSubscribers(identifier, true); - this.notifyOnError(error, subscriberList); - subscriberList = this.findSubscribers(identifier, false); - this.unsubscribe(subscriberList); - } - catch (DataDictionaryException e) - { - logger.error(e.getMessage(), e); - } - } - - private void notifyOnData(TICCoreFrame frame, Collection subscriberList) - { - for (TICCoreSubscriber subscriber : subscriberList) - { - subscriber.onData(frame); - } - } - - private void notifyOnError(TICCoreError error, Collection subscriberList) - { - for (TICCoreSubscriber subscriber : subscriberList) - { - subscriber.onError(error); - } - } - - private void unsubscribe(Collection subscriberList) - { - for (TICCoreSubscriber subscriber : subscriberList) - { - this.unsubscribe(subscriber); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.function.Predicate; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import enedis.lab.io.PlugSubscriber; +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.io.tic.TICPortFinder; +import enedis.lab.io.tic.TICPortFinderBase; +import enedis.lab.io.tic.TICPortPlugNotifier; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.time.Time; +import enedis.lab.util.task.FilteredNotifier; +import enedis.lab.util.task.FilteredNotifierBase; +import enedis.lab.util.task.Task; +import enedis.lab.util.task.TaskBase; + +/** + * TICCore implementation + */ +public class TICCoreBase implements TICCore, Task, TICCoreSubscriber, PlugSubscriber +{ + private static final int PLUG_NOTIFIER_POLLING_PERIOD = 100; + private static final int READ_NEXT_FRAME_TIMEOUT = 30000; + private static final int READ_NEXT_FRAME_POLLING_PERIOD = 100; + + private TICPortFinder portFinder; + private TICPortPlugNotifier plugNotifier; + private long plugNotifierPeriod; + private Constructor streamConstructor; + private TICMode streamMode; + private List nativePortNamesOnStart; + private Collection streamList; + private FilteredNotifier eventNotifier; + private static Logger logger = LogManager.getLogger(); + + public TICCoreBase() + { + this(TICMode.AUTO, null); + } + + public TICCoreBase(TICMode streamMode, List nativePortNamesStart) + { + this(TICPortFinderBase.getInstance(), PLUG_NOTIFIER_POLLING_PERIOD, TICCoreStreamBase.class, streamMode, nativePortNamesStart); + } + + public TICCoreBase(TICPortFinder ticPortFinder, long plugNotifierPeriod, Class streamClass, TICMode streamMode, List nativePortNamesOnStart) + { + super(); + this.portFinder = ticPortFinder; + this.plugNotifierPeriod = plugNotifierPeriod; + try + { + this.streamConstructor = streamClass.getConstructor(String.class, String.class, TICMode.class); + } + catch (NoSuchMethodException e) + { + throw new IllegalArgumentException("Class " + streamClass.getName() + " has no valid constructor : " + e.getMessage()); + } + if (streamMode == null) + { + throw new IllegalArgumentException("TICMode should be defined"); + } + this.streamMode = streamMode; + this.nativePortNamesOnStart = nativePortNamesOnStart; + this.streamList = Collections.synchronizedSet(new HashSet()); + this.eventNotifier = new FilteredNotifierBase(); + } + + @Override + public List getAvailableTICs() + { + List identifiers = new DataArrayList(); + + for (TICCoreStream stream : this.streamList) + { + identifiers.add(stream.getIdentifier()); + } + + return identifiers; + } + + @Override + public List getModemsInfo() + { + List descriptors = new DataArrayList(); + + for (TICCoreStream stream : this.streamList) + { + TICPortDescriptor descriptor = this.portFinder.findByPortName(stream.getIdentifier().getPortName()); + descriptors.add(descriptor); + } + + return descriptors; + } + + @Override + public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException + { + return this.readNextFrame(identifier, READ_NEXT_FRAME_TIMEOUT); + } + + @Override + public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException + { + TICCoreFrame frame = null; + TICPortDescriptor descriptor = null; + TICCoreStream stream = this.findStream(identifier); + + if (stream == null) + { + descriptor = this.portFinder.findNative(identifier.getPortName()); + if (descriptor == null) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + stream = this.startNewStream(descriptor); + } + ReadNextFrameSubscriber subscriber = new ReadNextFrameSubscriber(); + stream.subscribe(subscriber); + + long begin = System.nanoTime(); + long elapsed = 0; + while (elapsed < timeout) + { + if (subscriber.getError() != null) + { + TICCoreException exception = new TICCoreException(subscriber.getError().getErrorCode().intValue(), subscriber.getError().getErrorMessage()); + logger.error(exception.getMessage(), exception); + stream.unsubscribe(subscriber); + this.closeNativeStream(identifier, stream, subscriber); + throw exception; + } + if (subscriber.getData() != null) + { + frame = subscriber.getData(); + break; + } + Time.sleep(READ_NEXT_FRAME_POLLING_PERIOD); + elapsed = (System.nanoTime() - begin) / 1000000; + } + stream.unsubscribe(subscriber); + this.closeNativeStream(identifier, stream, subscriber); + if (elapsed >= timeout) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), "Stream " + identifier + " data read timeout !"); + logger.error(exception.getMessage(), exception); + throw exception; + } + return frame; + } + + @Override + public void subscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException + { + TICCoreStream stream = this.findStream(identifier); + + if (stream == null) + { + TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); + if (descriptor == null) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + this.startNewStream(descriptor); + } + try + { + this.eventNotifier.subscribe(identifier, subscriber); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + } + + @Override + public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException + { + TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); + if (descriptor != null) + { + if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) + { + Collection subscribers = this.findSubscribers(identifier, false); + if (subscribers.contains(subscriber) && subscribers.size() == 1) + { + this.stopStream(descriptor); + } + } + } + try + { + this.eventNotifier.unsubscribe(identifier, subscriber); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + } + + @Override + public void subscribe(TICCoreSubscriber subscriber) + { + this.eventNotifier.subscribe(subscriber); + } + + @Override + public void unsubscribe(TICCoreSubscriber subscriber) + { + this.eventNotifier.unsubscribe(subscriber); + } + + @Override + public List getIndentifiers(TICCoreSubscriber listener) + { + List identifiers = new ArrayList(); + + if (this.eventNotifier.getSubscribers(true, false).contains(listener)) + { + identifiers.addAll(this.getAvailableTICs()); + } + else if (this.eventNotifier.getSubscribers(false, true).contains(listener)) + { + identifiers.addAll(this.eventNotifier.getFilters(listener)); + } + + return identifiers; + } + + @Override + public void onData(TICCoreFrame frame) + { + logger.trace("TICCore frame:\n" + frame.toString(2)); + Collection subscriberList = this.findSubscribers(frame.getIdentifier(), true); + Task task = new TaskBase() + { + @Override + public void process() + { + TICCoreBase.this.notifyOnData(frame, subscriberList); + } + }; + task.start(); + } + + @Override + public void onError(TICCoreError error) + { + logger.error("TICCore error:\n" + error.toString(2)); + Collection subscriberList = this.findSubscribers(error.getIdentifier(), true); + Task task = new TaskBase() + { + @Override + public void process() + { + TICCoreBase.this.notifyOnError(error, subscriberList); + } + }; + task.start(); + } + + @Override + public void onPlugged(TICPortDescriptor descriptor) + { + logger.info("TICCore modem plugged:\n" + descriptor.toString(2)); + this.startNewStream(descriptor); + } + + @Override + public void onUnplugged(TICPortDescriptor descriptor) + { + logger.info("TICCore modem unplugged:\n" + descriptor.toString(2)); + TICIdentifier identifier = this.stopStream(descriptor); + Task task = new TaskBase() + { + @Override + public void process() + { + TICCoreBase.this.notifyOnUnpluggedAndUnsubscribe(identifier); + } + }; + task.start(); + } + + @Override + public void start() + { + if (!this.isRunning()) + { + logger.info("Starting TICCore"); + logger.debug("Starting TIC port plug notifier"); + this.plugNotifier = new TICPortPlugNotifier(this.plugNotifierPeriod, this.portFinder); + this.plugNotifier.subscribe(this); + this.plugNotifier.start(); + logger.debug("TIC port plug notifier started"); + if (this.nativePortNamesOnStart != null && this.nativePortNamesOnStart.size() > 0) + { + logger.debug("Starting natives TIC port: " + Arrays.toString(this.nativePortNamesOnStart.toArray())); + for (String portName : this.nativePortNamesOnStart) + { + TICPortDescriptor descriptor = this.portFinder.findNative(portName); + logger.debug("Starting native TIC port " + portName + " : " + descriptor); + if (descriptor != null && this.findStream(descriptor) == null) + { + this.startNewStream(descriptor); + } + } + } + } + } + + @Override + public void stop() + { + logger.info("Stopping TICCore"); + logger.debug("Stopping TIC port plug notifier"); + this.plugNotifier.unsubscribe(this); + this.plugNotifier.stop(); + logger.debug("Stopping all streams"); + for (TICCoreStream stream : this.streamList) + { + stream.unsubscribe(this); + stream.stop(); + } + this.streamList.clear(); + logger.debug("Removing all subscribers"); + this.unsubscribe(this.eventNotifier.getSubscribers()); + } + + @Override + public boolean isRunning() + { + return (this.plugNotifier != null) ? this.plugNotifier.isRunning() : false; + } + + private TICCoreStream findStream(TICIdentifier identifier) + { + for (TICCoreStream stream : this.streamList) + { + if (stream.getIdentifier().matches(identifier)) + { + return stream; + } + } + + return null; + } + + private TICCoreStream findStream(TICPortDescriptor descriptor) + { + for (TICCoreStream stream : this.streamList) + { + TICIdentifier identifier = stream.getIdentifier(); + if (descriptor.getPortId() != null && identifier.getPortId() != null) + { + if (descriptor.getPortId().equals(identifier.getPortId())) + { + return stream; + } + } + if (descriptor.getPortName() != null && identifier.getPortName() != null) + { + if (descriptor.getPortName().equals(identifier.getPortName())) + { + return stream; + } + } + } + + return null; + } + + private TICCoreStream startNewStream(TICPortDescriptor descriptor) + { + TICCoreStream stream = null; + logger.debug("TICCore starting new stream : " + descriptor.toString()); + try + { + stream = this.streamConstructor.newInstance(descriptor.getPortId(), descriptor.getPortName(), this.streamMode); + + stream.subscribe(this); + + stream.start(); + this.streamList.add(stream); + logger.debug("TICCore started new stream : " + descriptor.toString()); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + return stream; + } + + private TICIdentifier stopStream(TICPortDescriptor descriptor) + { + TICIdentifier identifier = null; + TICCoreStream stream = this.findStream(descriptor); + logger.debug("TICCore stopping new stream : " + descriptor.toString()); + if (stream != null) + { + identifier = stream.getIdentifier(); + stream.unsubscribe(this); + stream.stop(); + this.streamList.remove(stream); + logger.debug("TICCore stopped new stream : " + descriptor.toString()); + } + + return identifier; + } + + private void closeNativeStream(TICIdentifier identifier, TICCoreStream stream, ReadNextFrameSubscriber subscriber) + { + TICPortDescriptor nativeDescriptor = this.portFinder.findNative(identifier.getPortName()); + if (nativeDescriptor != null) + { + if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) + { + Collection streamSubscribers = stream.getSubscribers(); + Collection ticCoreSubscribers = this.findSubscribers(identifier, false); + if (!streamSubscribers.contains(subscriber) && streamSubscribers.contains(this) && streamSubscribers.size() == 1 && ticCoreSubscribers.size() == 0) + { + this.stopStream(nativeDescriptor); + } + } + } + } + + private Collection findSubscribers(TICIdentifier sourceIdentifier, boolean globalSubscribers) + { + Predicate predicate = new Predicate() + { + @Override + public boolean test(TICIdentifier identifier) + { + return sourceIdentifier.matches(identifier); + } + }; + + return this.eventNotifier.getSubscribers(predicate, globalSubscribers); + } + + private void notifyOnUnpluggedAndUnsubscribe(TICIdentifier identifier) + { + try + { + TICCoreError error = new TICCoreError(identifier, TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + "TICCore stream " + identifier + " has been unplugged (unsubscribe has been forced)"); + Collection subscriberList = this.findSubscribers(identifier, true); + this.notifyOnError(error, subscriberList); + subscriberList = this.findSubscribers(identifier, false); + this.unsubscribe(subscriberList); + } + catch (DataDictionaryException e) + { + logger.error(e.getMessage(), e); + } + } + + private void notifyOnData(TICCoreFrame frame, Collection subscriberList) + { + for (TICCoreSubscriber subscriber : subscriberList) + { + subscriber.onData(frame); + } + } + + private void notifyOnError(TICCoreError error, Collection subscriberList) + { + for (TICCoreSubscriber subscriber : subscriberList) + { + subscriber.onError(error); + } + } + + private void unsubscribe(Collection subscriberList) + { + for (TICCoreSubscriber subscriber : subscriberList) + { + this.unsubscribe(subscriber); + } + } +} diff --git a/src/main/java/enedis/tic/core/TICCoreError.java b/src/main/java/enedis/tic/core/TICCoreError.java index 2e9ad38..2aea957 100644 --- a/src/main/java/enedis/tic/core/TICCoreError.java +++ b/src/main/java/enedis/tic/core/TICCoreError.java @@ -26,35 +26,11 @@ */ public class TICCoreError extends DataDictionaryBase { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected static final String KEY_IDENTIFIER = "identifier"; protected static final String KEY_ERROR_CODE = "errorCode"; protected static final String KEY_ERROR_MESSAGE = "errorMessage"; protected static final String KEY_DATA = "data"; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private List> keys = new ArrayList>(); protected KeyDescriptorDataDictionary kIdentifier; @@ -62,12 +38,6 @@ public class TICCoreError extends DataDictionaryBase protected KeyDescriptorString kErrorMessage; protected KeyDescriptorDataDictionary kData; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected TICCoreError() { super(); @@ -133,19 +103,6 @@ public TICCoreError(TICIdentifier identifier, Number errorCode, String errorMess this.checkAndUpdate(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Get identifier * @@ -230,12 +187,6 @@ public void setData(DataDictionary data) throws DataDictionaryException this.setData((Object) data); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected void setIdentifier(Object identifier) throws DataDictionaryException { this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); @@ -256,12 +207,6 @@ protected void setData(Object data) throws DataDictionaryException this.data.put(KEY_DATA, this.kData.convert(data)); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void loadKeyDescriptors() { try diff --git a/src/main/java/enedis/tic/core/TICCoreException.java b/src/main/java/enedis/tic/core/TICCoreException.java index aa0fa67..45bd7d3 100644 --- a/src/main/java/enedis/tic/core/TICCoreException.java +++ b/src/main/java/enedis/tic/core/TICCoreException.java @@ -1,76 +1,21 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.types.ExceptionBase; - -public class TICCoreException extends ExceptionBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -3285641164559292710L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreException(int code, String info) - { - super(code, info); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.types.ExceptionBase; + +public class TICCoreException extends ExceptionBase +{ + private static final long serialVersionUID = -3285641164559292710L; + + public TICCoreException(int code, String info) + { + super(code, info); + } + +} diff --git a/src/main/java/enedis/tic/core/TICCoreFrame.java b/src/main/java/enedis/tic/core/TICCoreFrame.java index eb6c7f2..97442ea 100644 --- a/src/main/java/enedis/tic/core/TICCoreFrame.java +++ b/src/main/java/enedis/tic/core/TICCoreFrame.java @@ -28,35 +28,11 @@ */ public class TICCoreFrame extends DataDictionaryBase { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected static final String KEY_IDENTIFIER = "identifier"; protected static final String KEY_MODE = "mode"; protected static final String KEY_CAPTURE_DATE_TIME = "captureDateTime"; protected static final String KEY_CONTENT = "content"; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private List> keys = new ArrayList>(); protected KeyDescriptorDataDictionary kIdentifier; @@ -64,12 +40,6 @@ public class TICCoreFrame extends DataDictionaryBase protected KeyDescriptorLocalDateTime kCaptureDateTime; protected KeyDescriptorDataDictionary kContent; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected TICCoreFrame() { super(); @@ -122,25 +92,12 @@ public TICCoreFrame(TICIdentifier identifier, TICMode mode, LocalDateTime captur this.checkAndUpdate(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override protected void updateOptionalParameters() throws DataDictionaryException { super.updateOptionalParameters(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Get identifier * @@ -225,12 +182,6 @@ public void setContent(DataDictionaryBase content) throws DataDictionaryExceptio this.setContent((Object) content); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected void setIdentifier(Object identifier) throws DataDictionaryException { this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); @@ -251,12 +202,6 @@ protected void setContent(Object content) throws DataDictionaryException this.data.put(KEY_CONTENT, this.kContent.convert(content)); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void loadKeyDescriptors() { try diff --git a/src/main/java/enedis/tic/core/TICCoreStreamBase.java b/src/main/java/enedis/tic/core/TICCoreStreamBase.java index 21ff386..8975280 100644 --- a/src/main/java/enedis/tic/core/TICCoreStreamBase.java +++ b/src/main/java/enedis/tic/core/TICCoreStreamBase.java @@ -1,378 +1,302 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.time.LocalDateTime; -import java.util.Collection; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.io.channels.Channel; -import enedis.lab.io.channels.ChannelException; -import enedis.lab.io.datastreams.DataStreamBase; -import enedis.lab.io.datastreams.DataStreamDirection; -import enedis.lab.io.datastreams.DataStreamException; -import enedis.lab.io.datastreams.DataStreamListener; -import enedis.lab.io.datastreams.DataStreamStatus; -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.io.tic.TICPortFinderBase; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.channels.ChannelTICSerialPort; -import enedis.lab.protocol.tic.channels.ChannelTICSerialPortConfiguration; -import enedis.lab.protocol.tic.datastreams.TICInputStream; -import enedis.lab.protocol.tic.datastreams.TICStreamConfiguration; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.util.task.Notifier; -import enedis.lab.util.task.NotifierBase; -import enedis.lab.util.task.Task; - -/** - * TICCore stream implementation - */ -public class TICCoreStreamBase implements TICCoreStream, Task, DataStreamListener -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICIdentifier identifier; - private DataStreamBase stream; - private Channel channel; - private Notifier notifier; - static private Logger logger = LogManager.getLogger(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreStreamBase(String portId, String portName, TICMode ticMode) throws TICCoreException - { - TICPortDescriptor descriptor = null; - - if (portId != null) - { - descriptor = TICPortFinderBase.getInstance().findByPortId(portId); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_ID_NOT_FOUND.getCode(), "TICCore stream port id " + portId + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - } - else if (portName != null) - { - descriptor = TICPortFinderBase.getInstance().findByPortName(portName); - if (descriptor == null) - { - descriptor = TICPortFinderBase.getInstance().findNative(portName); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_NAME_NOT_FOUND.getCode(), - "TICCore stream port name " + portName + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - } - } - else - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_DESCRIPTOR_EMPTY.getCode(), "TICCore stream port descriptor empty!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - if (ticMode == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_MODE_NOT_DEFINED.getCode(), "TICCore stream mode not defined!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - try - { - this.identifier = new TICIdentifier(portId, portName, null); - this.channel = new ChannelTICSerialPort(new ChannelTICSerialPortConfiguration("Channel@" + descriptor.getPortName(), portName, ticMode)); - this.stream = new TICInputStream( - new TICStreamConfiguration("Stream@" + descriptor.getPortName(), DataStreamDirection.INPUT, "Channel@" + descriptor.getPortName(), ticMode)); - this.stream.setChannel(this.channel); - this.notifier = new NotifierBase(); - logger = LogManager.getLogger(); - } - catch (DataDictionaryException | ChannelException | DataStreamException e) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.OTHER_REASON.getCode(), "TICCore stream instanciation failed : " + e.getMessage()); - logger.error(exception.getMessage(), exception); - throw exception; - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCoreStream - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICIdentifier getIdentifier() - { - TICIdentifier identifier; - - synchronized (this.identifier) - { - identifier = (TICIdentifier) this.identifier.clone(); - } - - return identifier; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Notifier - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Collection getSubscribers() - { - return this.notifier.getSubscribers(); - } - - @Override - public boolean hasSubscriber(TICCoreSubscriber subscriber) - { - return this.notifier.hasSubscriber(subscriber); - } - - @Override - public void subscribe(TICCoreSubscriber subscriber) - { - this.notifier.subscribe(subscriber); - } - - @Override - public void unsubscribe(TICCoreSubscriber subscriber) - { - this.notifier.unsubscribe(subscriber); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataStreamListener - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onDataReceived(String dataStreamName, DataDictionary data) - { - TICCoreFrame frame = this.createFrame(data); - - if (frame != null) - { - this.notifyOnData(frame); - } - else - { - logger.debug("Frame creation skipped due to null TICFrame"); - } - } - - @Override - public void onDataSent(String dataStreamName, DataDictionary data) - { - } - - @Override - public void onStatusChanged(String dataStreamName, DataStreamStatus newStatus) - { - } - - @Override - public void onErrorDetected(String dataStreamName, int errorCode, String errorMessage, DataDictionary data) - { - TICCoreError error = this.createError(errorCode, errorMessage, data); - this.notifyOnError(error); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Task - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void start() - { - this.channel.start(); - try - { - this.stream.open(); - this.stream.subscribe(this); - } - catch (DataStreamException e) - { - logger.error(e.getMessage()); - } - } - - @Override - public void stop() - { - this.stream.unsubscribe(this); - try - { - this.stream.close(); - } - catch (DataStreamException e) - { - logger.error(e.getMessage()); - } - this.channel.stop(); - } - - @Override - public boolean isRunning() - { - return this.channel.isRunning(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICCoreFrame createFrame(DataDictionary data) - { - TICCoreFrame frame = new TICCoreFrame(); - try - { - frame.setCaptureDateTime(LocalDateTime.now()); - TICFrame ticFrame = (TICFrame) data.get(TICInputStream.KEY_DATA); - - if (ticFrame == null) - { - logger.warn("TICFrame is null. Skipping frame creation."); - return null; - } - - DataDictionaryBase content = new DataDictionaryBase(); - for (TICFrameDataSet frameDataSet : ticFrame.getDataSetList()) - { - String label = frameDataSet.getLabel(); - content.set(label, ticFrame.getData(label)); - } - frame.setContent(content); - String serialNumber = null; - if (ticFrame instanceof TICFrameStandard) - { - frame.setMode(TICMode.STANDARD); - serialNumber = (String) content.get("ADSC"); - } - else if (ticFrame instanceof TICFrameHistoric) - { - frame.setMode(TICMode.HISTORIC); - serialNumber = (String) content.get("ADCO"); - } - synchronized (this.identifier) - { - this.identifier.setSerialNumber(serialNumber); - frame.setIdentifier(this.identifier.clone()); - } - } - catch (DataDictionaryException e) - { - logger.error(e.getMessage()); - frame = null; - } - - return frame; - } - - private TICCoreError createError(int errorCode, String errorMessage, DataDictionary data) - { - TICCoreError error = null; - try - { - error = new TICCoreError(this.getIdentifier(), errorCode, errorMessage, data); - } - catch (DataDictionaryException e) - { - logger.error(e.getMessage()); - } - - return error; - } - - private void notifyOnData(TICCoreFrame frame) - { - if (frame == null) - { - return; - } - for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) - { - subscriber.onData(frame); - } - } - - private void notifyOnError(TICCoreError error) - { - if (error == null) - { - return; - } - for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) - { - subscriber.onError(error); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.time.LocalDateTime; +import java.util.Collection; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import enedis.lab.io.channels.Channel; +import enedis.lab.io.channels.ChannelException; +import enedis.lab.io.datastreams.DataStreamBase; +import enedis.lab.io.datastreams.DataStreamDirection; +import enedis.lab.io.datastreams.DataStreamException; +import enedis.lab.io.datastreams.DataStreamListener; +import enedis.lab.io.datastreams.DataStreamStatus; +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.io.tic.TICPortFinderBase; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.protocol.tic.channels.ChannelTICSerialPort; +import enedis.lab.protocol.tic.channels.ChannelTICSerialPortConfiguration; +import enedis.lab.protocol.tic.datastreams.TICInputStream; +import enedis.lab.protocol.tic.datastreams.TICStreamConfiguration; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; +import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.util.task.Notifier; +import enedis.lab.util.task.NotifierBase; +import enedis.lab.util.task.Task; + +/** + * TICCore stream implementation + */ +public class TICCoreStreamBase implements TICCoreStream, Task, DataStreamListener +{ + + private TICIdentifier identifier; + private DataStreamBase stream; + private Channel channel; + private Notifier notifier; + static private Logger logger = LogManager.getLogger(); + + public TICCoreStreamBase(String portId, String portName, TICMode ticMode) throws TICCoreException + { + TICPortDescriptor descriptor = null; + + if (portId != null) + { + descriptor = TICPortFinderBase.getInstance().findByPortId(portId); + if (descriptor == null) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_ID_NOT_FOUND.getCode(), "TICCore stream port id " + portId + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + } + else if (portName != null) + { + descriptor = TICPortFinderBase.getInstance().findByPortName(portName); + if (descriptor == null) + { + descriptor = TICPortFinderBase.getInstance().findNative(portName); + if (descriptor == null) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_NAME_NOT_FOUND.getCode(), + "TICCore stream port name " + portName + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + } + } + else + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_DESCRIPTOR_EMPTY.getCode(), "TICCore stream port descriptor empty!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + if (ticMode == null) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_MODE_NOT_DEFINED.getCode(), "TICCore stream mode not defined!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + try + { + this.identifier = new TICIdentifier(portId, portName, null); + this.channel = new ChannelTICSerialPort(new ChannelTICSerialPortConfiguration("Channel@" + descriptor.getPortName(), portName, ticMode)); + this.stream = new TICInputStream( + new TICStreamConfiguration("Stream@" + descriptor.getPortName(), DataStreamDirection.INPUT, "Channel@" + descriptor.getPortName(), ticMode)); + this.stream.setChannel(this.channel); + this.notifier = new NotifierBase(); + logger = LogManager.getLogger(); + } + catch (DataDictionaryException | ChannelException | DataStreamException e) + { + TICCoreException exception = new TICCoreException(TICCoreErrorCode.OTHER_REASON.getCode(), "TICCore stream instanciation failed : " + e.getMessage()); + logger.error(exception.getMessage(), exception); + throw exception; + } + } + + @Override + public TICIdentifier getIdentifier() + { + TICIdentifier identifier; + + synchronized (this.identifier) + { + identifier = (TICIdentifier) this.identifier.clone(); + } + + return identifier; + } + + @Override + public Collection getSubscribers() + { + return this.notifier.getSubscribers(); + } + + @Override + public boolean hasSubscriber(TICCoreSubscriber subscriber) + { + return this.notifier.hasSubscriber(subscriber); + } + + @Override + public void subscribe(TICCoreSubscriber subscriber) + { + this.notifier.subscribe(subscriber); + } + + @Override + public void unsubscribe(TICCoreSubscriber subscriber) + { + this.notifier.unsubscribe(subscriber); + } + + @Override + public void onDataReceived(String dataStreamName, DataDictionary data) + { + TICCoreFrame frame = this.createFrame(data); + + if (frame != null) + { + this.notifyOnData(frame); + } + else + { + logger.debug("Frame creation skipped due to null TICFrame"); + } + } + + @Override + public void onDataSent(String dataStreamName, DataDictionary data) + { + } + + @Override + public void onStatusChanged(String dataStreamName, DataStreamStatus newStatus) + { + } + + @Override + public void onErrorDetected(String dataStreamName, int errorCode, String errorMessage, DataDictionary data) + { + TICCoreError error = this.createError(errorCode, errorMessage, data); + this.notifyOnError(error); + } + + @Override + public void start() + { + this.channel.start(); + try + { + this.stream.open(); + this.stream.subscribe(this); + } + catch (DataStreamException e) + { + logger.error(e.getMessage()); + } + } + + @Override + public void stop() + { + this.stream.unsubscribe(this); + try + { + this.stream.close(); + } + catch (DataStreamException e) + { + logger.error(e.getMessage()); + } + this.channel.stop(); + } + + @Override + public boolean isRunning() + { + return this.channel.isRunning(); + } + + private TICCoreFrame createFrame(DataDictionary data) + { + TICCoreFrame frame = new TICCoreFrame(); + try + { + frame.setCaptureDateTime(LocalDateTime.now()); + TICFrame ticFrame = (TICFrame) data.get(TICInputStream.KEY_DATA); + + if (ticFrame == null) + { + logger.warn("TICFrame is null. Skipping frame creation."); + return null; + } + + DataDictionaryBase content = new DataDictionaryBase(); + for (TICFrameDataSet frameDataSet : ticFrame.getDataSetList()) + { + String label = frameDataSet.getLabel(); + content.set(label, ticFrame.getData(label)); + } + frame.setContent(content); + String serialNumber = null; + if (ticFrame instanceof TICFrameStandard) + { + frame.setMode(TICMode.STANDARD); + serialNumber = (String) content.get("ADSC"); + } + else if (ticFrame instanceof TICFrameHistoric) + { + frame.setMode(TICMode.HISTORIC); + serialNumber = (String) content.get("ADCO"); + } + synchronized (this.identifier) + { + this.identifier.setSerialNumber(serialNumber); + frame.setIdentifier(this.identifier.clone()); + } + } + catch (DataDictionaryException e) + { + logger.error(e.getMessage()); + frame = null; + } + + return frame; + } + + private TICCoreError createError(int errorCode, String errorMessage, DataDictionary data) + { + TICCoreError error = null; + try + { + error = new TICCoreError(this.getIdentifier(), errorCode, errorMessage, data); + } + catch (DataDictionaryException e) + { + logger.error(e.getMessage()); + } + + return error; + } + + private void notifyOnData(TICCoreFrame frame) + { + if (frame == null) + { + return; + } + for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) + { + subscriber.onData(frame); + } + } + + private void notifyOnError(TICCoreError error) + { + if (error == null) + { + return; + } + for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) + { + subscriber.onError(error); + } + } +} diff --git a/src/main/java/enedis/tic/core/TICIdentifier.java b/src/main/java/enedis/tic/core/TICIdentifier.java index 7f83c81..a6cde29 100644 --- a/src/main/java/enedis/tic/core/TICIdentifier.java +++ b/src/main/java/enedis/tic/core/TICIdentifier.java @@ -24,46 +24,16 @@ */ public class TICIdentifier extends DataDictionaryBase { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected static final String KEY_PORT_ID = "portId"; protected static final String KEY_PORT_NAME = "portName"; protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private List> keys = new ArrayList>(); protected KeyDescriptorString kPortId; protected KeyDescriptorString kPortName; protected KeyDescriptorString kSerialNumber; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected TICIdentifier() { super(); @@ -114,13 +84,6 @@ public TICIdentifier(String portId, String portName, String serialNumber) throws this.checkAndUpdate(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override protected void updateOptionalParameters() throws DataDictionaryException { @@ -130,12 +93,6 @@ protected void updateOptionalParameters() throws DataDictionaryException } } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - public boolean matches(TICIdentifier identifier) { if (identifier != null) @@ -232,12 +189,6 @@ public void setSerialNumber(String serialNumber) throws DataDictionaryException this.setSerialNumber((Object) serialNumber); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected void setPortId(Object portId) throws DataDictionaryException { this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); @@ -253,12 +204,6 @@ protected void setSerialNumber(Object serialNumber) throws DataDictionaryExcepti this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void loadKeyDescriptors() { try diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java b/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java index 82dcd19..e815213 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java +++ b/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java @@ -34,12 +34,6 @@ */ public class TIC2WebSocketApplication { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Project properties (see file project.properties) */ @@ -82,18 +76,6 @@ public class TIC2WebSocketApplication */ public static final String DESCRIPTION = PROJECT_PROPERTIES.getProperty("project_description", ""); - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Program entry point * @@ -115,12 +97,6 @@ public static void main(String[] args) } } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private String[] commandLineArgs; private CommandLine commandLineParser; private TIC2WebSocketCommandLine commandLine; @@ -133,25 +109,6 @@ public static void main(String[] args) private TIC2WebSocketRequestHandler requestHandler; private TICCore ticCore; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Application constructor * @@ -333,18 +290,6 @@ public int stop() return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private int parseCommandLine() { try diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java b/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java index ac5474b..8de4f88 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java +++ b/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java @@ -20,18 +20,6 @@ @Command() public class TIC2WebSocketCommandLine { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - static class VerboseLevelConverter implements ITypeConverter { @Override @@ -71,18 +59,6 @@ public Level convert(String value) throws Exception } } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Option(names = { "-h", "--help" }, usageHelp = true, description = "Display help") boolean helpRequested = false; @@ -106,35 +82,4 @@ public Level convert(String value) throws Exception @Option(names = { "--configFile" }, paramLabel = "PATH", description = "Set configuration file") String configFile = null; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - }; diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java index b45040a..8cd94ca 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java @@ -28,40 +28,10 @@ */ public class TIC2WebSocketClient implements TICCoreSubscriber { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private Logger logger; private Channel channel; private EventSender eventSender; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Constructor * @@ -75,13 +45,6 @@ public TIC2WebSocketClient(Channel channel, EventSender eventSender) { this.eventSender = eventSender; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCoreSubscriber - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public void onData(TICCoreFrame frame) { @@ -110,12 +73,6 @@ public void onError(TICCoreError error) } } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Get client websocket channel * @@ -126,16 +83,4 @@ public Channel getChannel() return this.channel; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - } diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java index 7cf68db..d777c35 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java @@ -20,38 +20,8 @@ */ public class TIC2WebSocketClientPoolBase implements TIC2WebSocketClientPool { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private Set clients; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Default constructor */ @@ -61,19 +31,6 @@ public TIC2WebSocketClientPoolBase() this.clients = new CopyOnWriteArraySet(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public Optional getClient(String channelId) { @@ -119,18 +76,6 @@ public void remove(String channelId) } } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void checkArguments(Channel channel, EventSender sender) { if (channel == null || sender == null) { diff --git a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java index 6fa8e44..a8e530e 100644 --- a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java +++ b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java @@ -28,12 +28,6 @@ */ public class TIC2WebSocketConfiguration extends ConfigurationBase { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected static final String KEY_SERVER_PORT = "serverPort"; protected static final String KEY_TIC_MODE = "ticMode"; protected static final String KEY_TIC_PORT_NAMES = "ticPortNames"; @@ -42,36 +36,12 @@ public class TIC2WebSocketConfiguration extends ConfigurationBase private static final Number SERVER_PORT_MAX = 65535; private static final int TIC_PORT_NAMES_MIN_SIZE = 1; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private List> keys = new ArrayList>(); protected KeyDescriptorNumberMinMax kServerPort; protected KeyDescriptorEnum kTicMode; protected KeyDescriptorListMinMaxSize kTicPortNames; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected TIC2WebSocketConfiguration() { super(); @@ -137,12 +107,6 @@ public TIC2WebSocketConfiguration(Number serverPort, TICMode ticMode, List ticPortNames) throws DataDictionaryExce this.setTicPortNames((Object) ticPortNames); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected void setServerPort(Object serverPort) throws DataDictionaryException { this.data.put(KEY_SERVER_PORT, this.kServerPort.convert(serverPort)); @@ -246,12 +204,6 @@ else if (i != portNames.lastIndexOf(portName)) this.data.put(KEY_TIC_PORT_NAMES, portNames); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void loadKeyDescriptors() { try diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java index 4e7b38c..e114dd3 100644 --- a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java +++ b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java @@ -17,40 +17,10 @@ public class TIC2WebSocketEndPointException extends Exception { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private static final long serialVersionUID = -2263755971102386572L; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private TIC2WebSocketEndPointErrorCode code; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Default constructor * @@ -102,19 +72,6 @@ public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, Throw this.code = code; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Get error code * @@ -125,16 +82,4 @@ public TIC2WebSocketEndPointErrorCode getCode() return this.code; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - } diff --git a/src/main/java/enedis/tic/service/message/EventOnError.java b/src/main/java/enedis/tic/service/message/EventOnError.java index da39693..0c43f6f 100644 --- a/src/main/java/enedis/tic/service/message/EventOnError.java +++ b/src/main/java/enedis/tic/service/message/EventOnError.java @@ -1,192 +1,137 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Event; -import enedis.tic.core.TICCoreError; - -/** - * EventOnError class - * - * Generated - */ -public class EventOnError extends Event -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "OnError"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected EventOnError() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public EventOnError(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public EventOnError(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param data - * @throws DataDictionaryException - */ - public EventOnError(LocalDateTime dateTime, TICCoreError data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Event - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - public TICCoreError getData() - { - return (TICCoreError) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICCoreError data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreError.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import enedis.lab.util.message.Event; +import enedis.tic.core.TICCoreError; + +/** + * EventOnError class + * + * Generated + */ +public class EventOnError extends Event +{ + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "OnError"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + protected EventOnError() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public EventOnError(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public EventOnError(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param data + * @throws DataDictionaryException + */ + public EventOnError(LocalDateTime dateTime, TICCoreError data) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + public TICCoreError getData() + { + return (TICCoreError) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICCoreError data) throws DataDictionaryException + { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreError.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/EventOnTICData.java b/src/main/java/enedis/tic/service/message/EventOnTICData.java index c47d701..c51e38e 100644 --- a/src/main/java/enedis/tic/service/message/EventOnTICData.java +++ b/src/main/java/enedis/tic/service/message/EventOnTICData.java @@ -1,192 +1,137 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Event; -import enedis.tic.core.TICCoreFrame; - -/** - * EventOnTICData class - * - * Generated - */ -public class EventOnTICData extends Event -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "OnTICData"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected EventOnTICData() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public EventOnTICData(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public EventOnTICData(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param data - * @throws DataDictionaryException - */ - public EventOnTICData(LocalDateTime dateTime, TICCoreFrame data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Event - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - public TICCoreFrame getData() - { - return (TICCoreFrame) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICCoreFrame data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, true, TICCoreFrame.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import enedis.lab.util.message.Event; +import enedis.tic.core.TICCoreFrame; + +/** + * EventOnTICData class + * + * Generated + */ +public class EventOnTICData extends Event +{ + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "OnTICData"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + protected EventOnTICData() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public EventOnTICData(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public EventOnTICData(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param data + * @throws DataDictionaryException + */ + public EventOnTICData(LocalDateTime dateTime, TICCoreFrame data) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + public TICCoreFrame getData() + { + return (TICCoreFrame) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICCoreFrame data) throws DataDictionaryException + { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, true, TICCoreFrame.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java index 0fdd485..2e342ed 100644 --- a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java +++ b/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java @@ -1,128 +1,74 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Request; - -/** - * RequestGetAvailableTICs class - * - * Generated - */ -public class RequestGetAvailableTICs extends Request -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Message name */ - public static final String NAME = "GetAvailableTICs"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @throws DataDictionaryException - */ - public RequestGetAvailableTICs() throws DataDictionaryException - { - super(); - - this.kName.setAcceptedValues(NAME); - - this.checkAndUpdate(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestGetAvailableTICs(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestGetAvailableTICs(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Request - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.message.Request; + +/** + * RequestGetAvailableTICs class + * + * Generated + */ +public class RequestGetAvailableTICs extends Request +{ + /** Message name */ + public static final String NAME = "GetAvailableTICs"; + + /** + * Default constructor + * + * @throws DataDictionaryException + */ + public RequestGetAvailableTICs() throws DataDictionaryException + { + super(); + + this.kName.setAcceptedValues(NAME); + + this.checkAndUpdate(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestGetAvailableTICs(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestGetAvailableTICs(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + +} diff --git a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java b/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java index 250981a..d3a2357 100644 --- a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java +++ b/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java @@ -1,123 +1,69 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Request; - -/** - * RequestGetModemsInfo class - * - * Generated - */ -public class RequestGetModemsInfo extends Request -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Message name */ - public static final String NAME = "GetModemsInfo"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public RequestGetModemsInfo() throws DataDictionaryException - { - super(); - - this.kName.setAcceptedValues(NAME); - - this.checkAndUpdate(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestGetModemsInfo(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestGetModemsInfo(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Request - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.message.Request; + +/** + * RequestGetModemsInfo class + * + * Generated + */ +public class RequestGetModemsInfo extends Request +{ + /** Message name */ + public static final String NAME = "GetModemsInfo"; + + public RequestGetModemsInfo() throws DataDictionaryException + { + super(); + + this.kName.setAcceptedValues(NAME); + + this.checkAndUpdate(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestGetModemsInfo(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestGetModemsInfo(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + +} diff --git a/src/main/java/enedis/tic/service/message/RequestReadTIC.java b/src/main/java/enedis/tic/service/message/RequestReadTIC.java index 95fb548..0a42a12 100644 --- a/src/main/java/enedis/tic/service/message/RequestReadTIC.java +++ b/src/main/java/enedis/tic/service/message/RequestReadTIC.java @@ -1,189 +1,134 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Request; -import enedis.tic.core.TICIdentifier; - -/** - * RequestReadTIC class - * - * Generated - */ -public class RequestReadTIC extends Request -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "ReadTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected RequestReadTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestReadTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestReadTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param data - * @throws DataDictionaryException - */ - public RequestReadTIC(TICIdentifier data) throws DataDictionaryException - { - this(); - - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Request - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - public TICIdentifier getData() - { - return (TICIdentifier) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICIdentifier data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, true, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import enedis.lab.util.message.Request; +import enedis.tic.core.TICIdentifier; + +/** + * RequestReadTIC class + * + * Generated + */ +public class RequestReadTIC extends Request +{ + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "ReadTIC"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + protected RequestReadTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestReadTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestReadTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param data + * @throws DataDictionaryException + */ + public RequestReadTIC(TICIdentifier data) throws DataDictionaryException + { + this(); + + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + public TICIdentifier getData() + { + return (TICIdentifier) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICIdentifier data) throws DataDictionaryException + { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, true, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java index 499917a..0e91d0c 100644 --- a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java @@ -1,190 +1,135 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Request; -import enedis.tic.core.TICIdentifier; - -/** - * RequestSubscribeTIC class - * - * Generated - */ -public class RequestSubscribeTIC extends Request -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "SubscribeTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected RequestSubscribeTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestSubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestSubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param data - * @throws DataDictionaryException - */ - public RequestSubscribeTIC(List data) throws DataDictionaryException - { - this(); - - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Request - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorList; +import enedis.lab.util.message.Request; +import enedis.tic.core.TICIdentifier; + +/** + * RequestSubscribeTIC class + * + * Generated + */ +public class RequestSubscribeTIC extends Request +{ + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "SubscribeTIC"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + protected RequestSubscribeTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestSubscribeTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestSubscribeTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param data + * @throws DataDictionaryException + */ + public RequestSubscribeTIC(List data) throws DataDictionaryException + { + this(); + + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() + { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException + { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java index 6556d82..c0a9f60 100644 --- a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java @@ -1,190 +1,135 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Request; -import enedis.tic.core.TICIdentifier; - -/** - * RequestUnsubscribeTIC class - * - * Generated - */ -public class RequestUnsubscribeTIC extends Request -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "UnsubscribeTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected RequestUnsubscribeTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestUnsubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestUnsubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param data - * @throws DataDictionaryException - */ - public RequestUnsubscribeTIC(List data) throws DataDictionaryException - { - this(); - - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Request - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorList; +import enedis.lab.util.message.Request; +import enedis.tic.core.TICIdentifier; + +/** + * RequestUnsubscribeTIC class + * + * Generated + */ +public class RequestUnsubscribeTIC extends Request +{ + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "UnsubscribeTIC"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + protected RequestUnsubscribeTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestUnsubscribeTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestUnsubscribeTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param data + * @throws DataDictionaryException + */ + public RequestUnsubscribeTIC(List data) throws DataDictionaryException + { + this(); + + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() + { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException + { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java index 8c92f4d..95ef2c3 100644 --- a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java +++ b/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java @@ -1,197 +1,142 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Response; -import enedis.tic.core.TICIdentifier; - -/** - * ResponseGetAvailableTICs class - * - * Generated - */ -public class ResponseGetAvailableTICs extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "GetAvailableTICs"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseGetAvailableTICs() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseGetAvailableTICs(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseGetAvailableTICs(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseGetAvailableTICs(LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorList; +import enedis.lab.util.message.Response; +import enedis.tic.core.TICIdentifier; + +/** + * ResponseGetAvailableTICs class + * + * Generated + */ +public class ResponseGetAvailableTICs extends Response +{ + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "GetAvailableTICs"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + protected ResponseGetAvailableTICs() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseGetAvailableTICs(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseGetAvailableTICs(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseGetAvailableTICs(LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() + { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException + { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java b/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java index 7a66ad1..fdb47d0 100644 --- a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java +++ b/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java @@ -1,197 +1,142 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Response; - -/** - * ResponseGetModemsInfo class - * - * Generated - */ -public class ResponseGetModemsInfo extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "GetModemsInfo"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseGetModemsInfo() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseGetModemsInfo(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseGetModemsInfo(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseGetModemsInfo(LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICPortDescriptor.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorList; +import enedis.lab.util.message.Response; + +/** + * ResponseGetModemsInfo class + * + * Generated + */ +public class ResponseGetModemsInfo extends Response +{ + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "GetModemsInfo"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + protected ResponseGetModemsInfo() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseGetModemsInfo(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseGetModemsInfo(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseGetModemsInfo(LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() + { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException + { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICPortDescriptor.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java b/src/main/java/enedis/tic/service/message/ResponseReadTIC.java index 0a864cb..1f3ed8f 100644 --- a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java +++ b/src/main/java/enedis/tic/service/message/ResponseReadTIC.java @@ -1,196 +1,141 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Response; -import enedis.tic.core.TICCoreFrame; - -/** - * ResponseReadTIC class - * - * Generated - */ -public class ResponseReadTIC extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "ReadTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseReadTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseReadTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseReadTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseReadTIC(LocalDateTime dateTime, Number errorCode, String errorMessage, TICCoreFrame data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - public TICCoreFrame getData() - { - return (TICCoreFrame) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICCoreFrame data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreFrame.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import enedis.lab.util.message.Response; +import enedis.tic.core.TICCoreFrame; + +/** + * ResponseReadTIC class + * + * Generated + */ +public class ResponseReadTIC extends Response +{ + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "ReadTIC"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + protected ResponseReadTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseReadTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseReadTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseReadTIC(LocalDateTime dateTime, Number errorCode, String errorMessage, TICCoreFrame data) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + public TICCoreFrame getData() + { + return (TICCoreFrame) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICCoreFrame data) throws DataDictionaryException + { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException + { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() + { + try + { + this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreFrame.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java index 9577924..b68500b 100644 --- a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java @@ -1,160 +1,105 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.util.message.Response; - -/** - * ResponseSubscribeTIC class - * - * Generated - */ -public class ResponseSubscribeTIC extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Message name */ - public static final String NAME = "SubscribeTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseSubscribeTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseSubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseSubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public ResponseSubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.util.message.Response; + +/** + * ResponseSubscribeTIC class + * + * Generated + */ +public class ResponseSubscribeTIC extends Response +{ + /** Message name */ + public static final String NAME = "SubscribeTIC"; + + private List> keys = new ArrayList>(); + + protected ResponseSubscribeTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseSubscribeTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseSubscribeTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public ResponseSubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + private void loadKeyDescriptors() + { + try + { + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java index b182e62..46eff64 100644 --- a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java @@ -1,160 +1,105 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.util.message.Response; - -/** - * ResponseUnsubscribeTIC class - * - * Generated - */ -public class ResponseUnsubscribeTIC extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Message name */ - public static final String NAME = "UnsubscribeTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseUnsubscribeTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseUnsubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseUnsubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public ResponseUnsubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.util.message.Response; + +/** + * ResponseUnsubscribeTIC class + * + * Generated + */ +public class ResponseUnsubscribeTIC extends Response +{ + /** Message name */ + public static final String NAME = "UnsubscribeTIC"; + + private List> keys = new ArrayList>(); + + protected ResponseUnsubscribeTIC() + { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseUnsubscribeTIC(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseUnsubscribeTIC(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public ResponseUnsubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException + { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_NAME)) + { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + private void loadKeyDescriptors() + { + try + { + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java index 16e5518..fa54692 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java @@ -24,41 +24,11 @@ */ public class TIC2WebSocketChannelInitializer extends ChannelInitializer { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private static final String WEBSOCKET_PATH = "/"; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private final TIC2WebSocketClientPool clientPool; private final TIC2WebSocketRequestHandler requestHandler; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Constructor * @@ -71,18 +41,6 @@ public TIC2WebSocketChannelInitializer(TIC2WebSocketClientPool clientPool, TIC2W this.requestHandler = requestHandler; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override protected void initChannel(SocketChannel ch) throws Exception { @@ -107,15 +65,4 @@ protected void initChannel(SocketChannel ch) throws Exception pipeline.addLast(new TIC2WebSocketHandler(clientPool, requestHandler)); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java index 65ae2a9..5d12ae6 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java @@ -44,42 +44,12 @@ */ public class TIC2WebSocketHandler extends SimpleChannelInboundHandler implements EventSender { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private static final Logger logger = LogManager.getLogger(TIC2WebSocketHandler.class); private final TIC2WebSocketClientPool clientPool; private final TIC2WebSocketRequestHandler requestHandler; private final TIC2WebSocketMessageFactory factory; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Constructor * @@ -93,25 +63,12 @@ public TIC2WebSocketHandler(TIC2WebSocketClientPool clientPool, TIC2WebSocketReq this.factory = new TIC2WebSocketMessageFactory(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// EventSender - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public void sendEvent(Channel channel, Event event) { this.sendMessage(channel, event); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { @@ -213,18 +170,6 @@ protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) thr } } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private TIC2WebSocketClient getClient(Channel channel) { String channelId = channel.id().asLongText(); diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java index 2c22b8d..5a1919e 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java @@ -27,30 +27,6 @@ */ public class TIC2WebSocketServer { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private static final Logger logger = LogManager.getLogger(TIC2WebSocketServer.class); private final String host; @@ -62,12 +38,6 @@ public class TIC2WebSocketServer private EventLoopGroup workerGroup; private Channel serverChannel; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Constructor * @@ -84,18 +54,6 @@ public TIC2WebSocketServer(String host, int port, TIC2WebSocketClientPool client this.requestHandler = requestHandler; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Start the WebSocket server * @@ -176,15 +134,4 @@ public void waitForClose() throws InterruptedException } } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java index 22f623b..65eb3b6 100644 --- a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java +++ b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java @@ -43,39 +43,9 @@ */ public class TIC2WebSocketRequestHandlerBase implements TIC2WebSocketRequestHandler { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private Logger logger; private TICCore ticCore; - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Default constructor * @@ -88,19 +58,6 @@ public TIC2WebSocketRequestHandlerBase(TICCore ticCore) this.ticCore = ticCore; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public Response handle(Request request, TIC2WebSocketClient client) { @@ -132,18 +89,6 @@ public Response handle(Request request, TIC2WebSocketClient client) return response; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private Response handleGetAvailableTICsRequest(RequestGetAvailableTICs request) { List ticIdentifiers = this.ticCore.getAvailableTICs(); From 2d9515851c895af3639f8dc3265fd2a61ee914d4 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Fri, 3 Oct 2025 14:59:29 +0200 Subject: [PATCH 20/59] chore: remove bloc comment sections from tests --- .../enedis/lab/io/tic/TICPortFinderMock.java | 149 ++++------ .../tic/core/TICCoreReadNextFrameTask.java | 163 ++++------- .../enedis/tic/core/TICCoreStreamMock.java | 265 +++++++----------- .../tic/core/TICCoreSubscriberMock.java | 115 ++------ .../tic/core/TICCoreSubscriberOnDataCall.java | 101 ++----- .../core/TICCoreSubscriberOnErrorCall.java | 101 ++----- 6 files changed, 275 insertions(+), 619 deletions(-) diff --git a/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java b/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java index 225f8f4..585cbf4 100644 --- a/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java +++ b/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java @@ -1,102 +1,47 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import enedis.lab.io.PortFinderMock; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; - -@SuppressWarnings("javadoc") -public class TICPortFinderMock extends PortFinderMock implements TICPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public DataList nativeDescriptorList = new DataArrayList(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICPortFinderMock() - { - super(); - } - - public TICPortFinderMock(TICPortDescriptor... descriptors) - { - this.setDescriptors(DataArrayList.asList(descriptors)); - } - - @Override - public TICPortDescriptor findNative(String portName) - { - for (TICPortDescriptor descriptor : this.nativeDescriptorList) - { - if (descriptor.getPortName() == null && portName == null) - { - return descriptor; - } - if (descriptor.getPortName().equals(portName)) - { - return descriptor; - } - } - - return null; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.tic; + +import enedis.lab.io.PortFinderMock; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataList; + +@SuppressWarnings("javadoc") +public class TICPortFinderMock extends PortFinderMock implements TICPortFinder +{ + public DataList nativeDescriptorList = new DataArrayList(); + + public TICPortFinderMock() + { + super(); + } + + public TICPortFinderMock(TICPortDescriptor... descriptors) + { + this.setDescriptors(DataArrayList.asList(descriptors)); + } + + @Override + public TICPortDescriptor findNative(String portName) + { + for (TICPortDescriptor descriptor : this.nativeDescriptorList) + { + if (descriptor.getPortName() == null && portName == null) + { + return descriptor; + } + if (descriptor.getPortName().equals(portName)) + { + return descriptor; + } + } + + return null; + } + +} diff --git a/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java b/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java index d31601d..54744b7 100644 --- a/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java +++ b/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java @@ -1,109 +1,54 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.util.task.TaskBase; - -public class TICCoreReadNextFrameTask extends TaskBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCore ticCore; - public TICIdentifier identifier; - public TICCoreFrame frame; - public Exception exception; - public int timeout; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier) - { - this(ticCore, identifier, -1); - } - - public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier, int timeout) - { - super(); - this.ticCore = ticCore; - this.identifier = identifier; - this.timeout = timeout; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TaskBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void process() - { - try - { - if (this.timeout > 0) - { - this.frame = this.ticCore.readNextFrame(this.identifier, this.timeout); - } - else - { - this.frame = this.ticCore.readNextFrame(this.identifier); - } - } - catch (Exception exception) - { - this.exception = exception; - } - - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.util.task.TaskBase; + +public class TICCoreReadNextFrameTask extends TaskBase +{ + public TICCore ticCore; + public TICIdentifier identifier; + public TICCoreFrame frame; + public Exception exception; + public int timeout; + + public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier) + { + this(ticCore, identifier, -1); + } + + public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier, int timeout) + { + super(); + this.ticCore = ticCore; + this.identifier = identifier; + this.timeout = timeout; + } + + @Override + protected void process() + { + try + { + if (this.timeout > 0) + { + this.frame = this.ticCore.readNextFrame(this.identifier, this.timeout); + } + else + { + this.frame = this.ticCore.readNextFrame(this.identifier); + } + } + catch (Exception exception) + { + this.exception = exception; + } + + } + +} diff --git a/src/test/java/enedis/tic/core/TICCoreStreamMock.java b/src/test/java/enedis/tic/core/TICCoreStreamMock.java index 4606620..ce87b90 100644 --- a/src/test/java/enedis/tic/core/TICCoreStreamMock.java +++ b/src/test/java/enedis/tic/core/TICCoreStreamMock.java @@ -1,167 +1,98 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionaryException; - -public class TICCoreStreamMock implements TICCoreStream -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public static List streams = new ArrayList(); - public Collection subscribers; - public boolean running; - public TICIdentifier identifier; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreStreamMock(String portId, String portName, TICMode mode) throws DataDictionaryException - { - super(); - this.subscribers = new HashSet(); - this.running = false; - this.identifier = new TICIdentifier(portId, portName, null); - streams.add(this); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCoreStream - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICIdentifier getIdentifier() - { - return this.identifier; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Task - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public boolean isRunning() - { - return this.running; - } - - @Override - public void start() - { - this.running = true; - } - - @Override - public void stop() - { - this.running = false; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Notifier - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Collection getSubscribers() - { - return this.subscribers; - } - - @Override - public boolean hasSubscriber(TICCoreSubscriber subscriber) - { - return this.subscribers.contains(subscriber); - } - - @Override - public void subscribe(TICCoreSubscriber subscriber) - { - this.subscribers.add(subscriber); - } - - @Override - public void unsubscribe(TICCoreSubscriber subscriber) - { - this.subscribers.remove(subscriber); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public void notifyOnData(TICCoreFrame frame) - { - for (TICCoreSubscriber subscriber : this.subscribers) - { - subscriber.onData(frame); - } - } - - public void notifyOnError(TICCoreError error) - { - for (TICCoreSubscriber subscriber : this.subscribers) - { - subscriber.onError(error); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionaryException; + +public class TICCoreStreamMock implements TICCoreStream +{ + public static List streams = new ArrayList(); + public Collection subscribers; + public boolean running; + public TICIdentifier identifier; + + public TICCoreStreamMock(String portId, String portName, TICMode mode) throws DataDictionaryException + { + super(); + this.subscribers = new HashSet(); + this.running = false; + this.identifier = new TICIdentifier(portId, portName, null); + streams.add(this); + } + + @Override + public TICIdentifier getIdentifier() + { + return this.identifier; + } + + @Override + public boolean isRunning() + { + return this.running; + } + + @Override + public void start() + { + this.running = true; + } + + @Override + public void stop() + { + this.running = false; + } + + @Override + public Collection getSubscribers() + { + return this.subscribers; + } + + @Override + public boolean hasSubscriber(TICCoreSubscriber subscriber) + { + return this.subscribers.contains(subscriber); + } + + @Override + public void subscribe(TICCoreSubscriber subscriber) + { + this.subscribers.add(subscriber); + } + + @Override + public void unsubscribe(TICCoreSubscriber subscriber) + { + this.subscribers.remove(subscriber); + } + + public void notifyOnData(TICCoreFrame frame) + { + for (TICCoreSubscriber subscriber : this.subscribers) + { + subscriber.onData(frame); + } + } + + public void notifyOnError(TICCoreError error) + { + for (TICCoreSubscriber subscriber : this.subscribers) + { + subscriber.onError(error); + } + } + +} diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java b/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java index c826a11..6f4e0e9 100644 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java +++ b/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java @@ -1,85 +1,30 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.util.ArrayList; -import java.util.List; - -public class TICCoreSubscriberMock implements TICCoreSubscriber -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public List onDataCalls = new ArrayList(); - public List onErrorCalls = new ArrayList(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCoreSubscriber - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onData(TICCoreFrame frame) - { - this.onDataCalls.add(new TICCoreSubscriberOnDataCall(frame)); - } - - @Override - public void onError(TICCoreError error) - { - this.onErrorCalls.add(new TICCoreSubscriberOnErrorCall(error)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import java.util.ArrayList; +import java.util.List; + +public class TICCoreSubscriberMock implements TICCoreSubscriber +{ + public List onDataCalls = new ArrayList(); + public List onErrorCalls = new ArrayList(); + + @Override + public void onData(TICCoreFrame frame) + { + this.onDataCalls.add(new TICCoreSubscriberOnDataCall(frame)); + } + + @Override + public void onError(TICCoreError error) + { + this.onErrorCalls.add(new TICCoreSubscriberOnErrorCall(error)); + } + +} diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java b/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java index 55555a2..9341c46 100644 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java +++ b/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java @@ -1,78 +1,23 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.mock.FunctionCall; - -@SuppressWarnings("javadoc") -public class TICCoreSubscriberOnDataCall extends FunctionCall -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreFrame frame; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreSubscriberOnDataCall(TICCoreFrame frame) - { - super(); - this.frame = frame; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.mock.FunctionCall; + +@SuppressWarnings("javadoc") +public class TICCoreSubscriberOnDataCall extends FunctionCall +{ + public TICCoreFrame frame; + + public TICCoreSubscriberOnDataCall(TICCoreFrame frame) + { + super(); + this.frame = frame; + } + +} diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java b/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java index 862a016..92111fc 100644 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java +++ b/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java @@ -1,78 +1,23 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.mock.FunctionCall; - -@SuppressWarnings("javadoc") -public class TICCoreSubscriberOnErrorCall extends FunctionCall -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreError error; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreSubscriberOnErrorCall(TICCoreError error) - { - super(); - this.error = error; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.mock.FunctionCall; + +@SuppressWarnings("javadoc") +public class TICCoreSubscriberOnErrorCall extends FunctionCall +{ + public TICCoreError error; + + public TICCoreSubscriberOnErrorCall(TICCoreError error) + { + super(); + this.error = error; + } + +} From 6936121082051b662cdc909af6251c505a57c025 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Fri, 3 Oct 2025 15:44:40 +0200 Subject: [PATCH 21/59] chore: remove bloc comment sections (suite) --- .../lab/io/channels/ChannelConfiguration.java | 16 - .../enedis/lab/io/tic/TICPortDescriptor.java | 73 -- .../enedis/lab/io/tic/TICPortFinderBase.java | 73 -- .../lab/io/tic/TICPortPlugNotifier.java | 72 -- .../enedis/lab/io/usb/USBPortDescriptor.java | 73 -- .../enedis/lab/io/usb/USBPortFinderBase.java | 73 -- .../codec/CodecTICFrameHistoricDataSet.java | 349 +++--- .../tic/codec/CodecTICFrameStandard.java | 263 ++--- .../codec/CodecTICFrameStandardDataSet.java | 343 +++--- .../lab/protocol/tic/codec/TICCodec.java | 535 +++++----- .../tic/datastreams/TICInputStream.java | 422 ++++---- .../datastreams/TICStreamConfiguration.java | 385 +++---- .../lab/protocol/tic/frame/TICFrame.java | 990 +++++++++--------- .../protocol/tic/frame/TICFrameDataSet.java | 572 +++++----- .../tic/frame/historic/TICFrameHistoric.java | 55 - .../historic/TICFrameHistoricDataSet.java | 325 +++--- .../tic/frame/standard/TICFrameStandard.java | 55 - .../standard/TICFrameStandardDataSet.java | 649 ++++++------ 18 files changed, 2150 insertions(+), 3173 deletions(-) diff --git a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java b/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java index 6f916be..8543fe1 100644 --- a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java +++ b/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java @@ -261,14 +261,6 @@ public void setAlias(String alias) throws DataDictionaryException { this.setAlias((Object) alias); } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Internal method to set the channel name with object conversion. * @@ -309,14 +301,6 @@ protected void setAlias(Object alias) throws DataDictionaryException { this.data.put(KEY_ALIAS, this.kAlias.convert(alias)); } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Initializes and configures all key descriptors for parameter validation. * diff --git a/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java b/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java index 9b48ccc..99f5f82 100644 --- a/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java +++ b/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java @@ -23,52 +23,12 @@ *

    Generated */ public class TICPortDescriptor extends SerialPortDescriptor { - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected static final String KEY_MODEM_TYPE = "modemType"; - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private List> keys = new ArrayList>(); protected KeyDescriptorEnum kModemType; - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected TICPortDescriptor() { super(); this.loadKeyDescriptors(); @@ -181,15 +141,6 @@ public TICPortDescriptor(USBPortDescriptor usbPortDescriptor, TICModemType modem this.checkAndUpdate(); } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortDescriptor - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override protected void updateOptionalParameters() throws DataDictionaryException { if (this.exists(KEY_MODEM_TYPE)) { @@ -204,14 +155,6 @@ protected void updateOptionalParameters() throws DataDictionaryException { super.updateOptionalParameters(); } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Get modem type * @@ -238,14 +181,6 @@ public void setModemType(TICModemType modemType) throws DataDictionaryException } } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected void setModemType(Object modemType) throws DataDictionaryException { if (modemType == null) { this.data.put(KEY_MODEM_TYPE, null); @@ -254,14 +189,6 @@ protected void setModemType(Object modemType) throws DataDictionaryException { } } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void checkProductId(Number productId, TICModemType modemType) throws DataDictionaryException { if (modemType != null diff --git a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java b/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java index 463b845..a861ab3 100644 --- a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java +++ b/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java @@ -20,30 +20,6 @@ /** Class used to find all TIC port descriptor */ public class TICPortFinderBase implements TICPortFinder { - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Program writing the TIC port descriptor list (JSON format) on the output stream * @@ -68,27 +44,11 @@ public static TICPortFinderBase getInstance() { return instance; } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private static TICPortFinderBase instance; private SerialPortFinder serialPortFinder; private USBPortFinder usbPortFinder; - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private TICPortFinderBase() { this(SerialPortFinderBase.getInstance(), USBPortFinderBase.getInstance()); } @@ -110,15 +70,6 @@ public TICPortFinderBase(SerialPortFinder serialPortFinder, USBPortFinder usbPor this.usbPortFinder = usbPortFinder; } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICPortFinder - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public DataList findAll() { DataList ticSerialPort = new DataArrayList(); @@ -169,28 +120,4 @@ public TICPortDescriptor findNative(String portName) { return ticPortDescriptor; } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - } diff --git a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java b/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java index f5ffc49..04f8f3f 100644 --- a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java +++ b/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java @@ -13,32 +13,8 @@ /** Class used to notify when a TIC port has been plugged or unplugged */ public class TICPortPlugNotifier extends PortPlugNotifier { - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private static final int DEFAULT_JSON_INDENTATION = 2; - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Program writing on the output stream when an TIC port has been plugged or unplugged * @@ -69,22 +45,6 @@ public void onUnplugged(TICPortDescriptor descriptor) { PortPlugNotifier.main(notifier, subscriber); } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Constructor with default parameter * @@ -105,36 +65,4 @@ public TICPortPlugNotifier(long period, TICPortFinder finder) { super(period, finder); } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Runnable - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java b/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java index eac94dc..0c26440 100644 --- a/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java +++ b/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java @@ -23,14 +23,6 @@ *

    Generated */ public class USBPortDescriptor extends DataDictionaryBase { - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected static final String KEY_BCD_DEVICE = "bcdDevice"; protected static final String KEY_BCD_USB = "bcdUSB"; protected static final String KEY_B_DESCRIPTOR_TYPE = "bDescriptorType"; @@ -49,30 +41,6 @@ public class USBPortDescriptor extends DataDictionaryBase { protected static final String KEY_PRODUCT = "product"; protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private List> keys = new ArrayList>(); protected KeyDescriptorNumber kBcdDevice; @@ -93,14 +61,6 @@ public class USBPortDescriptor extends DataDictionaryBase { protected KeyDescriptorString kProduct; protected KeyDescriptorString kSerialNumber; - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected USBPortDescriptor() { super(); this.loadKeyDescriptors(); @@ -192,28 +152,11 @@ public USBPortDescriptor( this.checkAndUpdate(); } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override protected void updateOptionalParameters() throws DataDictionaryException { super.updateOptionalParameters(); } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Get bcd device * @@ -537,14 +480,6 @@ public void setSerialNumber(String serialNumber) throws DataDictionaryException this.setSerialNumber((Object) serialNumber); } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected void setBcdDevice(Object bcdDevice) throws DataDictionaryException { this.data.put(KEY_BCD_DEVICE, this.kBcdDevice.convert(bcdDevice)); } @@ -613,14 +548,6 @@ protected void setSerialNumber(Object serialNumber) throws DataDictionaryExcepti this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void loadKeyDescriptors() { try { this.kBcdDevice = new KeyDescriptorNumber(KEY_BCD_DEVICE, true); diff --git a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java b/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java index 0b1491a..e4fe0a9 100644 --- a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java +++ b/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java @@ -19,30 +19,6 @@ /** Class used to find all USB port descriptor */ public class USBPortFinderBase implements USBPortFinder { - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Program writing the USB port descriptor list (JSON format) on the output stream * @@ -67,35 +43,10 @@ public static USBPortFinderBase getInstance() { return instance; } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private static USBPortFinderBase instance; - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private USBPortFinderBase() {} - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// USBPortFinder - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public DataList findAll() { DataList usbPortList = new DataArrayList(); @@ -173,28 +124,4 @@ public DataList findAll() { return usbPortList; } - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// - // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - } diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java index c275a52..fa78822 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java @@ -1,198 +1,151 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import java.util.List; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; -import enedis.lab.types.BytesArray; - -/** - * Codec TIC Frame Historic Data Set - * - */ -public class CodecTICFrameHistoricDataSet implements Codec -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Codec - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public byte[] encode(TICFrameHistoricDataSet ticFrameHistoricDataSet) throws CodecException - { - BytesArray dataSet = new BytesArray(); - - if ((ticFrameHistoricDataSet.getLabel() != null) && (ticFrameHistoricDataSet.getData() != null)) - { - dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); - dataSet.addAll(ticFrameHistoricDataSet.getLabel().getBytes()); - - dataSet.add(TICFrameHistoricDataSet.SEPARATOR); - dataSet.addAll(ticFrameHistoricDataSet.getData().getBytes()); - - dataSet.add(TICFrameHistoricDataSet.SEPARATOR); - - if (!ticFrameHistoricDataSet.isValid()) - { - throw new CodecException("Invalid Checksum value"); - } - dataSet.add(ticFrameHistoricDataSet.getChecksum()); - dataSet.add(TICFrameDataSet.END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public TICFrameHistoricDataSet decode(byte[] bytes) throws CodecException - { - BytesArray bytesArray = new BytesArray(bytes); - TICFrameHistoricDataSet dataSet = null; - - if (this.isStartStopDelimiterPresent(bytesArray)) - { - this.removeStartStopDelimiter(bytesArray); - - } - - List parts = this.splitFrame(bytesArray); - - // Structure "LABEL/DATA/CHECKSUM" : - if (this.isStructureLabelDataChecksum(parts)) - { - - if (this.isChecksumInOneByte(parts.get(2))) - - { - dataSet = new TICFrameHistoricDataSet(); - dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); - - } - else - { - throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); - } - } - else - { - throw new CodecException("Invalid format of TICFrameHistoricDataSet"); - } - - if (dataSet.isValid() == false) - { - throw new CodecException("Invalid Checksum value - Historic"); - } - - return dataSet; - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private boolean isStartStopDelimiterPresent(BytesArray bytes) - { - return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); - } - - private void removeStartStopDelimiter(BytesArray bytes) - - { - bytes.remove(0); - bytes.remove(bytes.size() - 1); - } - - private TICFrameHistoricDataSet createDataSetLabelDataChecksum(List parts, TICFrameHistoricDataSet dataSet) - { - - dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(2).get(0)); - return dataSet; - } - - private boolean isChecksumInOneByte(BytesArray part) - { - return part.size() == 1; - } - - private boolean isStructureLabelDataChecksum(List parts) - { - return parts.size() == 3; - } - - private List splitFrame(BytesArray bytesArray) throws CodecException - { - List parts; - - if (bytesArray.size() < 5) - { - throw new CodecException("Not enough bytes in TICFrameHistoricDataSet"); - } - if (bytesArray.get(bytesArray.size() - 1) == TICFrameHistoricDataSet.SEPARATOR) - { - BytesArray subList = bytesArray.subList(0, bytesArray.size() - 2); - parts = subList.split(TICFrameHistoricDataSet.SEPARATOR); - if (parts.size() < 1) - { - throw new CodecException("Invalid format of TICFrameHistoricDataSet"); - } - BytesArray checksum = parts.get(parts.size() - 1); - checksum.addAll(new byte[] { TICFrameHistoricDataSet.SEPARATOR }); - } - else - { - parts = bytesArray.split(TICFrameHistoricDataSet.SEPARATOR); - } - - return parts; - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.codec; + +import java.util.List; + +import enedis.lab.codec.Codec; +import enedis.lab.codec.CodecException; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; +import enedis.lab.types.BytesArray; + +/** + * Codec TIC Frame Historic Data Set + * + */ +public class CodecTICFrameHistoricDataSet implements Codec +{ + + @Override + public byte[] encode(TICFrameHistoricDataSet ticFrameHistoricDataSet) throws CodecException + { + BytesArray dataSet = new BytesArray(); + + if ((ticFrameHistoricDataSet.getLabel() != null) && (ticFrameHistoricDataSet.getData() != null)) + { + dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); + dataSet.addAll(ticFrameHistoricDataSet.getLabel().getBytes()); + + dataSet.add(TICFrameHistoricDataSet.SEPARATOR); + dataSet.addAll(ticFrameHistoricDataSet.getData().getBytes()); + + dataSet.add(TICFrameHistoricDataSet.SEPARATOR); + + if (!ticFrameHistoricDataSet.isValid()) + { + throw new CodecException("Invalid Checksum value"); + } + dataSet.add(ticFrameHistoricDataSet.getChecksum()); + dataSet.add(TICFrameDataSet.END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public TICFrameHistoricDataSet decode(byte[] bytes) throws CodecException + { + BytesArray bytesArray = new BytesArray(bytes); + TICFrameHistoricDataSet dataSet = null; + + if (this.isStartStopDelimiterPresent(bytesArray)) + { + this.removeStartStopDelimiter(bytesArray); + + } + + List parts = this.splitFrame(bytesArray); + + // Structure "LABEL/DATA/CHECKSUM" : + if (this.isStructureLabelDataChecksum(parts)) + { + + if (this.isChecksumInOneByte(parts.get(2))) + + { + dataSet = new TICFrameHistoricDataSet(); + dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); + + } + else + { + throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); + } + } + else + { + throw new CodecException("Invalid format of TICFrameHistoricDataSet"); + } + + if (dataSet.isValid() == false) + { + throw new CodecException("Invalid Checksum value - Historic"); + } + + return dataSet; + } + + private boolean isStartStopDelimiterPresent(BytesArray bytes) + { + return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); + } + + private void removeStartStopDelimiter(BytesArray bytes) + + { + bytes.remove(0); + bytes.remove(bytes.size() - 1); + } + + private TICFrameHistoricDataSet createDataSetLabelDataChecksum(List parts, TICFrameHistoricDataSet dataSet) + { + + dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); + dataSet.setChecksum(parts.get(2).get(0)); + return dataSet; + } + + private boolean isChecksumInOneByte(BytesArray part) + { + return part.size() == 1; + } + + private boolean isStructureLabelDataChecksum(List parts) + { + return parts.size() == 3; + } + + private List splitFrame(BytesArray bytesArray) throws CodecException + { + List parts; + + if (bytesArray.size() < 5) + { + throw new CodecException("Not enough bytes in TICFrameHistoricDataSet"); + } + if (bytesArray.get(bytesArray.size() - 1) == TICFrameHistoricDataSet.SEPARATOR) + { + BytesArray subList = bytesArray.subList(0, bytesArray.size() - 2); + parts = subList.split(TICFrameHistoricDataSet.SEPARATOR); + if (parts.size() < 1) + { + throw new CodecException("Invalid format of TICFrameHistoricDataSet"); + } + BytesArray checksum = parts.get(parts.size() - 1); + checksum.addAll(new byte[] { TICFrameHistoricDataSet.SEPARATOR }); + } + else + { + parts = bytesArray.split(TICFrameHistoricDataSet.SEPARATOR); + } + + return parts; + } + +} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java index 3e98acb..8b9d987 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java @@ -1,156 +1,107 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import java.util.List; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; -import enedis.lab.types.BytesArray; - -/** - * Codec TIC Frame Standard - * - */ -public class CodecTICFrameStandard implements Codec -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Codec - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICFrameStandard decode(byte[] bytes) throws CodecException - { - String errorMessage = ""; - BytesArray rawDataSet = null; - TICFrameStandard ticFrame = null; - CodecTICFrameStandardDataSet codecTICFrameStandardDataSet = new CodecTICFrameStandardDataSet(); - - BytesArray bytesArray = new BytesArray(bytes); - - // Extract the body of the frame - // NB: the presence of an EOT character makes the content of a frame invalid - if ((bytesArray.startsWith(TICFrame.BEGINNING_PATTERN) == true) && (bytesArray.endsWith(TICFrame.END_PATTERN) == true) && (bytesArray.contains(TICFrame.EOT) == false)) - { - bytesArray.remove(0); - bytesArray.remove(bytesArray.size() - 1); - - ticFrame = new TICFrameStandard(); - - // Isolate each memory area supposed to correspond to an Information Group - // (DataSet) - List datasetList = bytesArray.slice(TICFrameDataSet.BEGINNING_PATTERN, TICFrameDataSet.END_PATTERN, BytesArray.CONTIGUOUS); - - // Analyze each memory area to extract the controls of the Group of information - // If the format of a zone is invalid, then the browse is interrupted and the whole frame is invalid - // (null) - if (datasetList.isEmpty() == false) - { - for (int i = 0; i < datasetList.size(); i++) - { - TICFrameStandardDataSet dataSet = null; - rawDataSet = datasetList.get(i); - byte[] rawDataSetByte = rawDataSet.getBytes(); - try - { - dataSet = codecTICFrameStandardDataSet.decode(rawDataSetByte); - ticFrame.addDataSet(dataSet); - } - catch (CodecException exception) - { - errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; - } - } - } - } - - if (errorMessage.isEmpty()) - { - return ticFrame; - } - else - { - throw new CodecException(errorMessage, ticFrame); - } - } - - @Override - public byte[] encode(TICFrameStandard ticFrameStandard) throws CodecException - { - CodecTICFrameStandardDataSet codec = new CodecTICFrameStandardDataSet(); - BytesArray dataSet = new BytesArray(); - - List ticFrameStandardList = ticFrameStandard.getDataSetList(); - - if (ticFrameStandard != null && !ticFrameStandardList.isEmpty()) - { - List groups = ticFrameStandard.getDataSetList(); - dataSet.add(TICFrame.BEGINNING_PATTERN); - for (int i = 0; i < groups.size(); i++) - { - byte[] buff = codec.encode((TICFrameStandardDataSet) groups.get(i)); - dataSet.addAll(buff); - } - dataSet.add(TICFrame.END_PATTERN); - return dataSet.getBytes(); - } - else - { - return null; - } - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.codec; + +import java.util.List; + +import enedis.lab.codec.Codec; +import enedis.lab.codec.CodecException; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; +import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; +import enedis.lab.types.BytesArray; + +/** + * Codec TIC Frame Standard + * + */ +public class CodecTICFrameStandard implements Codec +{ + @Override + public TICFrameStandard decode(byte[] bytes) throws CodecException + { + String errorMessage = ""; + BytesArray rawDataSet = null; + TICFrameStandard ticFrame = null; + CodecTICFrameStandardDataSet codecTICFrameStandardDataSet = new CodecTICFrameStandardDataSet(); + + BytesArray bytesArray = new BytesArray(bytes); + + // Extract the body of the frame + // NB: the presence of an EOT character makes the content of a frame invalid + if ((bytesArray.startsWith(TICFrame.BEGINNING_PATTERN) == true) && (bytesArray.endsWith(TICFrame.END_PATTERN) == true) && (bytesArray.contains(TICFrame.EOT) == false)) + { + bytesArray.remove(0); + bytesArray.remove(bytesArray.size() - 1); + + ticFrame = new TICFrameStandard(); + + // Isolate each memory area supposed to correspond to an Information Group + // (DataSet) + List datasetList = bytesArray.slice(TICFrameDataSet.BEGINNING_PATTERN, TICFrameDataSet.END_PATTERN, BytesArray.CONTIGUOUS); + + // Analyze each memory area to extract the controls of the Group of information + // If the format of a zone is invalid, then the browse is interrupted and the whole frame is invalid + // (null) + if (datasetList.isEmpty() == false) + { + for (int i = 0; i < datasetList.size(); i++) + { + TICFrameStandardDataSet dataSet = null; + rawDataSet = datasetList.get(i); + byte[] rawDataSetByte = rawDataSet.getBytes(); + try + { + dataSet = codecTICFrameStandardDataSet.decode(rawDataSetByte); + ticFrame.addDataSet(dataSet); + } + catch (CodecException exception) + { + errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; + } + } + } + } + + if (errorMessage.isEmpty()) + { + return ticFrame; + } + else + { + throw new CodecException(errorMessage, ticFrame); + } + } + + @Override + public byte[] encode(TICFrameStandard ticFrameStandard) throws CodecException + { + CodecTICFrameStandardDataSet codec = new CodecTICFrameStandardDataSet(); + BytesArray dataSet = new BytesArray(); + + List ticFrameStandardList = ticFrameStandard.getDataSetList(); + + if (ticFrameStandard != null && !ticFrameStandardList.isEmpty()) + { + List groups = ticFrameStandard.getDataSetList(); + dataSet.add(TICFrame.BEGINNING_PATTERN); + for (int i = 0; i < groups.size(); i++) + { + byte[] buff = codec.encode((TICFrameStandardDataSet) groups.get(i)); + dataSet.addAll(buff); + } + dataSet.add(TICFrame.END_PATTERN); + return dataSet.getBytes(); + } + else + { + return null; + } + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java index 9e89e85..dc06499 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java @@ -1,197 +1,148 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import java.util.List; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; -import enedis.lab.types.BytesArray; - -/** - * Codec TIC Frame Standard Data Set - * - */ -public class CodecTICFrameStandardDataSet implements Codec -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Codec - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public byte[] encode(TICFrameStandardDataSet ticFrameStandardDataSet) throws CodecException - { - BytesArray dataSet = new BytesArray(); - - if ((null != ticFrameStandardDataSet.getLabel()) && (null != ticFrameStandardDataSet.getData())) - { - dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); - dataSet.addAll(ticFrameStandardDataSet.getLabel().getBytes()); - - if (ticFrameStandardDataSet.checkDateTime() == true) - { - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - dataSet.addAll(ticFrameStandardDataSet.getDateTime().getBytes()); - } - - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - dataSet.addAll(ticFrameStandardDataSet.getData().getBytes()); - - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - if (!ticFrameStandardDataSet.isValid()) - { - throw new CodecException("Invalid Checksum value"); - } - - dataSet.add(ticFrameStandardDataSet.getChecksum()); - dataSet.add(TICFrameDataSet.END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public TICFrameStandardDataSet decode(byte[] bytes) throws CodecException - { - BytesArray bytesArray = new BytesArray(bytes); - TICFrameStandardDataSet dataSet = null; - - if (this.isStartStopDelimiterPresent(bytesArray)) - { - this.removeStartStopDelimiter(bytesArray); - } - - List parts = bytesArray.split(TICFrameStandardDataSet.SEPARATOR); - - // Structure "LABEL/DATA/CHECKSUM" : - if (this.isStructureLabelDataChecksum(parts)) - { - if (this.isChecksumInOneByte(parts.get(2))) - { - dataSet = new TICFrameStandardDataSet(); - dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); - } - else - { - throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); - } - } - // Structure "LABEL/DATETIME/DATA/CHECKSUM" : - else if (this.isStructureLabelDateTimeDataChecksum(parts)) - { - if (this.isChecksumInOneByte(parts.get(3))) - { - dataSet = new TICFrameStandardDataSet(); - dataSet = this.createDataSetLabelDatetimeDataChecksum(parts, dataSet); - } - else - { - throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); - } - } - else - { - throw new CodecException("Invalid bytes for decode - Standard"); - } - - if (dataSet.isValid() == false) - { - throw new CodecException("Invalid Checksum value"); - } - - return dataSet; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private boolean isStructureLabelDateTimeDataChecksum(List parts) - { - return parts.size() == 4; - } - - private boolean isStructureLabelDataChecksum(List parts) - { - return parts.size() == 3; - } - - private boolean isChecksumInOneByte(BytesArray part) - { - return part.size() == 1; - } - - private void removeStartStopDelimiter(BytesArray bytes) - { - bytes.remove(0); - bytes.remove(bytes.size() - 1); - } - - private boolean isStartStopDelimiterPresent(BytesArray bytes) - { - return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); - } - - private TICFrameStandardDataSet createDataSetLabelDataChecksum(List parts, TICFrameStandardDataSet dataSet) - { - dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(2).get(0)); - return dataSet; - } - - private TICFrameStandardDataSet createDataSetLabelDatetimeDataChecksum(List parts, TICFrameStandardDataSet dataSet) - { - dataSet.setup(parts.get(0).getBytes(), parts.get(2).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(3).get(0)); - return dataSet; - } - +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.codec; + +import java.util.List; + +import enedis.lab.codec.Codec; +import enedis.lab.codec.CodecException; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; +import enedis.lab.types.BytesArray; + +/** + * Codec TIC Frame Standard Data Set + * + */ +public class CodecTICFrameStandardDataSet implements Codec +{ + @Override + public byte[] encode(TICFrameStandardDataSet ticFrameStandardDataSet) throws CodecException + { + BytesArray dataSet = new BytesArray(); + + if ((null != ticFrameStandardDataSet.getLabel()) && (null != ticFrameStandardDataSet.getData())) + { + dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); + dataSet.addAll(ticFrameStandardDataSet.getLabel().getBytes()); + + if (ticFrameStandardDataSet.checkDateTime() == true) + { + dataSet.add(TICFrameStandardDataSet.SEPARATOR); + dataSet.addAll(ticFrameStandardDataSet.getDateTime().getBytes()); + } + + dataSet.add(TICFrameStandardDataSet.SEPARATOR); + dataSet.addAll(ticFrameStandardDataSet.getData().getBytes()); + + dataSet.add(TICFrameStandardDataSet.SEPARATOR); + if (!ticFrameStandardDataSet.isValid()) + { + throw new CodecException("Invalid Checksum value"); + } + + dataSet.add(ticFrameStandardDataSet.getChecksum()); + dataSet.add(TICFrameDataSet.END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public TICFrameStandardDataSet decode(byte[] bytes) throws CodecException + { + BytesArray bytesArray = new BytesArray(bytes); + TICFrameStandardDataSet dataSet = null; + + if (this.isStartStopDelimiterPresent(bytesArray)) + { + this.removeStartStopDelimiter(bytesArray); + } + + List parts = bytesArray.split(TICFrameStandardDataSet.SEPARATOR); + + // Structure "LABEL/DATA/CHECKSUM" : + if (this.isStructureLabelDataChecksum(parts)) + { + if (this.isChecksumInOneByte(parts.get(2))) + { + dataSet = new TICFrameStandardDataSet(); + dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); + } + else + { + throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); + } + } + // Structure "LABEL/DATETIME/DATA/CHECKSUM" : + else if (this.isStructureLabelDateTimeDataChecksum(parts)) + { + if (this.isChecksumInOneByte(parts.get(3))) + { + dataSet = new TICFrameStandardDataSet(); + dataSet = this.createDataSetLabelDatetimeDataChecksum(parts, dataSet); + } + else + { + throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); + } + } + else + { + throw new CodecException("Invalid bytes for decode - Standard"); + } + + if (dataSet.isValid() == false) + { + throw new CodecException("Invalid Checksum value"); + } + + return dataSet; + } + + private boolean isStructureLabelDateTimeDataChecksum(List parts) + { + return parts.size() == 4; + } + + private boolean isStructureLabelDataChecksum(List parts) + { + return parts.size() == 3; + } + + private boolean isChecksumInOneByte(BytesArray part) + { + return part.size() == 1; + } + + private void removeStartStopDelimiter(BytesArray bytes) + { + bytes.remove(0); + bytes.remove(bytes.size() - 1); + } + + private boolean isStartStopDelimiterPresent(BytesArray bytes) + { + return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); + } + + private TICFrameStandardDataSet createDataSetLabelDataChecksum(List parts, TICFrameStandardDataSet dataSet) + { + dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); + dataSet.setChecksum(parts.get(2).get(0)); + return dataSet; + } + + private TICFrameStandardDataSet createDataSetLabelDatetimeDataChecksum(List parts, TICFrameStandardDataSet dataSet) + { + dataSet.setup(parts.get(0).getBytes(), parts.get(2).getBytes(), parts.get(1).getBytes()); + dataSet.setChecksum(parts.get(3).get(0)); + return dataSet; + } + } \ No newline at end of file diff --git a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java index 2649dfb..ed28431 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java @@ -1,295 +1,240 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; -import enedis.lab.protocol.tic.frame.standard.TICException; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; -import enedis.lab.types.BytesArray; - -/** - * Codec TIC - */ -public class TICCodec implements Codec -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final TICMode DEFAULT_MODE = TICMode.STANDARD; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private BytesArray Buffer; - private TICMode mode = TICMode.UNKNOWN; - private TICMode currentMode = TICMode.UNKNOWN; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TICCodec() - { - this.mode = TICMode.UNKNOWN; - this.currentMode = TICMode.UNKNOWN; - this.Buffer = new BytesArray(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Codec - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICFrame decode(byte[] newData) throws CodecException - { - CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); - CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); - TICFrameStandard ticFrameStandard = null; - TICFrameHistoric ticFrameHistoric = null; - - TICFrame ticFrame = null; - TICMode currentTICMode = null; - - try - { - switch (this.currentMode) - { - case STANDARD: - { - ticFrameStandard = codecStandard.decode(newData); - ticFrame = ticFrameStandard; - break; - } - case HISTORIC: - { - ticFrameHistoric = codecHistoric.decode(newData); - ticFrame = ticFrameHistoric; - break; - } - - case AUTO: - { - try - { - currentTICMode = TICMode.findModeFromFrameBuffer(newData); - } - catch (TICException exception) - { - throw new CodecException("can't determinated TIC Mode"); - } - - if (currentTICMode == TICMode.STANDARD) - { - ticFrameStandard = codecStandard.decode(newData); - ticFrame = ticFrameStandard; - break; - } - else if (currentTICMode == TICMode.HISTORIC) - { - ticFrameHistoric = codecHistoric.decode(newData); - ticFrame = ticFrameHistoric; - } - else - { - throw new CodecException("can't decode TIC, unable to find TIC Modem"); - } - - } - - default: - { - /**/ - } - } - } - catch (CodecException exception) - { - throw new CodecException(exception.getMessage(), exception.getData()); - } - return ticFrame; - } - - @Override - public byte[] encode(TICFrame ticFrame) throws CodecException - { - CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); - CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); - - byte[] bytesBuffer = new byte[0]; - - try - { - switch (ticFrame.getMode()) - { - case STANDARD: - { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } - case HISTORIC: - { - bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); - break; - } - default: - { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } - } - } - catch (CodecException exception) - { - throw new CodecException("Can't encode TICFrame" + exception.getMessage()); - } - return bytesBuffer; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Reset buffer - */ - public void reset() - { - this.Buffer.clear(); - } - - /** - * Append data to buffer - * - * @param data - */ - public void append(byte[] data) - { - this.Buffer.addAll(data); - } - - /** - * Append data to buffer - * - * @param data - */ - public void append(byte data) - { - this.Buffer.add(data); - } - - /** - * Append data to buffer - * - * @param data - */ - public void append(BytesArray data) - { - this.Buffer.addAll(data.getBytes()); - } - - /** - * Get mode - * - * @return mode - */ - public TICMode getMode() - { - return this.mode; - } - - /** - * Set mode - * - * @param mode - */ - public void setMode(TICMode mode) - { - if (this.mode != mode) - { - this.mode = mode; - - if (TICMode.AUTO != mode) - { - this.setCurrentMode(mode); - } - - else - { - this.setCurrentMode(DEFAULT_MODE); - } - } - } - - /** - * Get current mode - * - * @return current mode - */ - public TICMode getCurrentMode() - { - return this.currentMode; - } - - /** - * Set current mode - * - * @param currentMode - */ - public void setCurrentMode(TICMode currentMode) - { - if (currentMode != this.currentMode) - { - this.currentMode = currentMode; - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.codec; + +import enedis.lab.codec.Codec; +import enedis.lab.codec.CodecException; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; +import enedis.lab.protocol.tic.frame.standard.TICException; +import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; +import enedis.lab.types.BytesArray; + +/** + * Codec TIC + */ +public class TICCodec implements Codec +{ + private static final TICMode DEFAULT_MODE = TICMode.STANDARD; + + private BytesArray Buffer; + private TICMode mode = TICMode.UNKNOWN; + private TICMode currentMode = TICMode.UNKNOWN; + + /** + * Default constructor + */ + public TICCodec() + { + this.mode = TICMode.UNKNOWN; + this.currentMode = TICMode.UNKNOWN; + this.Buffer = new BytesArray(); + } + + @Override + public TICFrame decode(byte[] newData) throws CodecException + { + CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); + CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); + TICFrameStandard ticFrameStandard = null; + TICFrameHistoric ticFrameHistoric = null; + + TICFrame ticFrame = null; + TICMode currentTICMode = null; + + try + { + switch (this.currentMode) + { + case STANDARD: + { + ticFrameStandard = codecStandard.decode(newData); + ticFrame = ticFrameStandard; + break; + } + case HISTORIC: + { + ticFrameHistoric = codecHistoric.decode(newData); + ticFrame = ticFrameHistoric; + break; + } + + case AUTO: + { + try + { + currentTICMode = TICMode.findModeFromFrameBuffer(newData); + } + catch (TICException exception) + { + throw new CodecException("can't determinated TIC Mode"); + } + + if (currentTICMode == TICMode.STANDARD) + { + ticFrameStandard = codecStandard.decode(newData); + ticFrame = ticFrameStandard; + break; + } + else if (currentTICMode == TICMode.HISTORIC) + { + ticFrameHistoric = codecHistoric.decode(newData); + ticFrame = ticFrameHistoric; + } + else + { + throw new CodecException("can't decode TIC, unable to find TIC Modem"); + } + + } + + default: + { + /**/ + } + } + } + catch (CodecException exception) + { + throw new CodecException(exception.getMessage(), exception.getData()); + } + return ticFrame; + } + + @Override + public byte[] encode(TICFrame ticFrame) throws CodecException + { + CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); + CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); + + byte[] bytesBuffer = new byte[0]; + + try + { + switch (ticFrame.getMode()) + { + case STANDARD: + { + bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); + break; + } + case HISTORIC: + { + bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); + break; + } + default: + { + bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); + break; + } + } + } + catch (CodecException exception) + { + throw new CodecException("Can't encode TICFrame" + exception.getMessage()); + } + return bytesBuffer; + } + + /** + * Reset buffer + */ + public void reset() + { + this.Buffer.clear(); + } + + /** + * Append data to buffer + * + * @param data + */ + public void append(byte[] data) + { + this.Buffer.addAll(data); + } + + /** + * Append data to buffer + * + * @param data + */ + public void append(byte data) + { + this.Buffer.add(data); + } + + /** + * Append data to buffer + * + * @param data + */ + public void append(BytesArray data) + { + this.Buffer.addAll(data.getBytes()); + } + + /** + * Get mode + * + * @return mode + */ + public TICMode getMode() + { + return this.mode; + } + + /** + * Set mode + * + * @param mode + */ + public void setMode(TICMode mode) + { + if (this.mode != mode) + { + this.mode = mode; + + if (TICMode.AUTO != mode) + { + this.setCurrentMode(mode); + } + + else + { + this.setCurrentMode(DEFAULT_MODE); + } + } + } + + /** + * Get current mode + * + * @return current mode + */ + public TICMode getCurrentMode() + { + return this.currentMode; + } + + /** + * Set current mode + * + * @param currentMode + */ + public void setCurrentMode(TICMode currentMode) + { + if (currentMode != this.currentMode) + { + this.currentMode = currentMode; + } + } + +} diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java index ec4ebf3..abf35e0 100644 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java +++ b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java @@ -1,241 +1,181 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.datastreams; - -import enedis.lab.codec.CodecException; -import enedis.lab.io.datastreams.DataInputStream; -import enedis.lab.io.datastreams.DataStreamException; -import enedis.lab.io.datastreams.DataStreamType; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.channels.ChannelTICSerialPort; -import enedis.lab.protocol.tic.codec.TICCodec; -import enedis.lab.protocol.tic.frame.TICError; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; - -/** - * TIC input stream - */ -public class TICInputStream extends DataInputStream -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Key timestamp */ - public static final String KEY_TIMESTAMP = "timestamp"; - /** Key channel */ - public static final String KEY_CHANNEL = "channel"; - /** Key data */ - public static final String KEY_DATA = "data"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected TICCodec codec; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor using configuration - * - * @param configuration - * @throws DataStreamException - */ - public TICInputStream(TICStreamConfiguration configuration) throws DataStreamException - { - super(configuration); - this.codec = new TICCodec(); - this.codec.setCurrentMode(configuration.getTicMode()); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataInputStream - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataDictionary read() throws DataStreamException - { - return null; - } - - @Override - public DataStreamType getType() - { - return DataStreamType.TIC; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ChannelListener - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onDataRead(String channelName, byte[] data) - { - if (!this.notifier.getSubscribers().isEmpty()) - { - DataDictionary ticFrame = null; - try - { - ticFrame = this.decodeTICFrame(data); - if (ticFrame != null) - { - this.notifyOnDataReceived(ticFrame); - } - } - catch (DataDictionaryException exception) - { - this.logger.error(exception.getMessage(), exception); - } - catch (CodecException exception) - { - DataDictionaryBase errorDataDictionary = new DataDictionaryBase(); - - String KEY_PARTIAL_FRAME = "partialTICFrame"; - - errorDataDictionary.addKey(KEY_PARTIAL_FRAME); - - try - { - Object exceptionData = exception.getData(); - if (exceptionData != null && exceptionData instanceof TICFrame) - { - try - { - DataDictionary frameDataDictionary = ((TICFrame) exceptionData).getDataDictionary(); - if (frameDataDictionary != null) - { - errorDataDictionary.set(KEY_PARTIAL_FRAME, frameDataDictionary); - } - else - { - this.logger.warn("Frame data dictionary is null"); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } - catch (DataDictionaryException frameDataDictionaryException) - { - this.logger.warn("Can't get TICFrame data dictionary: " + frameDataDictionaryException.getMessage()); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } - else - { - this.logger.error("No frame data available"); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } - catch (DataDictionaryException dataDictionaryException) - { - this.logger.error("Can't convert TICFrame to DataDictonary" + dataDictionaryException.getMessage()); - } - - this.logger.error(exception.getMessage() + errorDataDictionary.toString()); - this.onErrorDetected(channelName, TICError.TIC_READER_READ_FRAME_CHECKSUM_INVALID.getValue(), exception.getMessage(), errorDataDictionary); - } - } - } - - @Override - public void onDataWritten(String channelName, byte[] data) - { - // Not used - } - - @Override - public void onErrorDetected(String channelName, int errorCode, String errorMessage) - { - this.notifyOnErrorDetected(errorCode, errorMessage, null); - } - - @Override - public void onErrorDetected(String channelName, int errorCode, String errorMessage, DataDictionary errorData) - { - this.notifyOnErrorDetected(errorCode, errorMessage, errorData); - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get Mode - * - * @return TIC Mode - */ - public TICMode getMode() - { - ChannelTICSerialPort channelTIC = (ChannelTICSerialPort) this.channel; - - return channelTIC.getMode(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected DataDictionary decodeTICFrame(byte[] data) throws DataDictionaryException, CodecException - { - TICFrame ticFrame; - try - { - ticFrame = this.codec.decode(data); - } - catch (CodecException exception) - { - throw new CodecException(exception.getMessage(), exception.getData()); - } - - long timestamp = System.currentTimeMillis(); - - DataDictionaryBase decodedTICFrame = new DataDictionaryBase(); - - decodedTICFrame.set(KEY_TIMESTAMP, timestamp); - decodedTICFrame.set(KEY_CHANNEL, this.getConfiguration().getChannelName()); - decodedTICFrame.set(KEY_DATA, ticFrame); - - return decodedTICFrame; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.datastreams; + +import enedis.lab.codec.CodecException; +import enedis.lab.io.datastreams.DataInputStream; +import enedis.lab.io.datastreams.DataStreamException; +import enedis.lab.io.datastreams.DataStreamType; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.protocol.tic.channels.ChannelTICSerialPort; +import enedis.lab.protocol.tic.codec.TICCodec; +import enedis.lab.protocol.tic.frame.TICError; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; + +/** + * TIC input stream + */ +public class TICInputStream extends DataInputStream +{ + /** Key timestamp */ + public static final String KEY_TIMESTAMP = "timestamp"; + /** Key channel */ + public static final String KEY_CHANNEL = "channel"; + /** Key data */ + public static final String KEY_DATA = "data"; + + protected TICCodec codec; + + /** + * Constructor using configuration + * + * @param configuration + * @throws DataStreamException + */ + public TICInputStream(TICStreamConfiguration configuration) throws DataStreamException + { + super(configuration); + this.codec = new TICCodec(); + this.codec.setCurrentMode(configuration.getTicMode()); + } + + @Override + public DataDictionary read() throws DataStreamException + { + return null; + } + + @Override + public DataStreamType getType() + { + return DataStreamType.TIC; + } + + @Override + public void onDataRead(String channelName, byte[] data) + { + if (!this.notifier.getSubscribers().isEmpty()) + { + DataDictionary ticFrame = null; + try + { + ticFrame = this.decodeTICFrame(data); + if (ticFrame != null) + { + this.notifyOnDataReceived(ticFrame); + } + } + catch (DataDictionaryException exception) + { + this.logger.error(exception.getMessage(), exception); + } + catch (CodecException exception) + { + DataDictionaryBase errorDataDictionary = new DataDictionaryBase(); + + String KEY_PARTIAL_FRAME = "partialTICFrame"; + + errorDataDictionary.addKey(KEY_PARTIAL_FRAME); + + try + { + Object exceptionData = exception.getData(); + if (exceptionData != null && exceptionData instanceof TICFrame) + { + try + { + DataDictionary frameDataDictionary = ((TICFrame) exceptionData).getDataDictionary(); + if (frameDataDictionary != null) + { + errorDataDictionary.set(KEY_PARTIAL_FRAME, frameDataDictionary); + } + else + { + this.logger.warn("Frame data dictionary is null"); + errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); + } + } + catch (DataDictionaryException frameDataDictionaryException) + { + this.logger.warn("Can't get TICFrame data dictionary: " + frameDataDictionaryException.getMessage()); + errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); + } + } + else + { + this.logger.error("No frame data available"); + errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); + } + } + catch (DataDictionaryException dataDictionaryException) + { + this.logger.error("Can't convert TICFrame to DataDictonary" + dataDictionaryException.getMessage()); + } + + this.logger.error(exception.getMessage() + errorDataDictionary.toString()); + this.onErrorDetected(channelName, TICError.TIC_READER_READ_FRAME_CHECKSUM_INVALID.getValue(), exception.getMessage(), errorDataDictionary); + } + } + } + + @Override + public void onDataWritten(String channelName, byte[] data) + { + // Not used + } + + @Override + public void onErrorDetected(String channelName, int errorCode, String errorMessage) + { + this.notifyOnErrorDetected(errorCode, errorMessage, null); + } + + @Override + public void onErrorDetected(String channelName, int errorCode, String errorMessage, DataDictionary errorData) + { + this.notifyOnErrorDetected(errorCode, errorMessage, errorData); + } + /** + * Get Mode + * + * @return TIC Mode + */ + public TICMode getMode() + { + ChannelTICSerialPort channelTIC = (ChannelTICSerialPort) this.channel; + + return channelTIC.getMode(); + } + + protected DataDictionary decodeTICFrame(byte[] data) throws DataDictionaryException, CodecException + { + TICFrame ticFrame; + try + { + ticFrame = this.codec.decode(data); + } + catch (CodecException exception) + { + throw new CodecException(exception.getMessage(), exception.getData()); + } + + long timestamp = System.currentTimeMillis(); + + DataDictionaryBase decodedTICFrame = new DataDictionaryBase(); + + decodedTICFrame.set(KEY_TIMESTAMP, timestamp); + decodedTICFrame.set(KEY_CHANNEL, this.getConfiguration().getChannelName()); + decodedTICFrame.set(KEY_DATA, ticFrame); + + return decodedTICFrame; + } + +} diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java index 4003b4b..04abc64 100644 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java +++ b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java @@ -1,220 +1,165 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.datastreams; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.io.datastreams.DataStreamConfiguration; -import enedis.lab.io.datastreams.DataStreamDirection; -import enedis.lab.io.datastreams.DataStreamType; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; - -/** - * TICStreamConfiguration class - * - * Generated - */ -public class TICStreamConfiguration extends DataStreamConfiguration -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_TIC_MODE = "ticMode"; - - private static final DataStreamType TYPE_ACCEPTED_VALUE = DataStreamType.TIC; - private static final DataStreamDirection[] DIRECTION_ACCEPTED_VALUES = { DataStreamDirection.OUTPUT, DataStreamDirection.INPUT }; - private static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kTicMode; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected TICStreamConfiguration() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUES); - this.kChannelName.setMandatory(true); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICStreamConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICStreamConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public TICStreamConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param direction - * @param channelName - * @param ticMode - * @throws DataDictionaryException - */ - public TICStreamConfiguration(String name, DataStreamDirection direction, String channelName, TICMode ticMode) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setDirection(direction); - this.setChannelName(channelName); - this.setTicMode(ticMode); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataStreamConfiguration - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - if (!this.exists(KEY_TIC_MODE)) - { - this.setTicMode(TIC_MODE_DEFAULT_VALUE); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get tic mode - * - * @return the tic mode - */ - public TICMode getTicMode() - { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Set tic mode - * - * @param ticMode - * @throws DataDictionaryException - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException - { - this.setTicMode((Object) ticMode); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setTicMode(Object ticMode) throws DataDictionaryException - { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); - this.keys.add(this.kTicMode); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.datastreams; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.io.datastreams.DataStreamConfiguration; +import enedis.lab.io.datastreams.DataStreamDirection; +import enedis.lab.io.datastreams.DataStreamType; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; + +/** + * TICStreamConfiguration class + * + * Generated + */ +public class TICStreamConfiguration extends DataStreamConfiguration +{ + protected static final String KEY_TIC_MODE = "ticMode"; + + private static final DataStreamType TYPE_ACCEPTED_VALUE = DataStreamType.TIC; + private static final DataStreamDirection[] DIRECTION_ACCEPTED_VALUES = { DataStreamDirection.OUTPUT, DataStreamDirection.INPUT }; + private static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kTicMode; + + protected TICStreamConfiguration() + { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUES); + this.kChannelName.setMandatory(true); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICStreamConfiguration(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICStreamConfiguration(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * + * Constructor setting configuration name/file and parameters to default values + * + * @param name + * the configuration name + * @param file + * the configuration file + */ + public TICStreamConfiguration(String name, File file) + { + this(); + this.init(name, file); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param direction + * @param channelName + * @param ticMode + * @throws DataDictionaryException + */ + public TICStreamConfiguration(String name, DataStreamDirection direction, String channelName, TICMode ticMode) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setDirection(direction); + this.setChannelName(channelName); + this.setTicMode(ticMode); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_TYPE)) + { + this.setType(TYPE_ACCEPTED_VALUE); + } + if (!this.exists(KEY_TIC_MODE)) + { + this.setTicMode(TIC_MODE_DEFAULT_VALUE); + } + super.updateOptionalParameters(); + } + + /** + * Get tic mode + * + * @return the tic mode + */ + public TICMode getTicMode() + { + return (TICMode) this.data.get(KEY_TIC_MODE); + } + + /** + * Set tic mode + * + * @param ticMode + * @throws DataDictionaryException + */ + public void setTicMode(TICMode ticMode) throws DataDictionaryException + { + this.setTicMode((Object) ticMode); + } + + protected void setTicMode(Object ticMode) throws DataDictionaryException + { + this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); + } + + private void loadKeyDescriptors() + { + try + { + this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); + this.keys.add(this.kTicMode); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java index cd89e18..d39dd68 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java @@ -1,522 +1,468 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import org.json.JSONObject; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.BytesArray; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; - -/** - * TICFrame - */ -public abstract class TICFrame -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Beginning pattern */ - public static final byte BEGINNING_PATTERN = 0x02; // STX - /** End pattern */ - public static final byte END_PATTERN = 0x03; // ETX - /** End of trame pattern */ - public static final byte EOT = 0x04; // EOT - - // Options - /** No specific options */ - public static final int EXPLICIT = 0x0000; - /** No checksum options */ - public static final int NOCHECKSUM = 0x0001; - /** No date time options */ - public static final int NODATETIME = 0x0002; - /** No tic mode options */ - public static final int NOTICMODE = 0x0004; - /** No invalid options */ - public static final int NOINVALID = 0x0008; - /** Trimmed options */ - public static final int TRIMMED = NOCHECKSUM | NODATETIME | NOINVALID; - - /** Key tic frame */ - public static final String KEY_TICFRAME = "ticFrame"; - /** Key tic mode */ - public static final String KEY_TICMODE = "ticMode"; - /** Key label */ - public static final String KEY_LABEL = "label"; - /** Key data */ - public static final String KEY_DATA = "data"; - /** Key checksum */ - public static final String KEY_CHECKSUM = "checksum"; - /** Key date time */ - public static final String KEY_DATETIME = "dateTime"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected List DataSetList; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TICFrame() - { - this.DataSetList = new ArrayList(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Object - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public int hashCode() - { - return Objects.hash(this.DataSetList); - } - - @Override - public boolean equals(Object obj) - { - if (this == obj) - return true; - if (obj == null) - return false; - if (this.getClass() != obj.getClass()) - return false; - TICFrame other = (TICFrame) obj; - return Objects.equals(this.DataSetList, other.DataSetList); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Return the list of DataSet - * - * @return the list of DataSet - */ - public List getDataSetList() - { - return this.DataSetList; - } - - /** - * Set the list of DataSet - * - * @param dataSetList - */ - public void setDataSetList(List dataSetList) - { - this.DataSetList = dataSetList; - } - - /** - * Return the index of the given DataSet - * - * @param label - * Label of the DataSet - * @return the index of the given DataSet or '-1' if it does not exist - */ - public int indexOf(String label) - { - int index = -1; - - for (int i = 0; i < this.DataSetList.size(); i++) - { - if (this.DataSetList.get(i).getLabel().equals(label)) - { - index = i; - break; - } - } - - return index; - } - - /** - * Return the given DataSet - * - * @param label - * Label of the DataSet - * @return the given DataSet or 'null' if it does not exist - */ - public TICFrameDataSet getDataSet(String label) - { - TICFrameDataSet dataSet = null; - - for (int i = 0; i < this.DataSetList.size(); i++) - { - if (this.DataSetList.get(i).getLabel().equals(label)) - { - dataSet = this.DataSetList.get(i); - break; - } - } - - return dataSet; - } - - /** - * Return the data field of the given DataSet - * - * @param label - * Label of the DataSet - * @return the data field of the given DataSet - */ - public String getData(String label) - { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) - { - return dataSet.getData(); - } - - else - { - return null; - } - } - - /** - * Add the dataSet at the end of the list. If the dataSet already existed, its data and checksum are just set - * - * @param dataSet - */ - public void addDataSet(TICFrameDataSet dataSet) - { - TICFrameDataSet oldDataSet = this.getDataSet(dataSet.getLabel()); - - if (oldDataSet == null) - { - this.DataSetList.add(dataSet); - } - - else - { - oldDataSet.setData(dataSet.getData()); - oldDataSet.setChecksum(dataSet.getChecksum()); - } - } - - /** - * Add the dataSet at the given index of the list. If the dataSet already existed, its data and checksum are set and - * it is moved at the given index - * - * @param index - * @param dataSet - */ - public void addDataSet(int index, TICFrameDataSet dataSet) - { - int dataSetIndex = this.indexOf(dataSet.getLabel()); - - if (dataSetIndex < 0) - { - this.DataSetList.add(index, dataSet); - } - - else - { - if (dataSetIndex != index) - { - this.removeDataSet(dataSetIndex); - this.DataSetList.add(index, dataSet); - } - - else - { - this.DataSetList.get(index).setData(dataSet.getData()); - this.DataSetList.get(index).setChecksum(dataSet.getChecksum()); - } - } - } - - /** - * Add data set - * - * @param label - * @param data - * @return tic frame data set - */ - public abstract TICFrameDataSet addDataSet(String label, String data); - - /** - * Add data set - * - * @param index - * @param label - * @param data - * @return tic frame data set - */ - public abstract TICFrameDataSet addDataSet(int index, String label, String data); - - /** - * Move the DataSet at the given index. - * - * @param label - * @param index - * @return true if the operation has been done, else return false - */ - public boolean moveDataSet(String label, int index) - { - int previous_index = this.indexOf(label); - - // Si l'ensemble des conditions suivantes sont vérifiées : - // - le Groupe d'Information existe, - // - l'index donné est cohérent - // - l'index donné diffère de l'index actuel - if ((previous_index >= 0) && (index >= 0) && (index < this.DataSetList.size()) && (index != previous_index)) - { - // Alors déplacer le Groupe d'Information à l'index donné: - - TICFrameDataSet dataSet = this.DataSetList.remove(previous_index); - - if (index < this.DataSetList.size()) - { - this.DataSetList.add(index, dataSet); - } - - else - { - this.DataSetList.add(dataSet); - } - - return true; - } - - else - { - // Sinon, l'opération n'est pas effectuée - return false; - } - } - - /** - * Set data set - * - * @param label - * @param data - * @return true if succeed - */ - public boolean setDataSet(String label, String data) - { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) - { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - return true; - } - - else - { - return false; - } - } - - /** - * Set data set - * - * @param label - * @param data - * @param checksum - * @return true if succeed - */ - public boolean setDataSet(String label, String data, byte checksum) - { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) - { - dataSet.setData(data); - dataSet.setChecksum(checksum); - - return true; - } - - else - { - return false; - } - } - - /** - * Remove data set - * - * @param label - * @return tic frame data set - */ - public TICFrameDataSet removeDataSet(String label) - { - TICFrameDataSet dataSet = null; - - int index = this.indexOf(label); - - if (index >= 0) - { - dataSet = this.DataSetList.remove(index); - } - - return dataSet; - } - - /** - * Remove data set - * - * @param index - * @return tic frame data set - */ - public TICFrameDataSet removeDataSet(int index) - { - TICFrameDataSet dataSet = null; - - if ((index >= 0) && (index < this.DataSetList.size())) - { - dataSet = this.DataSetList.remove(index); - } - - return dataSet; - } - - /** - * Get bytes - * - * @return bytes - */ - public byte[] getBytes() - { - BytesArray bytesFrame = new BytesArray(); - - bytesFrame.add(BEGINNING_PATTERN); - - for (int i = 0; i < this.DataSetList.size(); i++) - { - bytesFrame.addAll(this.DataSetList.get(i).getBytes()); - } - - bytesFrame.add(END_PATTERN); - - return bytesFrame.getBytes(); - } - - /** - * Get data dictionary - * - * @return data dictionary - * @throws DataDictionaryException - */ - public DataDictionary getDataDictionary() throws DataDictionaryException - { - return this.getDataDictionary(0); - } - - /** - * Return a DataDictionary object corresponding to the TIC frame - * - * @param options - * options to DataDictionary render (use like bits fields) : - EXPLICIT : frame is detailed - NOCHECKSUM - * : DataSet checksum are hidden - NODATETIME : DataSet dateTime fields are hidden - NOTICMODE : Tic mode - * is hidden; - TRIMMED : NOCHECKSUM | NODATETIME - FILTER_INVALID : Invalid DataSet are filtered - * - * - * @return data dictionary - * @throws DataDictionaryException - */ - public DataDictionary getDataDictionary(int options) throws DataDictionaryException - { - DataDictionaryBase dictionary = new DataDictionaryBase(); - - JSONObject jsonObject = new JSONObject(); - - if (0 == (options & NOTICMODE)) - { - dictionary.addKey(KEY_TICMODE); - dictionary.set(KEY_TICMODE, this.getMode().name()); - } - - for (int i = 0; i < this.DataSetList.size(); i++) - { - TICFrameDataSet dataSet = this.DataSetList.get(i); - - // Si le DataSet n'est pas valid et que l'option "filtrer les - // groupes invalides" est activée, - if ((false == dataSet.isValid()) && ((options & NOINVALID) > 0)) - { - // Ignorer le DataSet - } - - // Sinon, - else - { - jsonObject.append(KEY_TICFRAME, dataSet.toJSON(options)); - } - - } - - dictionary.addKey(KEY_TICFRAME); - dictionary.set(KEY_TICFRAME, jsonObject.get(KEY_TICFRAME)); - - return dictionary; - } - - /** - * Get mode - * - * @return mode - */ - public abstract TICMode getMode(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.json.JSONObject; + +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.BytesArray; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; + +/** + * TICFrame + */ +public abstract class TICFrame +{ + /** Beginning pattern */ + public static final byte BEGINNING_PATTERN = 0x02; // STX + /** End pattern */ + public static final byte END_PATTERN = 0x03; // ETX + /** End of trame pattern */ + public static final byte EOT = 0x04; // EOT + + // Options + /** No specific options */ + public static final int EXPLICIT = 0x0000; + /** No checksum options */ + public static final int NOCHECKSUM = 0x0001; + /** No date time options */ + public static final int NODATETIME = 0x0002; + /** No tic mode options */ + public static final int NOTICMODE = 0x0004; + /** No invalid options */ + public static final int NOINVALID = 0x0008; + /** Trimmed options */ + public static final int TRIMMED = NOCHECKSUM | NODATETIME | NOINVALID; + + /** Key tic frame */ + public static final String KEY_TICFRAME = "ticFrame"; + /** Key tic mode */ + public static final String KEY_TICMODE = "ticMode"; + /** Key label */ + public static final String KEY_LABEL = "label"; + /** Key data */ + public static final String KEY_DATA = "data"; + /** Key checksum */ + public static final String KEY_CHECKSUM = "checksum"; + /** Key date time */ + public static final String KEY_DATETIME = "dateTime"; + + protected List DataSetList; + + /** + * Default constructor + */ + public TICFrame() + { + this.DataSetList = new ArrayList(); + } + + @Override + public int hashCode() + { + return Objects.hash(this.DataSetList); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (this.getClass() != obj.getClass()) + return false; + TICFrame other = (TICFrame) obj; + return Objects.equals(this.DataSetList, other.DataSetList); + } + + /** + * Return the list of DataSet + * + * @return the list of DataSet + */ + public List getDataSetList() + { + return this.DataSetList; + } + + /** + * Set the list of DataSet + * + * @param dataSetList + */ + public void setDataSetList(List dataSetList) + { + this.DataSetList = dataSetList; + } + + /** + * Return the index of the given DataSet + * + * @param label + * Label of the DataSet + * @return the index of the given DataSet or '-1' if it does not exist + */ + public int indexOf(String label) + { + int index = -1; + + for (int i = 0; i < this.DataSetList.size(); i++) + { + if (this.DataSetList.get(i).getLabel().equals(label)) + { + index = i; + break; + } + } + + return index; + } + + /** + * Return the given DataSet + * + * @param label + * Label of the DataSet + * @return the given DataSet or 'null' if it does not exist + */ + public TICFrameDataSet getDataSet(String label) + { + TICFrameDataSet dataSet = null; + + for (int i = 0; i < this.DataSetList.size(); i++) + { + if (this.DataSetList.get(i).getLabel().equals(label)) + { + dataSet = this.DataSetList.get(i); + break; + } + } + + return dataSet; + } + + /** + * Return the data field of the given DataSet + * + * @param label + * Label of the DataSet + * @return the data field of the given DataSet + */ + public String getData(String label) + { + TICFrameDataSet dataSet = this.getDataSet(label); + + if (dataSet != null) + { + return dataSet.getData(); + } + + else + { + return null; + } + } + + /** + * Add the dataSet at the end of the list. If the dataSet already existed, its data and checksum are just set + * + * @param dataSet + */ + public void addDataSet(TICFrameDataSet dataSet) + { + TICFrameDataSet oldDataSet = this.getDataSet(dataSet.getLabel()); + + if (oldDataSet == null) + { + this.DataSetList.add(dataSet); + } + + else + { + oldDataSet.setData(dataSet.getData()); + oldDataSet.setChecksum(dataSet.getChecksum()); + } + } + + /** + * Add the dataSet at the given index of the list. If the dataSet already existed, its data and checksum are set and + * it is moved at the given index + * + * @param index + * @param dataSet + */ + public void addDataSet(int index, TICFrameDataSet dataSet) + { + int dataSetIndex = this.indexOf(dataSet.getLabel()); + + if (dataSetIndex < 0) + { + this.DataSetList.add(index, dataSet); + } + + else + { + if (dataSetIndex != index) + { + this.removeDataSet(dataSetIndex); + this.DataSetList.add(index, dataSet); + } + + else + { + this.DataSetList.get(index).setData(dataSet.getData()); + this.DataSetList.get(index).setChecksum(dataSet.getChecksum()); + } + } + } + + /** + * Add data set + * + * @param label + * @param data + * @return tic frame data set + */ + public abstract TICFrameDataSet addDataSet(String label, String data); + + /** + * Add data set + * + * @param index + * @param label + * @param data + * @return tic frame data set + */ + public abstract TICFrameDataSet addDataSet(int index, String label, String data); + + /** + * Move the DataSet at the given index. + * + * @param label + * @param index + * @return true if the operation has been done, else return false + */ + public boolean moveDataSet(String label, int index) + { + int previous_index = this.indexOf(label); + + // Si l'ensemble des conditions suivantes sont vérifiées : + // - le Groupe d'Information existe, + // - l'index donné est cohérent + // - l'index donné diffère de l'index actuel + if ((previous_index >= 0) && (index >= 0) && (index < this.DataSetList.size()) && (index != previous_index)) + { + // Alors déplacer le Groupe d'Information à l'index donné: + + TICFrameDataSet dataSet = this.DataSetList.remove(previous_index); + + if (index < this.DataSetList.size()) + { + this.DataSetList.add(index, dataSet); + } + + else + { + this.DataSetList.add(dataSet); + } + + return true; + } + + else + { + // Sinon, l'opération n'est pas effectuée + return false; + } + } + + /** + * Set data set + * + * @param label + * @param data + * @return true if succeed + */ + public boolean setDataSet(String label, String data) + { + TICFrameDataSet dataSet = this.getDataSet(label); + + if (dataSet != null) + { + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + return true; + } + + else + { + return false; + } + } + + /** + * Set data set + * + * @param label + * @param data + * @param checksum + * @return true if succeed + */ + public boolean setDataSet(String label, String data, byte checksum) + { + TICFrameDataSet dataSet = this.getDataSet(label); + + if (dataSet != null) + { + dataSet.setData(data); + dataSet.setChecksum(checksum); + + return true; + } + + else + { + return false; + } + } + + /** + * Remove data set + * + * @param label + * @return tic frame data set + */ + public TICFrameDataSet removeDataSet(String label) + { + TICFrameDataSet dataSet = null; + + int index = this.indexOf(label); + + if (index >= 0) + { + dataSet = this.DataSetList.remove(index); + } + + return dataSet; + } + + /** + * Remove data set + * + * @param index + * @return tic frame data set + */ + public TICFrameDataSet removeDataSet(int index) + { + TICFrameDataSet dataSet = null; + + if ((index >= 0) && (index < this.DataSetList.size())) + { + dataSet = this.DataSetList.remove(index); + } + + return dataSet; + } + + /** + * Get bytes + * + * @return bytes + */ + public byte[] getBytes() + { + BytesArray bytesFrame = new BytesArray(); + + bytesFrame.add(BEGINNING_PATTERN); + + for (int i = 0; i < this.DataSetList.size(); i++) + { + bytesFrame.addAll(this.DataSetList.get(i).getBytes()); + } + + bytesFrame.add(END_PATTERN); + + return bytesFrame.getBytes(); + } + + /** + * Get data dictionary + * + * @return data dictionary + * @throws DataDictionaryException + */ + public DataDictionary getDataDictionary() throws DataDictionaryException + { + return this.getDataDictionary(0); + } + + /** + * Return a DataDictionary object corresponding to the TIC frame + * + * @param options + * options to DataDictionary render (use like bits fields) : - EXPLICIT : frame is detailed - NOCHECKSUM + * : DataSet checksum are hidden - NODATETIME : DataSet dateTime fields are hidden - NOTICMODE : Tic mode + * is hidden; - TRIMMED : NOCHECKSUM | NODATETIME - FILTER_INVALID : Invalid DataSet are filtered + * + * + * @return data dictionary + * @throws DataDictionaryException + */ + public DataDictionary getDataDictionary(int options) throws DataDictionaryException + { + DataDictionaryBase dictionary = new DataDictionaryBase(); + + JSONObject jsonObject = new JSONObject(); + + if (0 == (options & NOTICMODE)) + { + dictionary.addKey(KEY_TICMODE); + dictionary.set(KEY_TICMODE, this.getMode().name()); + } + + for (int i = 0; i < this.DataSetList.size(); i++) + { + TICFrameDataSet dataSet = this.DataSetList.get(i); + + // Si le DataSet n'est pas valid et que l'option "filtrer les + // groupes invalides" est activée, + if ((false == dataSet.isValid()) && ((options & NOINVALID) > 0)) + { + // Ignorer le DataSet + } + + // Sinon, + else + { + jsonObject.append(KEY_TICFRAME, dataSet.toJSON(options)); + } + + } + + dictionary.addKey(KEY_TICFRAME); + dictionary.set(KEY_TICFRAME, jsonObject.get(KEY_TICFRAME)); + + return dictionary; + } + + /** + * Get mode + * + * @return mode + */ + public abstract TICMode getMode(); + +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java index ccea017..576b060 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java @@ -1,313 +1,259 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame; - -import java.util.Objects; - -import org.json.JSONObject; - -import enedis.lab.types.BytesArray; - -/** - * TIC frame data set - */ -public abstract class TICFrameDataSet -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Beginning pattern */ - public static final byte BEGINNING_PATTERN = 0x0A; // LF - /** End pattern */ - public static final byte END_PATTERN = 0x0D; // CR - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected BytesArray label = null; - protected BytesArray data = null; - protected byte checksum; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TICFrameDataSet() - { - this.init(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Object - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public int hashCode() - { - return Objects.hash(this.checksum, this.data, this.label); - } - - @Override - public boolean equals(Object obj) - { - if (this == obj) - return true; - if (obj == null) - return false; - if (this.getClass() != obj.getClass()) - return false; - TICFrameDataSet other = (TICFrameDataSet) obj; - return this.checksum == other.checksum && Objects.equals(this.data, other.data) && Objects.equals(this.label, other.label); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Setup from string label and data - * - * @param label - * @param data - */ - public void setup(String label, String data) - { - this.setup(label.getBytes(), data.getBytes()); - } - - /** - * Setup from byte[] label and data - * - * @param label - * @param data - */ - public void setup(byte[] label, byte[] data) - { - this.label = new BytesArray(label); - this.data = new BytesArray(data); - } - - /** - * Get label - * - * @return label - */ - public String getLabel() - { - return new String(this.label.getBytes()); - } - - /** - * Set the Label - * - * @param label - */ - public void setLabel(String label) - { - this.setLabel(label.getBytes()); - } - - /** - * Set the Label - * - * @param label - */ - public void setLabel(byte[] label) - { - this.setLabel(new BytesArray(label)); - } - - /** - * Set the Label - * - * @param label - */ - public void setLabel(BytesArray label) - { - this.label = label; - this.setChecksum(); - } - - /** - * Get data - * - * @return data - */ - public String getData() - { - return new String(this.data.getBytes()); - } - - /** - * Set data - * - * @param data - */ - public void setData(String data) - { - this.setData(data.getBytes()); - } - - /** - * Set data - * - * @param data - */ - public void setData(byte[] data) - { - this.setData(new BytesArray(data)); - } - - /** - * Set data - * - * @param data - */ - public void setData(BytesArray data) - { - this.data = data; - this.setChecksum(); - } - - /** - * Get checksum - * - * @return checksum - */ - public byte getChecksum() - { - - return this.checksum; - } - - /** - * Compute and set the checksum (NB: Label and Data shall have been set) - * - * @return true if the operation succeeds, else, return false - */ - public boolean setChecksum() - { - Byte checksum = this.getConsistentChecksum(); - - if (checksum != null) - { - this.checksum = checksum; - return true; - } - - else - { - return false; - } - } - - /** - * Set the checksum at a given value - * - * @param checksum - */ - public void setChecksum(byte checksum) - { - this.checksum = checksum; - - } - - /** - * - * @return true if fields are consistent with checksum - */ - public boolean isValid() - { - Byte consistentChecksum = this.getConsistentChecksum(); - - if ((consistentChecksum != null) && (consistentChecksum.byteValue() == (this.checksum & (byte) 0xFF))) - { - return true; - } - - else - { - return false; - } - } - - /** - * Get bytes - * - * @return bytes - */ - public abstract byte[] getBytes(); - - /** - * Conver to JSON - * - * @return json object - */ - public abstract JSONObject toJSON(); - - /** - * Conver to JSON - * - * @param option - * @return json object - */ - public abstract JSONObject toJSON(int option); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Return the consistent checksum of the DataSet - * - * @return the consistent checksum of the DataSet - */ - protected abstract Byte getConsistentChecksum(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void init() - { - this.label = null; - this.data = null; - this.checksum = -1; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame; + +import java.util.Objects; + +import org.json.JSONObject; + +import enedis.lab.types.BytesArray; + +/** + * TIC frame data set + */ +public abstract class TICFrameDataSet +{ + /** Beginning pattern */ + public static final byte BEGINNING_PATTERN = 0x0A; // LF + /** End pattern */ + public static final byte END_PATTERN = 0x0D; // CR + + protected BytesArray label = null; + protected BytesArray data = null; + protected byte checksum; + + /** + * Default constructor + */ + public TICFrameDataSet() + { + this.init(); + } + + @Override + public int hashCode() + { + return Objects.hash(this.checksum, this.data, this.label); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (this.getClass() != obj.getClass()) + return false; + TICFrameDataSet other = (TICFrameDataSet) obj; + return this.checksum == other.checksum && Objects.equals(this.data, other.data) && Objects.equals(this.label, other.label); + } + + /** + * Setup from string label and data + * + * @param label + * @param data + */ + public void setup(String label, String data) + { + this.setup(label.getBytes(), data.getBytes()); + } + + /** + * Setup from byte[] label and data + * + * @param label + * @param data + */ + public void setup(byte[] label, byte[] data) + { + this.label = new BytesArray(label); + this.data = new BytesArray(data); + } + + /** + * Get label + * + * @return label + */ + public String getLabel() + { + return new String(this.label.getBytes()); + } + + /** + * Set the Label + * + * @param label + */ + public void setLabel(String label) + { + this.setLabel(label.getBytes()); + } + + /** + * Set the Label + * + * @param label + */ + public void setLabel(byte[] label) + { + this.setLabel(new BytesArray(label)); + } + + /** + * Set the Label + * + * @param label + */ + public void setLabel(BytesArray label) + { + this.label = label; + this.setChecksum(); + } + + /** + * Get data + * + * @return data + */ + public String getData() + { + return new String(this.data.getBytes()); + } + + /** + * Set data + * + * @param data + */ + public void setData(String data) + { + this.setData(data.getBytes()); + } + + /** + * Set data + * + * @param data + */ + public void setData(byte[] data) + { + this.setData(new BytesArray(data)); + } + + /** + * Set data + * + * @param data + */ + public void setData(BytesArray data) + { + this.data = data; + this.setChecksum(); + } + + /** + * Get checksum + * + * @return checksum + */ + public byte getChecksum() + { + + return this.checksum; + } + + /** + * Compute and set the checksum (NB: Label and Data shall have been set) + * + * @return true if the operation succeeds, else, return false + */ + public boolean setChecksum() + { + Byte checksum = this.getConsistentChecksum(); + + if (checksum != null) + { + this.checksum = checksum; + return true; + } + + else + { + return false; + } + } + + /** + * Set the checksum at a given value + * + * @param checksum + */ + public void setChecksum(byte checksum) + { + this.checksum = checksum; + + } + + /** + * + * @return true if fields are consistent with checksum + */ + public boolean isValid() + { + Byte consistentChecksum = this.getConsistentChecksum(); + + if ((consistentChecksum != null) && (consistentChecksum.byteValue() == (this.checksum & (byte) 0xFF))) + { + return true; + } + + else + { + return false; + } + } + + /** + * Get bytes + * + * @return bytes + */ + public abstract byte[] getBytes(); + + /** + * Conver to JSON + * + * @return json object + */ + public abstract JSONObject toJSON(); + + /** + * Conver to JSON + * + * @param option + * @return json object + */ + public abstract JSONObject toJSON(int option); + + /** + * Return the consistent checksum of the DataSet + * + * @return the consistent checksum of the DataSet + */ + protected abstract Byte getConsistentChecksum(); + + private void init() + { + this.label = null; + this.data = null; + this.checksum = -1; + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java index 417b23b..a172f10 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java @@ -15,39 +15,9 @@ */ public class TICFrameHistoric extends TICFrame { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** Separator */ public static final byte SEPARATOR = 0x20; // SP - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Default constructor */ @@ -56,13 +26,6 @@ public TICFrameHistoric() super(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICFrame - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public TICFrameHistoricDataSet addDataSet(String label, String data) { @@ -119,22 +82,4 @@ public TICMode getMode() return TICMode.HISTORIC; } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java index 8c4ef34..2061d5b 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java @@ -1,190 +1,135 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.historic; - -import org.json.JSONObject; - -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.types.BytesArray; - -/** - * TIC frame historic data set - */ -public class TICFrameHistoricDataSet extends TICFrameDataSet -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Separator */ - public static final byte SEPARATOR = 0x20; // SP - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - */ - public TICFrameHistoricDataSet() - { - super(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICFrameDataSet - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Byte getConsistentChecksum() - { - Byte crc = 0; - byte[] labelByte; - byte[] dataByte; - if ((this.label != null) && (this.data != null)) - { - labelByte = this.label.getBytes(); - for (int i = 0; i < labelByte.length; i++) - { - crc = this.computeUpdate(crc, labelByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - dataByte = this.data.getBytes(); - - for (int i = 0; i < dataByte.length; i++) - { - crc = this.computeUpdate(crc, dataByte[i]); - } - - return this.computeEnd(crc); - } - else - { - return null; - } - - } - - /** - * compute Update - * - * @param crc - * @param octet - * @return Byte crc after compute update - */ - public Byte computeUpdate(Byte crc, byte octet) - { - return (byte) (crc + (octet & 0xff)); - } - - /** - * compute End - * - * @param crc - * @return Byte crc after compute end - */ - public Byte computeEnd(Byte crc) - { - return (byte) ((crc & 0x3F) + 0x20); - } - - @Override - public byte[] getBytes() - { - BytesArray dataSet = new BytesArray(); - - if ((this.label != null) && (this.data != null)) - { - dataSet.add(BEGINNING_PATTERN); - dataSet.addAll(this.label.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.addAll(this.data.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.add(this.checksum); - - dataSet.add(END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public JSONObject toJSON() - { - return this.toJSON(TICFrame.TRIMMED); - } - - @Override - public JSONObject toJSON(int option) - { - JSONObject jsonObject = new JSONObject(); - - if ((option & TICFrame.NOCHECKSUM) > 0) - { - jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); - } - - else - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - - return jsonObject; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame.historic; + +import org.json.JSONObject; + +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.types.BytesArray; + +/** + * TIC frame historic data set + */ +public class TICFrameHistoricDataSet extends TICFrameDataSet +{ + /** Separator */ + public static final byte SEPARATOR = 0x20; // SP + + /** + * Constructor + */ + public TICFrameHistoricDataSet() + { + super(); + } + + @Override + public Byte getConsistentChecksum() + { + Byte crc = 0; + byte[] labelByte; + byte[] dataByte; + if ((this.label != null) && (this.data != null)) + { + labelByte = this.label.getBytes(); + for (int i = 0; i < labelByte.length; i++) + { + crc = this.computeUpdate(crc, labelByte[i]); + } + + crc = this.computeUpdate(crc, SEPARATOR); + dataByte = this.data.getBytes(); + + for (int i = 0; i < dataByte.length; i++) + { + crc = this.computeUpdate(crc, dataByte[i]); + } + + return this.computeEnd(crc); + } + else + { + return null; + } + + } + + /** + * compute Update + * + * @param crc + * @param octet + * @return Byte crc after compute update + */ + public Byte computeUpdate(Byte crc, byte octet) + { + return (byte) (crc + (octet & 0xff)); + } + + /** + * compute End + * + * @param crc + * @return Byte crc after compute end + */ + public Byte computeEnd(Byte crc) + { + return (byte) ((crc & 0x3F) + 0x20); + } + + @Override + public byte[] getBytes() + { + BytesArray dataSet = new BytesArray(); + + if ((this.label != null) && (this.data != null)) + { + dataSet.add(BEGINNING_PATTERN); + dataSet.addAll(this.label.getBytes()); + + dataSet.add(SEPARATOR); + dataSet.addAll(this.data.getBytes()); + + dataSet.add(SEPARATOR); + dataSet.add(this.checksum); + + dataSet.add(END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public JSONObject toJSON() + { + return this.toJSON(TICFrame.TRIMMED); + } + + @Override + public JSONObject toJSON(int option) + { + JSONObject jsonObject = new JSONObject(); + + if ((option & TICFrame.NOCHECKSUM) > 0) + { + jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); + } + + else + { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_CHECKSUM, this.checksum); + jsonObject.put(new String(this.label.getBytes()), content); + } + + return jsonObject; + } + +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java index d23c26a..1a2437a 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java @@ -17,12 +17,6 @@ */ public class TICFrameStandard extends TICFrame { - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** Begin info group */ public static final byte INFOGROUP_BEGIN = 0x03; // LF /** End info group */ @@ -35,30 +29,6 @@ public class TICFrameStandard extends TICFrame /** MIN_SIZE */ public static final int MIN_SIZE = INFOGROUP_MIN_SIZE + 2; // ETX + INFGROUP_MIN_SIZE + STX - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Default constructor */ @@ -67,13 +37,6 @@ public TICFrameStandard() super(); } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICFrame - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public TICFrameStandardDataSet addDataSet(String label, String data) { @@ -171,12 +134,6 @@ public String getData(String label) } } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** * Add data set * @@ -232,16 +189,4 @@ public LocalDateTime getDateTimeAsLocalDateTime(String label) } } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java index 8ee0198..a41c06c 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java @@ -1,352 +1,297 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.standard; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; - -import org.json.JSONObject; - -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.types.BytesArray; - -/** - * TIC frame standard data set - */ -public class TICFrameStandardDataSet extends TICFrameDataSet -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Separator */ - public static final byte SEPARATOR = 0x09; // HT - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected BytesArray dateTime = null; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - */ - public TICFrameStandardDataSet() - { - super(); - this.init(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICFrameDataSet - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Byte getConsistentChecksum() - { - Byte crc = 0; - byte[] labelByte; - byte[] dataByte; - byte[] dateByte; - - if ((this.label != null) && (this.data != null)) - { - labelByte = this.label.getBytes(); - for (int i = 0; i < labelByte.length; i++) - { - crc = this.computeUpdate(crc, labelByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - - if (this.dateTime != null) - { - dateByte = this.dateTime.getBytes(); - for (int i = 0; i < dateByte.length; i++) - { - crc = this.computeUpdate(crc, dateByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - } - - dataByte = this.data.getBytes(); - - for (int i = 0; i < dataByte.length; i++) - { - crc = this.computeUpdate(crc, dataByte[i]); - } - crc = this.computeUpdate(crc, SEPARATOR); - - return this.computeEnd(crc); - } - else - { - return null; - } - - } - - /** - * Compute update - * - * @param crc - * @param octet - * @return Byte crc compute after Update - */ - public Byte computeUpdate(Byte crc, byte octet) - { - return (byte) (crc + (octet & 0xff)); - } - - /** - * Compute end - * - * @param crc - * @return Byte crc compute after End - */ - public Byte computeEnd(Byte crc) - { - return (byte) ((crc & 0x3F) + 0x20); - } - - @Override - public byte[] getBytes() - { - BytesArray dataSet = new BytesArray(); - - if ((this.label != null) && (this.data != null)) - { - dataSet.add(BEGINNING_PATTERN); - dataSet.addAll(this.label.getBytes()); - - if (null != this.dateTime) - { - dataSet.add(SEPARATOR); - dataSet.addAll(this.dateTime.getBytes()); - } - - dataSet.add(SEPARATOR); - dataSet.addAll(this.data.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.add(this.checksum); - - dataSet.add(END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public JSONObject toJSON() - { - return this.toJSON(TICFrame.TRIMMED); - } - - @Override - public JSONObject toJSON(int option) - { - JSONObject jsonObject = new JSONObject(); - - if ((option & TICFrame.NOCHECKSUM) > 0) - { - if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) - { - jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); - } - - else - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); - jsonObject.put(new String(this.label.getBytes()), content); - } - } - - else - { - if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - - else - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - } - - return jsonObject; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Setup - * - * @param label - * @param data - * @param dateTime - */ - public void setup(String label, String data, String dateTime) - { - this.setup(label.getBytes(), data.getBytes(), dateTime.getBytes()); - } - - /** - * Setup - * - * @param label - * @param data - * @param dateTime - */ - public void setup(byte[] label, byte[] data, byte[] dateTime) - { - super.setup(label, data); - this.dateTime = new BytesArray(dateTime); - } - - /** - * Get datetime - * - * @return datetime - */ - public String getDateTime() - { - return new String(this.dateTime.getBytes()); - } - - /** - * Check datetime - * - * @return result - */ - public Boolean checkDateTime() - { - if (this.dateTime == null) - { - return false; - } - return true; - } - - /** - * Get datetime as local datetime - * - * @return datetime as local datetime - */ - // DATE H190730100158 yyMMddHHmmss - public LocalDateTime getDateTimeAsLocalDateTime() - { - String strDateTime = this.getDateTime(); - LocalDateTime localDateTime = null; - - if (strDateTime != null && strDateTime.length() == 13) - { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMddHHmmss"); - try - { - localDateTime = LocalDateTime.parse(strDateTime.substring(1), formatter); - } - catch (DateTimeParseException e) - { - localDateTime = null; - } - } - - return localDateTime; - } - - /** - * Set date time - * - * @param dateTime - */ - public void setDateTime(String dateTime) - { - this.setDateTime(dateTime.getBytes()); - } - - /** - * Set datetime - * - * @param dateTime - */ - public void setDateTime(byte[] dateTime) - { - this.setDateTime(new BytesArray(dateTime)); - } - - /** - * Set date time - * - * @param dateTime - */ - public void setDateTime(BytesArray dateTime) - { - this.dateTime = dateTime; - this.setChecksum(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void init() - { - this.dateTime = null; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame.standard; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import org.json.JSONObject; + +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.types.BytesArray; + +/** + * TIC frame standard data set + */ +public class TICFrameStandardDataSet extends TICFrameDataSet +{ + /** Separator */ + public static final byte SEPARATOR = 0x09; // HT + + protected BytesArray dateTime = null; + + /** + * Constructor + */ + public TICFrameStandardDataSet() + { + super(); + this.init(); + } + + @Override + public Byte getConsistentChecksum() + { + Byte crc = 0; + byte[] labelByte; + byte[] dataByte; + byte[] dateByte; + + if ((this.label != null) && (this.data != null)) + { + labelByte = this.label.getBytes(); + for (int i = 0; i < labelByte.length; i++) + { + crc = this.computeUpdate(crc, labelByte[i]); + } + + crc = this.computeUpdate(crc, SEPARATOR); + + if (this.dateTime != null) + { + dateByte = this.dateTime.getBytes(); + for (int i = 0; i < dateByte.length; i++) + { + crc = this.computeUpdate(crc, dateByte[i]); + } + + crc = this.computeUpdate(crc, SEPARATOR); + } + + dataByte = this.data.getBytes(); + + for (int i = 0; i < dataByte.length; i++) + { + crc = this.computeUpdate(crc, dataByte[i]); + } + crc = this.computeUpdate(crc, SEPARATOR); + + return this.computeEnd(crc); + } + else + { + return null; + } + + } + + /** + * Compute update + * + * @param crc + * @param octet + * @return Byte crc compute after Update + */ + public Byte computeUpdate(Byte crc, byte octet) + { + return (byte) (crc + (octet & 0xff)); + } + + /** + * Compute end + * + * @param crc + * @return Byte crc compute after End + */ + public Byte computeEnd(Byte crc) + { + return (byte) ((crc & 0x3F) + 0x20); + } + + @Override + public byte[] getBytes() + { + BytesArray dataSet = new BytesArray(); + + if ((this.label != null) && (this.data != null)) + { + dataSet.add(BEGINNING_PATTERN); + dataSet.addAll(this.label.getBytes()); + + if (null != this.dateTime) + { + dataSet.add(SEPARATOR); + dataSet.addAll(this.dateTime.getBytes()); + } + + dataSet.add(SEPARATOR); + dataSet.addAll(this.data.getBytes()); + + dataSet.add(SEPARATOR); + dataSet.add(this.checksum); + + dataSet.add(END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public JSONObject toJSON() + { + return this.toJSON(TICFrame.TRIMMED); + } + + @Override + public JSONObject toJSON(int option) + { + JSONObject jsonObject = new JSONObject(); + + if ((option & TICFrame.NOCHECKSUM) > 0) + { + if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) + { + jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); + } + + else + { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); + jsonObject.put(new String(this.label.getBytes()), content); + } + } + + else + { + if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) + { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_CHECKSUM, this.checksum); + jsonObject.put(new String(this.label.getBytes()), content); + } + + else + { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); + content.put(TICFrame.KEY_CHECKSUM, this.checksum); + jsonObject.put(new String(this.label.getBytes()), content); + } + } + + return jsonObject; + } + + /** + * Setup + * + * @param label + * @param data + * @param dateTime + */ + public void setup(String label, String data, String dateTime) + { + this.setup(label.getBytes(), data.getBytes(), dateTime.getBytes()); + } + + /** + * Setup + * + * @param label + * @param data + * @param dateTime + */ + public void setup(byte[] label, byte[] data, byte[] dateTime) + { + super.setup(label, data); + this.dateTime = new BytesArray(dateTime); + } + + /** + * Get datetime + * + * @return datetime + */ + public String getDateTime() + { + return new String(this.dateTime.getBytes()); + } + + /** + * Check datetime + * + * @return result + */ + public Boolean checkDateTime() + { + if (this.dateTime == null) + { + return false; + } + return true; + } + + /** + * Get datetime as local datetime + * + * @return datetime as local datetime + */ + // DATE H190730100158 yyMMddHHmmss + public LocalDateTime getDateTimeAsLocalDateTime() + { + String strDateTime = this.getDateTime(); + LocalDateTime localDateTime = null; + + if (strDateTime != null && strDateTime.length() == 13) + { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMddHHmmss"); + try + { + localDateTime = LocalDateTime.parse(strDateTime.substring(1), formatter); + } + catch (DateTimeParseException e) + { + localDateTime = null; + } + } + + return localDateTime; + } + + /** + * Set date time + * + * @param dateTime + */ + public void setDateTime(String dateTime) + { + this.setDateTime(dateTime.getBytes()); + } + + /** + * Set datetime + * + * @param dateTime + */ + public void setDateTime(byte[] dateTime) + { + this.setDateTime(new BytesArray(dateTime)); + } + + /** + * Set date time + * + * @param dateTime + */ + public void setDateTime(BytesArray dateTime) + { + this.dateTime = dateTime; + this.setChecksum(); + } + + private void init() + { + this.dateTime = null; + } +} From 83441cd283534cdac40011b46ae61c4454641619 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Fri, 3 Oct 2025 16:51:17 +0200 Subject: [PATCH 22/59] chore: format enedis.lab.protocol package --- .../tic/channels/ChannelTICSerialPort.java | 625 +++-- .../ChannelTICSerialPortConfiguration.java | 512 ++-- .../serialport/ChannelSerialPort.java | 2086 ++++++++++------- .../ChannelSerialPortErrorCode.java | 160 +- .../tic/codec/CodecTICFrameHistoric.java | 332 ++- 5 files changed, 2084 insertions(+), 1631 deletions(-) diff --git a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java index 468c112..da15adb 100644 --- a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java +++ b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java @@ -1,326 +1,299 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.channels; - -import java.util.Arrays; - -import enedis.lab.io.channels.ChannelConfiguration; -import enedis.lab.io.channels.ChannelException; -import enedis.lab.io.channels.serialport.ChannelSerialPort; -import enedis.lab.io.channels.serialport.ChannelSerialPortErrorCode; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.BytesArray; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.time.Time; - -/** - * Channel TIC SerialPort - */ -public class ChannelTICSerialPort extends ChannelSerialPort -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final byte START_PATTERN = (byte) 0x02; - private static final byte END_PATTERN = (byte) 0x03; - private static final int RECEIVE_DATA_POLLING_PERIOD = 100; - private static final byte[] HISTORIC_BUFFER_START = { 2, 10, 65, 68, 67, 79 }; - private static final byte[] STANDARD_BUFFER_START = { 2, 10, 65, 68, 83, 67 }; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICMode currentMode; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor: create an instance from a SerialPortConfiguration - * - * @param configuration - * @throws ChannelException - */ - public ChannelTICSerialPort(ChannelTICSerialPortConfiguration configuration) throws ChannelException - { - super(configuration); - this.currentMode = null; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Channel - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException - { - if (configuration == null) - { - ChannelException.raiseInvalidConfiguration("null"); - } - if (!(configuration instanceof ChannelTICSerialPortConfiguration)) - { - ChannelException.raiseInvalidConfigurationType(configuration, ChannelTICSerialPortConfiguration.class.getSimpleName()); - } - super.setup(configuration); - } - - @Override - public ChannelTICSerialPortConfiguration getConfiguration() - { - return (ChannelTICSerialPortConfiguration) this.configuration; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get mode - * - * @return TIC Mode - */ - public TICMode getMode() - { - return (this.getCurrentMode() != null) ? this.getCurrentMode() : this.getSelectedMode(); - } - - /** - * Get selected mode - * - * @return selected Mode - */ - public TICMode getSelectedMode() - { - return this.getConfiguration().getTicMode(); - } - - /** - * Get current mode - * - * @return current Mode - */ - public TICMode getCurrentMode() - { - return this.currentMode; - } - - @Override - public byte[] read() throws ChannelException - { - long beginTime = System.currentTimeMillis(); - long elapsedTime = 0; - long timeout = this.getSyncReadTimeout(); - BytesArray buffer = new BytesArray(); - byte[] ticFrame = null; - int startOfFrame = -1, endOfFrame = -1; - - while (elapsedTime < timeout || timeout == 0) - { - if (this.available() > 0) - { - buffer.addAll(super.read(1)); - if (startOfFrame == -1) - { - startOfFrame = buffer.indexOf(START_PATTERN); - if (startOfFrame != -1) - { - endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); - if (endOfFrame != -1) - { - ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); - break; - } - } - } - else - { - endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); - if (endOfFrame != -1) - { - ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); - break; - } - } - } - else - { - Time.sleep(RECEIVE_DATA_POLLING_PERIOD); - elapsedTime = (System.currentTimeMillis() - beginTime); - } - } - - return ticFrame; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void receiveData() - { - if (this.currentMode == null) - { - if (this.autoDetectMode() == null) - { - this.onReadTimeout(); - return; - } - } - try - { - byte[] ticFrame = this.read(); - - if (ticFrame == null) - { - this.onReadTimeout(); - } - else - { - this.notifyOnDataRead(ticFrame); - } - } - catch (ChannelException e) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), "TIC read failed: " + e.getErrorInfo()); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void onReadTimeout() - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_TIMEOUT.getCode(ERROR_CODE_OFFSET), "TIC read timeout"); - this.configurePort(); - this.currentMode = null; - } - - private void configurePort() - { - this.closePort(); - this.openPort(); - try - { - this.flush(); - } - catch (ChannelException e) - { - } - } - - private void setSelectedMode(TICMode ticMode) - { - try - { - this.getConfiguration().setTicMode(ticMode); - } - catch (DataDictionaryException e) - { - this.logger.error("Cannot set TIC mode " + ticMode, e); - } - } - - private boolean checkAndUpdateMode(TICMode ticMode) - { - boolean result = false; - this.setSelectedMode(ticMode); - this.configurePort(); - try - { - byte[] ticFrame = this.read(); - if (ticFrame != null && ticFrame.length > HISTORIC_BUFFER_START.length) - { - byte[] ticFrameStart = new byte[HISTORIC_BUFFER_START.length]; - System.arraycopy(ticFrame, 0, ticFrameStart, 0, ticFrameStart.length); - if (ticMode == TICMode.HISTORIC) - { - if (Arrays.equals(ticFrameStart, HISTORIC_BUFFER_START)) - { - result = true; - } - } - else if (ticMode == TICMode.STANDARD) - { - if (Arrays.equals(ticFrameStart, STANDARD_BUFFER_START)) - { - result = true; - } - } - } - } - catch (ChannelException e) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), "TIC read failed: " + e.getErrorInfo()); - } - if (result) - { - this.currentMode = ticMode; - } - this.setSelectedMode(TICMode.AUTO); - - return result; - } - - private TICMode autoDetectMode() - { - if (this.getSelectedMode() != TICMode.AUTO) - { - this.currentMode = this.getSelectedMode(); - return this.getSelectedMode(); - } - this.logger.debug("Auto detecting TIC Mode"); - if (!this.checkAndUpdateMode(TICMode.HISTORIC)) - { - if (!this.checkAndUpdateMode(TICMode.STANDARD)) - { - this.logger.debug("TIC Mode not detected"); - - return null; - } - this.logger.debug("TIC Mode STANDARD detected"); - - return TICMode.STANDARD; - } - this.logger.debug("TIC Mode HISTORIC detected"); - - return TICMode.HISTORIC; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.channels; + +import java.util.Arrays; + +import enedis.lab.io.channels.ChannelConfiguration; +import enedis.lab.io.channels.ChannelException; +import enedis.lab.io.channels.serialport.ChannelSerialPort; +import enedis.lab.io.channels.serialport.ChannelSerialPortErrorCode; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.BytesArray; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.time.Time; + +/** + * A specialized serial port channel implementation for TIC (Teleinformation Client) communication. + * This channel extends the base serial port functionality to handle TIC protocol-specific + * frame detection, mode auto-detection, and TIC data stream processing. + * + *

    The channel supports both HISTORIC and STANDARD TIC modes, with automatic mode + * detection capabilities. It handles TIC frame parsing with proper start/end pattern + * recognition and provides timeout handling for reliable data reception. + * + *

    Key features: + *

      + *
    • Automatic TIC mode detection (HISTORIC vs STANDARD)
    • + *
    • TIC frame parsing with start/end pattern recognition
    • + *
    • Timeout handling for reliable data reception
    • + *
    • Port configuration and reconnection management
    • + *
    + * + * @author Enedis Smarties team + */ +public class ChannelTICSerialPort extends ChannelSerialPort +{ + /** TIC frame start pattern (STX character) */ + private static final byte START_PATTERN = (byte) 0x02; + /** TIC frame end pattern (ETX character) */ + private static final byte END_PATTERN = (byte) 0x03; + /** Polling period in milliseconds for data reception */ + private static final int RECEIVE_DATA_POLLING_PERIOD = 100; + /** Historic mode buffer start pattern */ + private static final byte[] HISTORIC_BUFFER_START = { 2, 10, 65, 68, 67, 79 }; + /** Standard mode buffer start pattern */ + private static final byte[] STANDARD_BUFFER_START = { 2, 10, 65, 68, 83, 67 }; + + /** Current TIC mode detected during operation */ + private TICMode currentMode; + + /** + * Creates a new TIC serial port channel instance with the specified configuration. + * + * @param configuration the TIC serial port configuration containing port settings + * and TIC mode parameters + * @throws ChannelException if the configuration is invalid or channel creation fails + */ + public ChannelTICSerialPort(ChannelTICSerialPortConfiguration configuration) throws ChannelException + { + super(configuration); + this.currentMode = null; + } + + + @Override + public void setup(ChannelConfiguration configuration) throws ChannelException + { + if (configuration == null) + { + ChannelException.raiseInvalidConfiguration("null"); + } + if (!(configuration instanceof ChannelTICSerialPortConfiguration)) + { + ChannelException.raiseInvalidConfigurationType(configuration, ChannelTICSerialPortConfiguration.class.getSimpleName()); + } + super.setup(configuration); + } + + @Override + public ChannelTICSerialPortConfiguration getConfiguration() + { + return (ChannelTICSerialPortConfiguration) this.configuration; + } + + /** + * Gets the current TIC mode being used by the channel. + * Returns the detected mode if available, otherwise returns the selected mode from configuration. + * + * @return the current TIC mode (HISTORIC, STANDARD, or AUTO) + */ + public TICMode getMode() + { + return (this.getCurrentMode() != null) ? this.getCurrentMode() : this.getSelectedMode(); + } + + /** + * Gets the TIC mode selected in the channel configuration. + * + * @return the selected TIC mode from configuration + */ + public TICMode getSelectedMode() + { + return this.getConfiguration().getTicMode(); + } + + /** + * Gets the currently detected TIC mode during operation. + * This may differ from the selected mode if auto-detection is enabled. + * + * @return the currently detected TIC mode, or null if not yet detected + */ + public TICMode getCurrentMode() + { + return this.currentMode; + } + + @Override + public byte[] read() throws ChannelException + { + long beginTime = System.currentTimeMillis(); + long elapsedTime = 0; + long timeout = this.getSyncReadTimeout(); + BytesArray buffer = new BytesArray(); + byte[] ticFrame = null; + int startOfFrame = -1, endOfFrame = -1; + + while (elapsedTime < timeout || timeout == 0) + { + if (this.available() > 0) + { + buffer.addAll(super.read(1)); + if (startOfFrame == -1) + { + startOfFrame = buffer.indexOf(START_PATTERN); + if (startOfFrame != -1) + { + endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); + if (endOfFrame != -1) + { + ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); + break; + } + } + } + else + { + endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); + if (endOfFrame != -1) + { + ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); + break; + } + } + } + else + { + Time.sleep(RECEIVE_DATA_POLLING_PERIOD); + elapsedTime = (System.currentTimeMillis() - beginTime); + } + } + + return ticFrame; + } + + + @Override + protected void receiveData() + { + if (this.currentMode == null) + { + if (this.autoDetectMode() == null) + { + this.onReadTimeout(); + return; + } + } + try + { + byte[] ticFrame = this.read(); + + if (ticFrame == null) + { + this.onReadTimeout(); + } + else + { + this.notifyOnDataRead(ticFrame); + } + } + catch (ChannelException e) + { + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), "TIC read failed: " + e.getErrorInfo()); + } + } + + + private void onReadTimeout() + { + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_TIMEOUT.getCode(ERROR_CODE_OFFSET), "TIC read timeout"); + this.configurePort(); + this.currentMode = null; + } + + private void configurePort() + { + this.closePort(); + this.openPort(); + try + { + this.flush(); + } + catch (ChannelException e) + { + } + } + + private void setSelectedMode(TICMode ticMode) + { + try + { + this.getConfiguration().setTicMode(ticMode); + } + catch (DataDictionaryException e) + { + this.logger.error("Cannot set TIC mode " + ticMode, e); + } + } + + private boolean checkAndUpdateMode(TICMode ticMode) + { + boolean result = false; + this.setSelectedMode(ticMode); + this.configurePort(); + try + { + byte[] ticFrame = this.read(); + if (ticFrame != null && ticFrame.length > HISTORIC_BUFFER_START.length) + { + byte[] ticFrameStart = new byte[HISTORIC_BUFFER_START.length]; + System.arraycopy(ticFrame, 0, ticFrameStart, 0, ticFrameStart.length); + if (ticMode == TICMode.HISTORIC) + { + if (Arrays.equals(ticFrameStart, HISTORIC_BUFFER_START)) + { + result = true; + } + } + else if (ticMode == TICMode.STANDARD) + { + if (Arrays.equals(ticFrameStart, STANDARD_BUFFER_START)) + { + result = true; + } + } + } + } + catch (ChannelException e) + { + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), "TIC read failed: " + e.getErrorInfo()); + } + if (result) + { + this.currentMode = ticMode; + } + this.setSelectedMode(TICMode.AUTO); + + return result; + } + + private TICMode autoDetectMode() + { + if (this.getSelectedMode() != TICMode.AUTO) + { + this.currentMode = this.getSelectedMode(); + return this.getSelectedMode(); + } + this.logger.debug("Auto detecting TIC Mode"); + if (!this.checkAndUpdateMode(TICMode.HISTORIC)) + { + if (!this.checkAndUpdateMode(TICMode.STANDARD)) + { + this.logger.debug("TIC Mode not detected"); + + return null; + } + this.logger.debug("TIC Mode STANDARD detected"); + + return TICMode.STANDARD; + } + this.logger.debug("TIC Mode HISTORIC detected"); + + return TICMode.HISTORIC; + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java index ecfeff6..bbd6e07 100644 --- a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java +++ b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java @@ -1,250 +1,262 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.channels; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.io.channels.serialport.ChannelSerialPortConfiguration; -import enedis.lab.io.channels.serialport.Parity; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; - -/** - * ChannelTICSerialPortConfiguration class - * - * Generated - */ -public class ChannelTICSerialPortConfiguration extends ChannelSerialPortConfiguration -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_TIC_MODE = "ticMode"; - - protected static final Number BAUDRATE_HISTORIC = 1200; - protected static final Number BAUDRATE_STANDARD = 9600; - protected static final Number[] BAUDRATE_ACCEPTED_VALUES = { BAUDRATE_HISTORIC, BAUDRATE_STANDARD }; - protected static final Parity PARITY_ACCEPTED_VALUE = Parity.EVEN; - protected static final Number DATA_BITS_ACCEPTED_VALUE = 7; - protected static final Number STOP_BITS_ACCEPTED_VALUE = 1.0d; - protected static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kTicMode; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ChannelTICSerialPortConfiguration() - { - super(); - this.loadKeyDescriptors(); - - this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); - this.kParity.setAcceptedValues(PARITY_ACCEPTED_VALUE); - this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUE); - this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ChannelTICSerialPortConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ChannelTICSerialPortConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public ChannelTICSerialPortConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param portName - * @param ticMode - * @throws DataDictionaryException - */ - public ChannelTICSerialPortConfiguration(String name, String portName, TICMode ticMode) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setPortName(portName); - this.setTicMode(ticMode); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ChannelSerialPortConfiguration - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TIC_MODE)) - { - this.setTicMode(TIC_MODE_DEFAULT_VALUE); - } - switch (this.getTicMode()) - { - case HISTORIC: - { - this.setBaudrate(BAUDRATE_HISTORIC); - this.setParity(PARITY_ACCEPTED_VALUE); - this.setDataBits(DATA_BITS_ACCEPTED_VALUE); - this.setStopBits(STOP_BITS_ACCEPTED_VALUE); - break; - } - case STANDARD: - case AUTO: - default: - { - this.setBaudrate(BAUDRATE_STANDARD); - this.setParity(PARITY_ACCEPTED_VALUE); - this.setDataBits(DATA_BITS_ACCEPTED_VALUE); - this.setStopBits(STOP_BITS_ACCEPTED_VALUE); - } - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get tic mode - * - * @return the tic mode - */ - public TICMode getTicMode() - { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Set tic mode - * - * @param ticMode - * @throws DataDictionaryException - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException - { - this.setTicMode((Object) ticMode); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setTicMode(Object ticMode) throws DataDictionaryException - { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - switch (this.getTicMode()) - { - case HISTORIC: - this.setBaudrate(BAUDRATE_HISTORIC); - break; - case STANDARD: - case AUTO: - this.setBaudrate(BAUDRATE_STANDARD); - break; - default: - break; - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); - this.keys.add(this.kTicMode); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.channels; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import enedis.lab.io.channels.serialport.ChannelSerialPortConfiguration; +import enedis.lab.io.channels.serialport.Parity; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.KeyDescriptor; +import enedis.lab.types.datadictionary.KeyDescriptorEnum; + +/** + * Configuration class for TIC serial port channels. + *

    + * This class extends {@link ChannelSerialPortConfiguration} to provide specific + * configuration for TIC protocol communication over serial ports. It handles + * TIC mode selection (HISTORIC, STANDARD, or AUTO) and automatically configures + * the appropriate serial port parameters based on the selected TIC mode. + *

    + * The class supports both manual TIC mode selection and automatic detection, + * with different baud rates for HISTORIC (1200 bps) and STANDARD (9600 bps) modes. + * + * @author Enedis Smarties team + */ +public class ChannelTICSerialPortConfiguration extends ChannelSerialPortConfiguration +{ + + protected static final String KEY_TIC_MODE = "ticMode"; + + protected static final Number BAUDRATE_HISTORIC = 1200; + protected static final Number BAUDRATE_STANDARD = 9600; + protected static final Number[] BAUDRATE_ACCEPTED_VALUES = { BAUDRATE_HISTORIC, BAUDRATE_STANDARD }; + protected static final Parity PARITY_ACCEPTED_VALUE = Parity.EVEN; + protected static final Number DATA_BITS_ACCEPTED_VALUE = 7; + protected static final Number STOP_BITS_ACCEPTED_VALUE = 1.0d; + protected static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; + + + + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kTicMode; + + + protected ChannelTICSerialPortConfiguration() + { + super(); + this.loadKeyDescriptors(); + + this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); + this.kParity.setAcceptedValues(PARITY_ACCEPTED_VALUE); + this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUE); + this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUE); + } + + /** + * Creates a new TIC serial port configuration from a map of parameters. + *

    + * The map should contain configuration parameters that will be copied to this + * configuration object. The TIC mode and serial port parameters will be + * automatically configured based on the provided values. + * + * @param map the map containing configuration parameters + * @throws DataDictionaryException if the map contains invalid parameters + */ + public ChannelTICSerialPortConfiguration(Map map) throws DataDictionaryException + { + this(); + this.copy(fromMap(map)); + } + + /** + * Creates a new TIC serial port configuration by copying from another data dictionary. + *

    + * This constructor creates a new configuration object by copying all parameters + * from the provided data dictionary. The TIC mode and serial port parameters + * will be automatically configured based on the copied values. + * + * @param other the data dictionary to copy configuration from + * @throws DataDictionaryException if the data dictionary contains invalid parameters + */ + public ChannelTICSerialPortConfiguration(DataDictionary other) throws DataDictionaryException + { + this(); + this.copy(other); + } + + /** + * Creates a new TIC serial port configuration with the specified name and file. + *

    + * This constructor initializes the configuration with default values and associates + * it with the provided name and configuration file. The TIC mode will be set + * to AUTO by default, allowing automatic detection of the appropriate mode. + * + * @param name the configuration name + * @param file the configuration file + */ + public ChannelTICSerialPortConfiguration(String name, File file) + { + this(); + this.init(name, file); + } + + /** + * Creates a new TIC serial port configuration with specific parameters. + *

    + * This constructor creates a configuration with the specified name, port name, + * and TIC mode. The serial port parameters (baud rate, parity, data bits, stop bits) + * will be automatically configured based on the selected TIC mode. + * + * @param name the configuration name + * @param portName the serial port name (e.g., "/dev/ttyUSB0" on Linux, "COM1" on Windows) + * @param ticMode the TIC mode (HISTORIC, STANDARD, or AUTO) + * @throws DataDictionaryException if the provided parameters are invalid + */ + public ChannelTICSerialPortConfiguration(String name, String portName, TICMode ticMode) throws DataDictionaryException + { + this(); + + this.setName(name); + this.setPortName(portName); + this.setTicMode(ticMode); + + this.checkAndUpdate(); + } + + + @Override + protected void updateOptionalParameters() throws DataDictionaryException + { + if (!this.exists(KEY_TIC_MODE)) + { + this.setTicMode(TIC_MODE_DEFAULT_VALUE); + } + switch (this.getTicMode()) + { + case HISTORIC: + { + this.setBaudrate(BAUDRATE_HISTORIC); + this.setParity(PARITY_ACCEPTED_VALUE); + this.setDataBits(DATA_BITS_ACCEPTED_VALUE); + this.setStopBits(STOP_BITS_ACCEPTED_VALUE); + break; + } + case STANDARD: + case AUTO: + default: + { + this.setBaudrate(BAUDRATE_STANDARD); + this.setParity(PARITY_ACCEPTED_VALUE); + this.setDataBits(DATA_BITS_ACCEPTED_VALUE); + this.setStopBits(STOP_BITS_ACCEPTED_VALUE); + } + } + super.updateOptionalParameters(); + } + + + /** + * Gets the current TIC mode configuration. + *

    + * Returns the TIC mode that has been set for this configuration. The mode + * determines the communication protocol and serial port parameters used + * for TIC communication. + * + * @return the current TIC mode (HISTORIC, STANDARD, or AUTO) + */ + public TICMode getTicMode() + { + return (TICMode) this.data.get(KEY_TIC_MODE); + } + + /** + * Sets the TIC mode for this configuration. + *

    + * When the TIC mode is set, the serial port parameters are automatically + * updated to match the requirements of the selected mode: + *

      + *
    • HISTORIC: 1200 bps, 7 data bits, even parity, 1 stop bit
    • + *
    • STANDARD: 9600 bps, 7 data bits, even parity, 1 stop bit
    • + *
    • AUTO: Automatically detects the appropriate mode
    • + *
    + * + * @param ticMode the TIC mode to set + * @throws DataDictionaryException if the TIC mode is invalid + */ + public void setTicMode(TICMode ticMode) throws DataDictionaryException + { + this.setTicMode((Object) ticMode); + } + + + /** + * Sets the TIC mode using an object value and automatically configures + * the baud rate based on the selected mode. + *

    + * This protected method is used internally to set the TIC mode from various + * object types and automatically adjusts the baud rate: + *

      + *
    • HISTORIC mode: sets baud rate to 1200 bps
    • + *
    • STANDARD or AUTO mode: sets baud rate to 9600 bps
    • + *
    + * + * @param ticMode the TIC mode object to set + * @throws DataDictionaryException if the TIC mode object is invalid + */ + protected void setTicMode(Object ticMode) throws DataDictionaryException + { + this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); + switch (this.getTicMode()) + { + case HISTORIC: + this.setBaudrate(BAUDRATE_HISTORIC); + break; + case STANDARD: + case AUTO: + this.setBaudrate(BAUDRATE_STANDARD); + break; + default: + break; + } + } + + + /** + * Loads and initializes the key descriptors for TIC mode configuration. + *

    + * This private method sets up the key descriptor for the TIC mode parameter, + * which is used for validation and conversion of TIC mode values. It creates + * a key descriptor for the TICMode enum and adds it to the list of keys + * managed by this configuration object. + *

    + * If an error occurs during initialization, it wraps the DataDictionaryException + * in a RuntimeException to maintain the constructor's contract. + */ + private void loadKeyDescriptors() + { + try + { + this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); + this.keys.add(this.kTicMode); + + this.addAllKeyDescriptor(this.keys); + } + catch (DataDictionaryException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java index 5a47500..cb319b2 100644 --- a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java +++ b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java @@ -1,823 +1,1263 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -import org.apache.commons.lang3.SystemUtils; - -import enedis.lab.io.channels.ChannelConfiguration; -import enedis.lab.io.channels.ChannelException; -import enedis.lab.io.channels.ChannelPhysical; -import enedis.lab.io.channels.ChannelStatus; -import enedis.lab.io.serialport.SerialPortDescriptor; -import enedis.lab.io.serialport.SerialPortFinderBase; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.SystemError; -import jssc.SerialPort; -import jssc.SerialPortEvent; -import jssc.SerialPortEventListener; -import jssc.SerialPortException; -import jssc.SerialPortTimeoutException; - -/** - * Channel serial port - */ -public class ChannelSerialPort extends ChannelPhysical implements SerialPortEventListener -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final int ERROR_CODE_OFFSET = 1000; - private static final int DEFAULT_DELAY = 500; - private static final int DELAY_REOPEN = DEFAULT_DELAY * 10; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Check if the port is found - * - * @param portId - * @param portName - * @return true if the specified port is found - */ - public static boolean isPortFound(String portId, String portName) - { - return SerialPortFinderBase.getInstance().findByPortIdOrPortName(portId, portName) != null; - } - - /** - * Find the port name corresponding to the given portId or portName. portId has a higher priority - * - * @param portId - * @param portName - * @return port name - */ - public static String findPortName(String portId, String portName) - { - SerialPortDescriptor descriptor = null; - if (portId != null) - { - descriptor = SerialPortFinderBase.getInstance().findByPortId(portId); - } - else - { - descriptor = SerialPortFinderBase.getInstance().findByPortName(portName); - } - - return (descriptor != null) ? descriptor.getPortName() : null; - } - - /** - * Convert the parity provided by the SerialPortConfiguration to a value usable by the channel - * - * @param parity - * @return parity - */ - public static int parityFromConfiguration(Parity parity) - { - int retVal = -1; - - switch (parity) - { - case EVEN: - retVal = SerialPort.PARITY_EVEN; - break; - case MARK: - retVal = SerialPort.PARITY_MARK; - break; - case NONE: - retVal = SerialPort.PARITY_NONE; - break; - case ODD: - retVal = SerialPort.PARITY_ODD; - break; - case SPACE: - retVal = SerialPort.PARITY_SPACE; - break; - default: /* Cas non atteignable */ - } - - return retVal; - } - - /** - * Convert the stopBits parameter provided by the SerialPortConfiguration to a value usable by the channel - * - * @param stop_bits - * @return stopBits - */ - public static int stopBitsFromConfiguration(float stop_bits) - { - int retVal = -1; - - if (1 == stop_bits) - { - retVal = SerialPort.STOPBITS_1; - } - - else if (1.5 == stop_bits) - { - retVal = SerialPort.STOPBITS_1_5; - } - - else if (2 == stop_bits) - { - retVal = SerialPort.STOPBITS_2; - } - - return retVal; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private SerialPort portHandler = null; - - private String previousPortName; - private String currentPortName; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor: create an instance from a ChannelConfiguration - * - * @param configuration - * @throws ChannelException - */ - public ChannelSerialPort(ChannelConfiguration configuration) throws ChannelException - { - super(configuration); - this.setPeriod(DEFAULT_DELAY); - - this.currentPortName = this.findPortName(); - this.previousPortName = this.currentPortName; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Channel - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void start() - { - this.logger.info("Channel " + this.getName() + " start (" + this.findPortName() + ")"); - this.openPort(); - super.start(); - } - - @Override - public void stop() - { - this.logger.info("Channel " + this.getName() + " stop (" + this.findPortName() + ")"); - super.stop(); - this.closePort(); - } - - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException - { - if (configuration == null) - { - ChannelException.raiseInvalidConfiguration("null"); - } - if (!(configuration instanceof ChannelSerialPortConfiguration)) - { - ChannelException.raiseInvalidConfigurationType(configuration, ChannelSerialPortConfiguration.class.getSimpleName()); - } - super.setup(configuration); - } - - @Override - public byte[] read() throws ChannelException - { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - buffer = this.portHandler.readBytes(); - } - catch (SerialPortException exception) - { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - @Override - public void write(byte[] data) throws ChannelException - { - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - this.portHandler.writeBytes(data); - } - catch (SerialPortException exception) - { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot write bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - } - - @Override - public ChannelSerialPortConfiguration getConfiguration() - { - return (ChannelSerialPortConfiguration) this.configuration.clone(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ChannelBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void setup() throws ChannelException - { - /* 1. Save current state */ - ChannelStatus currentStatus = this.status; - /* 2. Stop serial port */ - this.stop(); - /* 3. update path if symbolic link */ - this.updateRealPath(); - /* 4. Create serial port handler */ - if ((null == this.portHandler) || (false == this.getPortName().equals(this.portHandler.getPortName()))) - { - this.portHandler = new SerialPort(this.getPortName()); - } - /* 5. Restore current state */ - if (ChannelStatus.STARTED == currentStatus) - { - this.start(); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortEventListener - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void serialEvent(SerialPortEvent event) - { - switch (event.getEventType()) - { - case SerialPortEvent.RXCHAR: - this.receiveData(); - break; - default: /* Aucune action */ - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Runnable - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void process() - { - int delay = DEFAULT_DELAY; - - if (this.status == ChannelStatus.STARTED) - { - if (!this.isPortFound()) - { - this.onPortNotFound(); - } - else if (this.hasPortChanged()) - { - this.onPortNameChanged(); - } - else if (!this.hasEventListener()) - { - this.receiveData(); - } - } - else - { - if (this.isPortFound()) - { - this.openPort(); - if (this.status == ChannelStatus.ERROR) - { - delay = DELAY_REOPEN; - } - } - } - this.setPeriod(delay); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Blocking Read - * - * @param bytesCount - * @return read bytes - * @throws ChannelException - */ - public byte[] read(int bytesCount) throws ChannelException - { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - buffer = this.portHandler.readBytes(bytesCount); - } - catch (SerialPortException exception) - { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - /** - * Blocking read with timeout - * - * @param bytesCount - * @param timeout - * @return read bytes - * @throws ChannelException - */ - public byte[] read(int bytesCount, int timeout) throws ChannelException - { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - buffer = this.portHandler.readBytes(bytesCount, timeout); - } - catch (SerialPortException exception) - { - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - catch (SerialPortTimeoutException exception) - { - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - /** - * Get available bytes count - * - * @return Available bytes count - * @throws ChannelException - * if serial port not open or internal error occurs - */ - public int available() throws ChannelException - { - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - return this.portHandler.getInputBufferBytesCount(); - } - catch (SerialPortException exception) - { - this.logger.error("Cannot get bytes available", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return 0; - } - - /** - * Flush port - * - * @throws ChannelException - */ - public void flush() throws ChannelException - { - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - int mask = SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_RXABORT | SerialPort.PURGE_TXCLEAR | SerialPort.PURGE_TXABORT; - if (!this.portHandler.purgePort(mask)) - { - this.logger.error("Channel " + this.getName() + " failed to purge port " + this.getPortName() + " (" + SystemError.getMessage() + ") "); - } - } - catch (SerialPortException exception) - { - this.logger.error("Cannot purge port", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - } - - /** - * - * @return the identifier of the port. If the parameter has not been set, 'null' is returned. - */ - public String getPortId() - { - return this.getChannelConfiguration().getPortId(); - } - - /** - * - * @return the name of the port. If the parameter has not been set, 'null' is returned. - */ - public String getPortName() - { - return this.getChannelConfiguration().getPortName(); - } - - /** - * - * @return the name of the port opened. If the port has not been opened, 'null' is returned. - */ - public String getPortNameOpened() - { - return (this.portHandler != null) ? this.portHandler.getPortName() : null; - } - - /** - * Find the port name associated with configuration - * - * @return Port name found or null if nothing found - */ - public String findPortName() - { - return findPortName(this.getPortId(), this.getPortName()); - } - - /** - * @return the current baudrate value. If the Baudrate has not been set (port has not been used), -1 is returned - */ - public int getBaudrate() - { - return this.getChannelConfiguration().getBaudrate().intValue(); - } - - /** - * @return the number of bits used for data (5, 6, 7, 8). If the parameter does not exist in channel configuration - * or is not consistent, the value '-1' is returned. - */ - public int getDataBits() - { - return this.getChannelConfiguration().getDataBits().intValue(); - } - - /** - * @return the current number of bits used for stop (1.0 , 1.5 , 2.0). - * - * NB : If the port has been used, the method returns -1. - */ - public float getStopBits() - { - return this.getChannelConfiguration().getStopBits().floatValue(); - } - - /** - * @return the current parity used by the port - * - * NB : If the port has been used, the method returns null. - */ - public Parity getParity() - { - return this.getChannelConfiguration().getParity(); - } - - /** - * @return the timeout used for a synchronous read operation - */ - public int getSyncReadTimeout() - { - return this.getChannelConfiguration().getSyncReadTimeout().intValue(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected boolean hasEventListener() - { - return false; - } - - protected boolean addEventListener() - { - if (this.portHandler == null) - { - return false; - } - int mask = SerialPort.MASK_RXCHAR + SerialPort.MASK_CTS + SerialPort.MASK_DSR; - try - { - if (!this.portHandler.setEventsMask(mask)) - { - this.logger.error("Channel " + this.getName() + " failed to set event mask on port " + this.getPortName() + " (" + SystemError.getMessage() + ") "); - return false; - } - } - catch (SerialPortException exception) - { - this.logger.error("Channel " + this.getName() + " failed to set event mask on port " + this.getPortName() + " : " + exception.getMessage()); - return false; - } - try - { - this.portHandler.addEventListener(this); - } - catch (SerialPortException e) - { - this.logger.error("Channel " + this.getName() + " failed to add listener :" + e.getMessage(), e); - return false; - } - - return true; - } - - protected boolean removeEventListener() - { - if (this.portHandler == null) - { - return false; - } - try - { - if (!this.portHandler.removeEventListener()) - { - this.logger.error("Channel " + this.getName() + " failed to remove listener"); - return false; - } - } - catch (SerialPortException e) - { - this.logger.error("Channel " + this.getName() + " failed to remove listener :" + e.getMessage(), e); - return false; - } - - return true; - } - - protected void receiveData() - { - try - { - if (this.available() > 0) - { - byte[] buffer = this.read(); - this.notifyOnDataRead(buffer); - } - } - catch (ChannelException exception) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), exception.getMessage()); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private boolean hasPortChanged() - { - boolean portChanged = false; - - this.currentPortName = this.findPortName(); - - if (this.currentPortName != null && !this.currentPortName.equalsIgnoreCase(this.previousPortName)) - { - portChanged = true; - } - - return portChanged; - } - - private void onPortNameChanged() - { - String errorMessage = "Port name has changed ( " + this.previousPortName + " --> " + this.currentPortName + " )"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_NAME_HAS_CHANGED.getCode(ERROR_CODE_OFFSET), errorMessage); - this.previousPortName = this.currentPortName; - } - - private void updateRealPath() throws ChannelException - { - if (SystemUtils.IS_OS_LINUX) - { - String realPortName; - try - { - Process p = Runtime.getRuntime().exec("realpath " + this.getPortName()); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream())); - realPortName = stdInput.readLine(); - this.getChannelConfiguration().setPortName(realPortName); - } - catch (IOException | DataDictionaryException e) - { - throw new ChannelException(ChannelException.ERRCODE_INVALID_CONFIGURATION, "Cannot resolve realpath (" + e.getMessage() + ")"); - } - } - } - - private ChannelSerialPortConfiguration getChannelConfiguration() - { - return (ChannelSerialPortConfiguration) this.configuration; - } - - protected void openPort() - { - /* 1. Check if serial port is already opened */ - if (this.portHandler != null && this.portHandler.isOpened()) - { - return; - } - /* 2. Open serial port */ - String portNameFound = this.findPortName(); - try - { - this.logger.info("Channel " + this.getName() + " opening port " + portNameFound); - if (this.portHandler == null) - { - this.portHandler = new SerialPort(portNameFound); - } - if (!this.portHandler.openPort()) - { - this.logger.error("Channel " + this.getName() + " failed to open port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); - return; - } - this.logger.info("Channel " + this.getName() + " configuring port " + portNameFound); - if (!this.portHandler.setParams(this.getBaudrate(), this.getDataBits(), stopBitsFromConfiguration(this.getStopBits()), parityFromConfiguration(this.getParity()))) - { - this.logger.error("Channel " + this.getName() + " failed to configure port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); - return; - } - } - catch (SerialPortException exception) - { - String errorMessage = "Channel " + this.getName() + " failed to open port " + this.getPortNameOpened() + " : " + exception.getMessage(); - this.logger.error(errorMessage); - if (exception.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_BUSY.getCode(ERROR_CODE_OFFSET), errorMessage); - } - this.closePortForced(); - return; - } - /* 3. Add serial event listener */ - if (this.hasEventListener()) - { - if (!this.addEventListener()) - { - return; - } - } - this.setStatus(ChannelStatus.STARTED); - } - - protected void closePort() - { - /* 1. Check if serial port is already closed */ - if (this.portHandler == null || !this.portHandler.isOpened()) - { - return; - } - /* 2. Remove serial event listener */ - if (this.hasEventListener()) - { - this.removeEventListener(); - } - /* 3. Close serial port */ - try - { - this.logger.info("Channel " + this.getName() + " closing port " + this.getPortNameOpened()); - if (!this.portHandler.closePort()) - { - this.logger.error("Channel " + this.getName() + " failed to close port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); - return; - } - } - catch (SerialPortException exception) - { - this.logger.error("Channel " + this.getName() + " failed to close port " + this.getPortNameOpened() + " : " + exception.getMessage()); - return; - } - this.setStatus(ChannelStatus.STOPPED); - } - - protected void closePortForced() - { - try - { - this.portHandler.closePort(); - } - catch (SerialPortException exception) - { - this.logger.error("Channel " + this.getName() + " fail on close : close " + this.getPortNameOpened() + " failed due to " + exception.getMessage()); - } - this.portHandler = new SerialPort(this.findPortName()); - } - - protected boolean isPortFound() - { - return isPortFound(this.getPortId(), this.getPortName()); - } - - protected boolean isPortOpenedFound() - { - String portNameOpened = this.getPortNameOpened(); - if (portNameOpened == null) - { - return false; - } - String portNameFound = this.findPortName(); - - return portNameOpened.equals(portNameFound); - } - - protected void onPortNotFound() - { - String errorMessage = "Channel " + this.getName() + " : serial port " + this.findPortName() + " not found"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); - } - - protected void onPortOpenedNotFound() - { - String errorMessage = "Channel " + this.getName() + " : serial port " + this.getPortNameOpened() + " opened not found"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_OPENED_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels.serialport; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.apache.commons.lang3.SystemUtils; + +import enedis.lab.io.channels.ChannelConfiguration; +import enedis.lab.io.channels.ChannelException; +import enedis.lab.io.channels.ChannelPhysical; +import enedis.lab.io.channels.ChannelStatus; +import enedis.lab.io.serialport.SerialPortDescriptor; +import enedis.lab.io.serialport.SerialPortFinderBase; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.SystemError; +import jssc.SerialPort; +import jssc.SerialPortEvent; +import jssc.SerialPortEventListener; +import jssc.SerialPortException; +import jssc.SerialPortTimeoutException; + +/** + * Serial port communication channel implementation for TIC protocol. + * + *

    This class extends {@link ChannelPhysical} and implements {@link SerialPortEventListener} to provide + * a robust serial port communication channel. It handles automatic port discovery, configuration, + * connection management, and real-time data transmission for TIC-compliant devices. + * + *

    Key features include: + *

      + *
    • Automatic port detection and reconnection when devices are plugged/unplugged
    • + *
    • Support for both port ID and port name identification methods
    • + *
    • Configurable serial communication parameters (baud rate, parity, data bits, stop bits)
    • + *
    • Event-driven data reception with optional timeout handling
    • + *
    • Symbolic link resolution for Linux systems
    • + *
    • Error detection and automatic recovery mechanisms
    • + *
    + * + *

    The channel operates in a periodic polling mode to monitor port availability and automatically + * handles port changes, busy conditions, and communication errors. It supports both synchronous + * and asynchronous data operations with proper exception handling. + * + * @author Enedis Smarties team + * @see ChannelPhysical + * @see ChannelSerialPortConfiguration + * @see SerialPortEventListener + * @see ChannelStatus + * @see ChannelSerialPortErrorCode + */ +public class ChannelSerialPort extends ChannelPhysical implements SerialPortEventListener +{ + /** Error code offset for serial port specific errors. */ + protected static final int ERROR_CODE_OFFSET = 1000; + + /** Default polling delay in milliseconds for port monitoring. */ + private static final int DEFAULT_DELAY = 500; + + /** Extended delay in milliseconds for port reopening attempts. */ + private static final int DELAY_REOPEN = DEFAULT_DELAY * 10; + + + /** + * Checks if a serial port is available on the system. + * + *

    This method queries the system to determine if a port with the specified identifier + * or name is currently available. The port ID takes precedence over port name if both + * are provided. + * + * @param portId the unique port identifier (may be null) + * @param portName the port name or path (may be null) + * @return true if the port is found and available, false otherwise + * @see SerialPortFinderBase#findByPortIdOrPortName(String, String) + */ + public static boolean isPortFound(String portId, String portName) + { + return SerialPortFinderBase.getInstance().findByPortIdOrPortName(portId, portName) != null; + } + + /** + * Finds the actual port name corresponding to the given port ID or port name. + * + *

    This method resolves the port identifier to its actual system port name. The port ID + * parameter takes priority over the port name parameter if both are provided. This is useful + * for converting abstract port identifiers to concrete system paths. + * + * @param portId the unique port identifier (has priority if not null) + * @param portName the port name or path (used as fallback if portId is null) + * @return the resolved port name, or null if no matching port is found + * @see SerialPortFinderBase#findByPortId(String) + * @see SerialPortFinderBase#findByPortName(String) + */ + public static String findPortName(String portId, String portName) + { + SerialPortDescriptor descriptor = null; + if (portId != null) + { + descriptor = SerialPortFinderBase.getInstance().findByPortId(portId); + } + else + { + descriptor = SerialPortFinderBase.getInstance().findByPortName(portName); + } + + return (descriptor != null) ? descriptor.getPortName() : null; + } + + /** + * Converts a {@link Parity} enumeration value to the corresponding JSSC library constant. + * + *

    This utility method maps the application-level parity enumeration to the underlying + * serial communication library constants required for port configuration. + * + * @param parity the parity setting from the configuration + * @return the corresponding JSSC SerialPort parity constant, or -1 for unsupported values + * @see SerialPort#PARITY_EVEN + * @see SerialPort#PARITY_ODD + * @see SerialPort#PARITY_NONE + * @see SerialPort#PARITY_MARK + * @see SerialPort#PARITY_SPACE + */ + public static int parityFromConfiguration(Parity parity) + { + int retVal = -1; + + switch (parity) + { + case EVEN: + retVal = SerialPort.PARITY_EVEN; + break; + case MARK: + retVal = SerialPort.PARITY_MARK; + break; + case NONE: + retVal = SerialPort.PARITY_NONE; + break; + case ODD: + retVal = SerialPort.PARITY_ODD; + break; + case SPACE: + retVal = SerialPort.PARITY_SPACE; + break; + default: /* Cas non atteignable */ + } + + return retVal; + } + + /** + * Converts a stop bits configuration value to the corresponding JSSC library constant. + * + *

    This utility method maps the configured stop bits value (1.0, 1.5, or 2.0) to the + * underlying serial communication library constants required for port configuration. + * + * @param stop_bits the stop bits value from the configuration (1.0, 1.5, or 2.0) + * @return the corresponding JSSC SerialPort stop bits constant, or -1 for unsupported values + * @see SerialPort#STOPBITS_1 + * @see SerialPort#STOPBITS_1_5 + * @see SerialPort#STOPBITS_2 + */ + public static int stopBitsFromConfiguration(float stop_bits) + { + int retVal = -1; + + if (1 == stop_bits) + { + retVal = SerialPort.STOPBITS_1; + } + + else if (1.5 == stop_bits) + { + retVal = SerialPort.STOPBITS_1_5; + } + + else if (2 == stop_bits) + { + retVal = SerialPort.STOPBITS_2; + } + + return retVal; + } + + /** JSSC SerialPort instance for handling low-level serial communication. */ + private SerialPort portHandler = null; + + /** Previously detected port name, used for change detection. */ + private String previousPortName; + + /** Currently detected port name, used for change detection. */ + private String currentPortName; + + /** + * Constructs a new serial port channel with the specified configuration. + * + *

    This constructor initializes the channel with the provided configuration and sets up + * the initial port detection. The channel will automatically resolve the port name based + * on the configuration's port ID or port name settings. + * + *

    The constructor sets up the periodic polling mechanism with a default delay and + * initializes the port change detection system. + * + * @param configuration the channel configuration containing port settings and parameters + * @throws ChannelException if the configuration is invalid or port cannot be resolved + * @throws IllegalArgumentException if configuration is null or not a ChannelSerialPortConfiguration + * @see ChannelSerialPortConfiguration + */ + public ChannelSerialPort(ChannelConfiguration configuration) throws ChannelException + { + super(configuration); + this.setPeriod(DEFAULT_DELAY); + + this.currentPortName = this.findPortName(); + this.previousPortName = this.currentPortName; + } + + + /** + * Starts the serial port channel and begins communication. + * + *

    This method initiates the channel by opening the serial port connection and starting + * the underlying periodic task. The port is opened with the configuration parameters + * specified during construction. + * + *

    If the port cannot be opened, the channel will enter an error state and attempt + * to reconnect periodically. + * + * @see #stop() + * @see #openPort() + */ + @Override + public void start() + { + this.logger.info("Channel " + this.getName() + " start (" + this.findPortName() + ")"); + this.openPort(); + super.start(); + } + + /** + * Stops the serial port channel and closes the connection. + * + *

    This method gracefully stops the channel by first stopping the underlying periodic task + * and then closing the serial port connection. All event listeners are removed and the + * port is properly released. + * + *

    The channel can be restarted by calling {@link #start()} again. + * + * @see #start() + * @see #closePort() + */ + @Override + public void stop() + { + this.logger.info("Channel " + this.getName() + " stop (" + this.findPortName() + ")"); + super.stop(); + this.closePort(); + } + + /** + * Configures the channel with a new configuration. + * + *

    This method validates and applies a new configuration to the channel. The configuration + * must be a valid {@link ChannelSerialPortConfiguration} instance. If the channel is currently + * started, it will be stopped and restarted with the new configuration. + * + *

    The configuration update includes port parameter changes, which may require reopening + * the serial port connection. + * + * @param configuration the new channel configuration to apply + * @throws ChannelException if the configuration is null, invalid, or not a serial port configuration + * @see ChannelSerialPortConfiguration + */ + @Override + public void setup(ChannelConfiguration configuration) throws ChannelException + { + if (configuration == null) + { + ChannelException.raiseInvalidConfiguration("null"); + } + if (!(configuration instanceof ChannelSerialPortConfiguration)) + { + ChannelException.raiseInvalidConfigurationType(configuration, ChannelSerialPortConfiguration.class.getSimpleName()); + } + super.setup(configuration); + } + + /** + * Reads available data from the serial port. + * + *

    This method performs a non-blocking read operation to retrieve all available data + * from the serial port input buffer. The method returns immediately with whatever data + * is currently available, or an empty array if no data is present. + * + *

    The channel must be in a STARTED state and the port must be open for this operation + * to succeed. + * + * @return array of bytes read from the port, or empty array if no data available + * @throws ChannelException if the channel is not ready, port is not open, or read operation fails + * @see #read(int) + * @see #read(int, int) + */ + @Override + public byte[] read() throws ChannelException + { + byte[] buffer = null; + + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + buffer = this.portHandler.readBytes(); + } + catch (SerialPortException exception) + { + this.setStatus(ChannelStatus.ERROR); + this.logger.error("Cannot read bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + + return buffer; + } + + /** + * Writes data to the serial port. + * + *

    This method transmits the provided data bytes to the serial port. The operation + * is synchronous and will block until all data has been written to the port buffer. + * + *

    The channel must be in a STARTED state and the port must be open for this operation + * to succeed. + * + * @param data the byte array to write to the port (must not be null) + * @throws ChannelException if the channel is not ready, port is not open, or write operation fails + * @throws IllegalArgumentException if data parameter is null + */ + @Override + public void write(byte[] data) throws ChannelException + { + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + this.portHandler.writeBytes(data); + } + catch (SerialPortException exception) + { + this.setStatus(ChannelStatus.ERROR); + this.logger.error("Cannot write bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + } + + /** + * Returns a copy of the current channel configuration. + * + *

    This method returns a defensive copy of the channel's configuration to prevent + * external modification of the internal state. The returned configuration contains + * all current settings including port identification, communication parameters, + * and timeout values. + * + * @return a copy of the current channel configuration + * @see ChannelSerialPortConfiguration + */ + @Override + public ChannelSerialPortConfiguration getConfiguration() + { + return (ChannelSerialPortConfiguration) this.configuration.clone(); + } + + + /** + * Sets up the channel with updated configuration. + * + *

    This protected method handles the internal setup process when the channel configuration + * is updated. It saves the current status, stops the channel, updates the real path for + * symbolic links on Linux systems, creates a new port handler if needed, and restores + * the previous state. + * + *

    This method ensures that configuration changes are applied atomically and that the + * channel state is properly maintained throughout the update process. + * + * @throws ChannelException if the setup process fails or configuration is invalid + * @see #updateRealPath() + */ + @Override + protected void setup() throws ChannelException + { + /* 1. Save current state */ + ChannelStatus currentStatus = this.status; + /* 2. Stop serial port */ + this.stop(); + /* 3. update path if symbolic link */ + this.updateRealPath(); + /* 4. Create serial port handler */ + if ((null == this.portHandler) || (false == this.getPortName().equals(this.portHandler.getPortName()))) + { + this.portHandler = new SerialPort(this.getPortName()); + } + /* 5. Restore current state */ + if (ChannelStatus.STARTED == currentStatus) + { + this.start(); + } + } + + + /** + * Handles serial port events from the JSSC library. + * + *

    This method is called by the JSSC library when serial port events occur. Currently, + * it only handles RXCHAR events (data received) by triggering the data reception process. + * Other event types are ignored as they are not relevant for TIC communication. + * + *

    The event handling is designed to be lightweight and delegates the actual data + * processing to the {@link #receiveData()} method. + * + * @param event the serial port event that occurred + * @see SerialPortEvent#RXCHAR + * @see #receiveData() + */ + @Override + public void serialEvent(SerialPortEvent event) + { + switch (event.getEventType()) + { + case SerialPortEvent.RXCHAR: + this.receiveData(); + break; + default: /* No action for other event types */ + } + } + + + /** + * Main processing loop for the serial port channel. + * + *

    This method is called periodically to monitor the port status and handle various + * scenarios including port detection, connection management, and data reception. + * + *

    When the channel is STARTED, it: + *

      + *
    • Checks if the port is still available and handles port not found scenarios
    • + *
    • Detects port name changes (e.g., due to USB device reconnection)
    • + *
    • Receives data if no event listener is configured (polling mode)
    • + *
    + * + *

    When the channel is not STARTED, it attempts to open the port if it becomes + * available, with extended delay on error conditions. + * + *

    The processing delay is dynamically adjusted based on the current state and + * error conditions to optimize performance and responsiveness. + * + * @see #onPortNotFound() + * @see #onPortNameChanged() + * @see #receiveData() + * @see #openPort() + */ + @Override + protected void process() + { + int delay = DEFAULT_DELAY; + + if (this.status == ChannelStatus.STARTED) + { + if (!this.isPortFound()) + { + this.onPortNotFound(); + } + else if (this.hasPortChanged()) + { + this.onPortNameChanged(); + } + else if (!this.hasEventListener()) + { + this.receiveData(); + } + } + else + { + if (this.isPortFound()) + { + this.openPort(); + if (this.status == ChannelStatus.ERROR) + { + delay = DELAY_REOPEN; + } + } + } + this.setPeriod(delay); + } + + + /** + * Performs a blocking read operation for a specific number of bytes. + * + *

    This method reads exactly the specified number of bytes from the serial port. + * The operation will block until the requested number of bytes is available or + * until the operation times out. + * + *

    The channel must be in a STARTED state and the port must be open for this + * operation to succeed. + * + * @param bytesCount the exact number of bytes to read + * @return array containing the read bytes, or null if the read operation fails + * @throws ChannelException if the channel is not ready, port is not open, or read operation fails + * @throws IllegalArgumentException if bytesCount is negative or zero + * @see #read() + * @see #read(int, int) + */ + public byte[] read(int bytesCount) throws ChannelException + { + byte[] buffer = null; + + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + buffer = this.portHandler.readBytes(bytesCount); + } + catch (SerialPortException exception) + { + this.setStatus(ChannelStatus.ERROR); + this.logger.error("Cannot read bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + + return buffer; + } + + /** + * Performs a blocking read operation with a specified timeout. + * + *

    This method reads exactly the specified number of bytes from the serial port + * within the given timeout period. If the timeout expires before the requested + * number of bytes is available, a timeout exception is thrown. + * + *

    The channel must be in a STARTED state and the port must be open for this + * operation to succeed. + * + * @param bytesCount the exact number of bytes to read + * @param timeout the timeout in milliseconds for the read operation + * @return array containing the read bytes, or null if the read operation fails + * @throws ChannelException if the channel is not ready, port is not open, read operation fails, or timeout occurs + * @throws IllegalArgumentException if bytesCount is negative or zero, or timeout is negative + * @see #read() + * @see #read(int) + */ + public byte[] read(int bytesCount, int timeout) throws ChannelException + { + byte[] buffer = null; + + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + buffer = this.portHandler.readBytes(bytesCount, timeout); + } + catch (SerialPortException exception) + { + this.logger.error("Cannot read bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + catch (SerialPortTimeoutException exception) + { + ChannelException.raiseInternalError(exception.getMessage()); + } + + return buffer; + } + + /** + * Returns the number of bytes available for reading from the serial port. + * + *

    This method queries the serial port input buffer to determine how many bytes + * are currently available for reading. This information is useful for determining + * whether data is available before performing a read operation. + * + *

    The channel must be in a STARTED state and the port must be open for this + * operation to succeed. + * + * @return the number of bytes available in the input buffer + * @throws ChannelException if the channel is not ready, port is not open, or the operation fails + * @see #read() + */ + public int available() throws ChannelException + { + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + return this.portHandler.getInputBufferBytesCount(); + } + catch (SerialPortException exception) + { + this.logger.error("Cannot get bytes available", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + + return 0; + } + + /** + * Flushes all input and output buffers of the serial port. + * + *

    This method clears both the input and output buffers of the serial port, + * discarding any pending data. This is useful for ensuring a clean communication + * state, particularly after errors or before starting a new communication session. + * + *

    The flush operation includes clearing receive buffers, transmit buffers, + * and aborting any pending operations. + * + *

    The channel must be in a STARTED state and the port must be open for this + * operation to succeed. + * + * @throws ChannelException if the channel is not ready, port is not open, or flush operation fails + * @see SerialPort#PURGE_RXCLEAR + * @see SerialPort#PURGE_TXCLEAR + */ + public void flush() throws ChannelException + { + if (this.portHandler == null || !this.portHandler.isOpened()) + { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try + { + int mask = SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_RXABORT | SerialPort.PURGE_TXCLEAR | SerialPort.PURGE_TXABORT; + if (!this.portHandler.purgePort(mask)) + { + this.logger.error("Channel " + this.getName() + " failed to purge port " + this.getPortName() + " (" + SystemError.getMessage() + ") "); + } + } + catch (SerialPortException exception) + { + this.logger.error("Cannot purge port", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + } + + /** + * Returns the port identifier from the channel configuration. + * + *

    The port ID is an alternative way to identify serial ports, typically used when + * the system provides unique identifiers for ports. This identifier takes precedence + * over the port name when both are specified. + * + * @return the port identifier, or null if not set in the configuration + * @see ChannelSerialPortConfiguration#getPortId() + * @see #getPortName() + */ + public String getPortId() + { + return this.getChannelConfiguration().getPortId(); + } + + /** + * Returns the port name from the channel configuration. + * + *

    The port name is the system path or name used to identify the serial port. + * This is typically a device path like "/dev/ttyUSB0" on Linux or "COM1" on Windows. + * + * @return the port name, or null if not set in the configuration + * @see ChannelSerialPortConfiguration#getPortName() + * @see #getPortId() + */ + public String getPortName() + { + return this.getChannelConfiguration().getPortName(); + } + + /** + * Returns the name of the currently opened serial port. + * + *

    This method returns the actual port name that is currently opened by the JSSC + * library. This may differ from the configured port name if symbolic links were + * resolved or if the port was dynamically assigned. + * + * @return the name of the opened port, or null if no port is currently open + * @see #getPortName() + * @see #getPortId() + */ + public String getPortNameOpened() + { + return (this.portHandler != null) ? this.portHandler.getPortName() : null; + } + + /** + * Finds the port name associated with the current channel configuration. + * + *

    This method resolves the port name based on the configuration's port ID or + * port name settings. It uses the same resolution logic as the static + * {@link #findPortName(String, String)} method but operates on the current + * configuration. + * + * @return the resolved port name, or null if no matching port is found + * @see #findPortName(String, String) + * @see #getPortId() + * @see #getPortName() + */ + public String findPortName() + { + return findPortName(this.getPortId(), this.getPortName()); + } + + /** + * Returns the current baud rate configured for the serial port. + * + *

    The baud rate determines the speed of serial communication in bits per second. + * + * @return the configured baud rate value + * @see ChannelSerialPortConfiguration#getBaudrate() + */ + public int getBaudrate() + { + return this.getChannelConfiguration().getBaudrate().intValue(); + } + + /** + * Returns the number of data bits configured for serial communication. + * + *

    Data bits define the number of bits used to represent each character transmitted. + * Standard values are 7 (for ASCII) or 8 (for binary data). TIC protocol typically + * uses 7 data bits. + * + * @return the number of data bits (5, 6, 7, or 8) + * @see ChannelSerialPortConfiguration#getDataBits() + */ + public int getDataBits() + { + return this.getChannelConfiguration().getDataBits().intValue(); + } + + /** + * Returns the number of stop bits configured for serial communication. + * + *

    Stop bits mark the end of each data frame. Standard values are 1.0, 1.5, or 2.0. + * Most serial communications use 1.0 stop bits. + * + * @return the number of stop bits (1.0, 1.5, or 2.0) + * @see ChannelSerialPortConfiguration#getStopBits() + */ + public float getStopBits() + { + return this.getChannelConfiguration().getStopBits().floatValue(); + } + + /** + * Returns the parity setting configured for serial communication. + * + *

    Parity is used for error detection by adding an extra bit to each data frame. + * TIC protocol typically uses EVEN parity for reliable data transmission. + * + * @return the parity setting from the configuration + * @see Parity + * @see ChannelSerialPortConfiguration#getParity() + */ + public Parity getParity() + { + return this.getChannelConfiguration().getParity(); + } + + /** + * Returns the timeout value for synchronous read operations. + * + *

    This timeout determines how long a synchronous read operation will wait for + * data before timing out. The value is specified in milliseconds. + * + * @return the timeout value in milliseconds for synchronous read operations + * @see ChannelSerialPortConfiguration#getSyncReadTimeout() + * @see #read(int, int) + */ + public int getSyncReadTimeout() + { + return this.getChannelConfiguration().getSyncReadTimeout().intValue(); + } + + + /** + * Determines if the channel has an event listener configured. + * + *

    This method indicates whether the channel is configured to use event-driven + * data reception (via SerialPortEventListener) or polling-based reception. + * + *

    The base implementation returns false, indicating polling mode. Subclasses + * can override this method to enable event-driven mode. + * + * @return true if event listener is configured, false for polling mode + * @see #addEventListener() + * @see #removeEventListener() + */ + protected boolean hasEventListener() + { + return false; + } + + /** + * Adds a serial port event listener for asynchronous data reception. + * + *

    This method configures the serial port to generate events when data is received, + * control signals change, or other port events occur. It sets up the event mask to + * listen for RXCHAR (data received), CTS (clear to send), and DSR (data set ready) + * events. + * + *

    The channel instance is registered as the event listener, so {@link #serialEvent(SerialPortEvent)} + * will be called when events occur. + * + * @return true if the event listener was successfully added, false otherwise + * @see #removeEventListener() + * @see #serialEvent(SerialPortEvent) + * @see SerialPort#MASK_RXCHAR + */ + protected boolean addEventListener() + { + if (this.portHandler == null) + { + return false; + } + int mask = SerialPort.MASK_RXCHAR + SerialPort.MASK_CTS + SerialPort.MASK_DSR; + try + { + if (!this.portHandler.setEventsMask(mask)) + { + this.logger.error("Channel " + this.getName() + " failed to set event mask on port " + this.getPortName() + " (" + SystemError.getMessage() + ") "); + return false; + } + } + catch (SerialPortException exception) + { + this.logger.error("Channel " + this.getName() + " failed to set event mask on port " + this.getPortName() + " : " + exception.getMessage()); + return false; + } + try + { + this.portHandler.addEventListener(this); + } + catch (SerialPortException e) + { + this.logger.error("Channel " + this.getName() + " failed to add listener :" + e.getMessage(), e); + return false; + } + + return true; + } + + /** + * Removes the serial port event listener. + * + *

    This method unregisters the channel instance as an event listener from the + * serial port. After calling this method, the channel will no longer receive + * asynchronous events and will operate in polling mode. + * + *

    This method is typically called when stopping the channel or switching to + * polling-based data reception. + * + * @return true if the event listener was successfully removed, false otherwise + * @see #addEventListener() + * @see #hasEventListener() + */ + protected boolean removeEventListener() + { + if (this.portHandler == null) + { + return false; + } + try + { + if (!this.portHandler.removeEventListener()) + { + this.logger.error("Channel " + this.getName() + " failed to remove listener"); + return false; + } + } + catch (SerialPortException e) + { + this.logger.error("Channel " + this.getName() + " failed to remove listener :" + e.getMessage(), e); + return false; + } + + return true; + } + + /** + * Receives and processes data from the serial port. + * + *

    This method checks for available data in the serial port input buffer and + * reads it if present. The received data is then passed to registered listeners + * via the notification system. + * + *

    If no data is available, the method returns immediately without error. + * If a read error occurs, it notifies listeners with an appropriate error code. + * + *

    This method is called both from the periodic processing loop (polling mode) + * and from the serial event handler (event-driven mode). + * + * @see #read() + * @see #available() + * @see #notifyOnDataRead(byte[]) + * @see ChannelSerialPortErrorCode#READ_FAILED + */ + protected void receiveData() + { + try + { + if (this.available() > 0) + { + byte[] buffer = this.read(); + this.notifyOnDataRead(buffer); + } + } + catch (ChannelException exception) + { + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), exception.getMessage()); + } + } + + + /** + * Checks if the port name has changed since the last check. + * + *

    This method compares the currently detected port name with the previously + * detected port name to determine if the port has changed. This typically occurs + * when a USB device is unplugged and plugged back in, causing the system to assign + * a new device path. + * + *

    The comparison is case-insensitive to handle variations in how the system + * reports port names. + * + * @return true if the port name has changed, false otherwise + * @see #onPortNameChanged() + */ + private boolean hasPortChanged() + { + boolean portChanged = false; + + this.currentPortName = this.findPortName(); + + if (this.currentPortName != null && !this.currentPortName.equalsIgnoreCase(this.previousPortName)) + { + portChanged = true; + } + + return portChanged; + } + + /** + * Handles the event when the port name has changed. + * + *

    This method is called when a port name change is detected. It logs the change, + * sets the channel status to ERROR, forcibly closes the current port connection, + * notifies listeners of the error, and updates the previous port name for future + * comparisons. + * + *

    Port name changes typically occur when USB devices are reconnected and the + * system assigns a new device path (e.g., /dev/ttyUSB0 → /dev/ttyUSB1). + * + * @see #hasPortChanged() + * @see ChannelSerialPortErrorCode#PORT_NAME_HAS_CHANGED + */ + private void onPortNameChanged() + { + String errorMessage = "Port name has changed ( " + this.previousPortName + " --> " + this.currentPortName + " )"; + this.logger.error(errorMessage); + this.setStatus(ChannelStatus.ERROR); + this.closePortForced(); + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_NAME_HAS_CHANGED.getCode(ERROR_CODE_OFFSET), errorMessage); + this.previousPortName = this.currentPortName; + } + + /** + * Updates the port name to its real path by resolving symbolic links. + * + *

    This method is only executed on Linux systems to resolve symbolic links to their + * actual device paths. This is important for USB devices that may be represented + * by symbolic links in /dev/disk/by-id/ or similar locations. + * + *

    The method uses the system 'realpath' command to resolve the symbolic link + * and updates the channel configuration with the resolved path. + * + * @throws ChannelException if the realpath resolution fails or configuration update fails + * @see SystemUtils#IS_OS_LINUX + */ + private void updateRealPath() throws ChannelException + { + if (SystemUtils.IS_OS_LINUX) + { + String realPortName; + try + { + Process p = Runtime.getRuntime().exec("realpath " + this.getPortName()); + BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream())); + realPortName = stdInput.readLine(); + this.getChannelConfiguration().setPortName(realPortName); + } + catch (IOException | DataDictionaryException e) + { + throw new ChannelException(ChannelException.ERRCODE_INVALID_CONFIGURATION, "Cannot resolve realpath (" + e.getMessage() + ")"); + } + } + } + + /** + * Returns the channel configuration cast to the specific serial port configuration type. + * + *

    This helper method provides type-safe access to the serial port specific + * configuration without requiring explicit casting throughout the code. + * + * @return the channel configuration as a ChannelSerialPortConfiguration instance + */ + private ChannelSerialPortConfiguration getChannelConfiguration() + { + return (ChannelSerialPortConfiguration) this.configuration; + } + + /** + * Opens and configures the serial port connection. + * + *

    This method performs the complete process of opening a serial port connection: + *

      + *
    1. Checks if the port is already open to avoid duplicate operations
    2. + *
    3. Creates a new SerialPort handler if needed
    4. + *
    5. Opens the port using the resolved port name
    6. + *
    7. Configures the port with the specified communication parameters
    8. + *
    9. Adds event listener if configured for event-driven mode
    10. + *
    11. Sets the channel status to STARTED
    12. + *
    + * + *

    If any step fails, the method logs the error, handles specific error conditions + * (like PORT_BUSY), and ensures the channel enters an appropriate error state. + * + * @see #closePort() + * @see #findPortName() + * @see #hasEventListener() + * @see #addEventListener() + * @see ChannelSerialPortErrorCode#PORT_BUSY + */ + protected void openPort() + { + /* 1. Check if serial port is already opened */ + if (this.portHandler != null && this.portHandler.isOpened()) + { + return; + } + /* 2. Open serial port */ + String portNameFound = this.findPortName(); + try + { + this.logger.info("Channel " + this.getName() + " opening port " + portNameFound); + if (this.portHandler == null) + { + this.portHandler = new SerialPort(portNameFound); + } + if (!this.portHandler.openPort()) + { + this.logger.error("Channel " + this.getName() + " failed to open port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); + return; + } + this.logger.info("Channel " + this.getName() + " configuring port " + portNameFound); + if (!this.portHandler.setParams(this.getBaudrate(), this.getDataBits(), stopBitsFromConfiguration(this.getStopBits()), parityFromConfiguration(this.getParity()))) + { + this.logger.error("Channel " + this.getName() + " failed to configure port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); + return; + } + } + catch (SerialPortException exception) + { + String errorMessage = "Channel " + this.getName() + " failed to open port " + this.getPortNameOpened() + " : " + exception.getMessage(); + this.logger.error(errorMessage); + if (exception.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) + { + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_BUSY.getCode(ERROR_CODE_OFFSET), errorMessage); + } + this.closePortForced(); + return; + } + /* 3. Add serial event listener */ + if (this.hasEventListener()) + { + if (!this.addEventListener()) + { + return; + } + } + this.setStatus(ChannelStatus.STARTED); + } + + /** + * Closes the serial port connection gracefully. + * + *

    This method performs the complete process of closing a serial port connection: + *

      + *
    1. Checks if the port is already closed to avoid duplicate operations
    2. + *
    3. Removes event listener if one was configured
    4. + *
    5. Closes the port and releases system resources
    6. + *
    7. Sets the channel status to STOPPED
    8. + *
    + * + *

    If any step fails, the method logs the error but continues with the remaining + * cleanup steps to ensure the channel is in a consistent state. + * + * @see #openPort() + * @see #closePortForced() + * @see #hasEventListener() + * @see #removeEventListener() + */ + protected void closePort() + { + /* 1. Check if serial port is already closed */ + if (this.portHandler == null || !this.portHandler.isOpened()) + { + return; + } + /* 2. Remove serial event listener */ + if (this.hasEventListener()) + { + this.removeEventListener(); + } + /* 3. Close serial port */ + try + { + this.logger.info("Channel " + this.getName() + " closing port " + this.getPortNameOpened()); + if (!this.portHandler.closePort()) + { + this.logger.error("Channel " + this.getName() + " failed to close port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); + return; + } + } + catch (SerialPortException exception) + { + this.logger.error("Channel " + this.getName() + " failed to close port " + this.getPortNameOpened() + " : " + exception.getMessage()); + return; + } + this.setStatus(ChannelStatus.STOPPED); + } + + /** + * Forcefully closes the serial port and resets the port handler. + * + *

    This method performs an emergency closure of the serial port connection without + * the normal cleanup procedures. It attempts to close the port and then creates a + * new SerialPort handler instance for future use. + * + *

    This method is typically used in error conditions where the normal close + * procedure may fail or when the port needs to be forcibly reset. + * + *

    Unlike {@link #closePort()}, this method does not remove event listeners or + * update the channel status, making it suitable for error recovery scenarios. + * + * @see #closePort() + * @see #findPortName() + */ + protected void closePortForced() + { + try + { + this.portHandler.closePort(); + } + catch (SerialPortException exception) + { + this.logger.error("Channel " + this.getName() + " fail on close : close " + this.getPortNameOpened() + " failed due to " + exception.getMessage()); + } + this.portHandler = new SerialPort(this.findPortName()); + } + + /** + * Checks if the configured port is currently available on the system. + * + *

    This method uses the current channel configuration to determine if the + * specified port is available. It delegates to the static {@link #isPortFound(String, String)} + * method with the current port ID and port name. + * + * @return true if the configured port is found and available, false otherwise + * @see #isPortFound(String, String) + * @see #getPortId() + * @see #getPortName() + */ + protected boolean isPortFound() + { + return isPortFound(this.getPortId(), this.getPortName()); + } + + /** + * Checks if the currently opened port matches the expected port. + * + *

    This method compares the name of the currently opened port with the port name + * that should be opened according to the current configuration. This is useful + * for detecting situations where the port has changed or become unavailable. + * + * @return true if the opened port matches the expected port, false otherwise + * @see #getPortNameOpened() + * @see #findPortName() + */ + protected boolean isPortOpenedFound() + { + String portNameOpened = this.getPortNameOpened(); + if (portNameOpened == null) + { + return false; + } + String portNameFound = this.findPortName(); + + return portNameOpened.equals(portNameFound); + } + + /** + * Handles the event when the configured port is not found. + * + *

    This method is called when the system cannot locate the configured serial port. + * It logs the error, sets the channel status to ERROR, forcibly closes any existing + * connection, and notifies listeners of the error condition. + * + *

    This typically occurs when a USB device is unplugged or the device driver + * is not properly installed. + * + * @see #isPortFound() + * @see ChannelSerialPortErrorCode#PORT_NOT_FOUND + */ + protected void onPortNotFound() + { + String errorMessage = "Channel " + this.getName() + " : serial port " + this.findPortName() + " not found"; + this.logger.error(errorMessage); + this.setStatus(ChannelStatus.ERROR); + this.closePortForced(); + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); + } + + /** + * Handles the event when the currently opened port is no longer found. + * + *

    This method is called when the system can no longer locate the port that was + * previously opened by the channel. This indicates that the device has been + * physically disconnected or the system has lost access to it. + * + *

    It logs the error, sets the channel status to ERROR, forcibly closes the + * connection, and notifies listeners of the error condition. + * + * @see #isPortOpenedFound() + * @see ChannelSerialPortErrorCode#PORT_OPENED_NOT_FOUND + */ + protected void onPortOpenedNotFound() + { + String errorMessage = "Channel " + this.getName() + " : serial port " + this.getPortNameOpened() + " opened not found"; + this.logger.error(errorMessage); + this.setStatus(ChannelStatus.ERROR); + this.closePortForced(); + this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_OPENED_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java index 29647b4..2a53bdb 100644 --- a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java +++ b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java @@ -1,61 +1,99 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -/** - * Channel serial port error code - */ -public enum ChannelSerialPortErrorCode -{ - /** Port not found */ - PORT_NOT_FOUND(1), - - /** Port opened not found */ - PORT_OPENED_NOT_FOUND(2), - - /** Port name has changed */ - PORT_NAME_HAS_CHANGED(3), - - /** Port busy */ - PORT_BUSY(4), - - /** Read operation failed */ - READ_FAILED(5), - - /** Read operation timed out */ - READ_TIMEOUT(6); - - private int code; - - private ChannelSerialPortErrorCode(int code) - { - this.code = code; - } - - /** - * Get error code without offset - * - * @return error code - */ - public int getCode() - { - return this.code; - } - - /** - * Get error code with offset - * - * @param offset - * - * @return error code - */ - public int getCode(int offset) - { - return offset + this.code; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io.channels.serialport; + +/** + * Enumeration of error codes for serial port channel operations. + * + *

    This enum defines specific error codes that can occur during serial port communication + * operations. Each error code is associated with a unique numeric identifier that can be used + * for error reporting, logging, and debugging purposes. + * + *

    The error codes are designed to be used with an offset mechanism, allowing different + * components or modules to have their own error code ranges while maintaining uniqueness + * across the entire system. + * + * @author Enedis Smarties team + */ +public enum ChannelSerialPortErrorCode +{ + /** + * Error code indicating that the specified serial port was not found on the system. + * This typically occurs when the port identifier or name does not correspond to any + * available serial port. + */ + PORT_NOT_FOUND(1), + + /** + * Error code indicating that a previously opened serial port is no longer available. + * This can happen when the port is disconnected or becomes unavailable after being opened. + */ + PORT_OPENED_NOT_FOUND(2), + + /** + * Error code indicating that the serial port name has changed during operation. + * This typically occurs when the port is reconnected and gets assigned a different name. + */ + PORT_NAME_HAS_CHANGED(3), + + /** + * Error code indicating that the serial port is currently busy and cannot be accessed. + * This typically occurs when another application or process is already using the port. + */ + PORT_BUSY(4), + + /** + * Error code indicating that a read operation on the serial port has failed. + * This can be due to various reasons such as hardware issues, communication errors, + * or port configuration problems. + */ + READ_FAILED(5), + + /** + * Error code indicating that a read operation on the serial port has timed out. + * This occurs when no data is received within the specified timeout period. + */ + READ_TIMEOUT(6); + + private int code; + + /** + * Constructs a new error code with the specified numeric identifier. + * + * @param code the numeric identifier for this error code + */ + private ChannelSerialPortErrorCode(int code) + { + this.code = code; + } + + /** + * Returns the base error code without any offset applied. + * + * @return the base error code value + */ + public int getCode() + { + return this.code; + } + + /** + * Returns the error code with the specified offset added. + * + *

    This method allows different components or modules to have their own error code + * ranges by applying an offset to the base error code. This helps maintain uniqueness + * across the entire system while allowing for organized error code management. + * + * @param offset the offset to add to the base error code + * @return the error code with the specified offset applied + */ + public int getCode(int offset) + { + return offset + this.code; + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java index 726f099..4dda3b3 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java @@ -1,172 +1,162 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import java.util.List; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; -import enedis.lab.types.BytesArray; - -/** - * Codec TIC Frame Historic - * - */ -public class CodecTICFrameHistoric implements Codec -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Separator */ - public static final byte SEPARATOR = 0x20; // SP - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Codec - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICFrameHistoric decode(byte[] bytesBuffer) throws CodecException - { - String errorMessage = ""; - BytesArray rawDataSet = null; - TICFrameHistoric ticFrame = null; - CodecTICFrameHistoricDataSet codecTICFrameHistoricDataSet = new CodecTICFrameHistoricDataSet(); - - BytesArray bytesArray = new BytesArray(bytesBuffer); - - if ((true == bytesArray.startsWith(TICFrame.BEGINNING_PATTERN)) && (true == bytesArray.endsWith(TICFrame.END_PATTERN)) && (bytesArray.contains(TICFrame.EOT) == false)) - { - bytesArray.remove(0); - bytesArray.remove(bytesArray.size() - 1); - - ticFrame = new TICFrameHistoric(); - - List datasetList = bytesArray.slice(TICFrameDataSet.BEGINNING_PATTERN, TICFrameDataSet.END_PATTERN, BytesArray.CONTIGUOUS); - - if (datasetList.isEmpty() == false) - { - for (int i = 0; i < datasetList.size(); i++) - { - TICFrameHistoricDataSet dataSet = null; - rawDataSet = datasetList.get(i); - byte[] rawDataSetByte = rawDataSet.getBytes(); - - try - { - dataSet = codecTICFrameHistoricDataSet.decode(rawDataSetByte); - ticFrame.addDataSet(dataSet); - } - catch (CodecException exception) - { - errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; - } - } - } - } - - if (errorMessage.isEmpty()) - { - return ticFrame; - } - else - { - throw new CodecException(errorMessage, ticFrame); - } - - } - - @Override - public byte[] encode(TICFrameHistoric ticFrameHistoric) throws CodecException - { - String errorMessage = ""; - CodecTICFrameHistoricDataSet codec = new CodecTICFrameHistoricDataSet(); - BytesArray dataSet = new BytesArray(); - - List ticFrameHistoricList = ticFrameHistoric.getDataSetList(); - - if (ticFrameHistoric != null && !ticFrameHistoricList.isEmpty()) - { - List groups = ticFrameHistoric.getDataSetList(); - dataSet.add(TICFrame.BEGINNING_PATTERN); - for (int i = 0; i < groups.size(); i++) - { - try - { - byte[] buff = codec.encode((TICFrameHistoricDataSet) groups.get(i)); - dataSet.addAll(buff); - } - catch (CodecException e) - { - errorMessage += e.getMessage() + " : " + new String(ticFrameHistoric.getBytes()) + "\n"; - } - } - dataSet.add(TICFrame.END_PATTERN); - } - - else - { - return null; - } - - if (errorMessage.isEmpty()) - { - return dataSet.getBytes(); - } - else - { - throw new CodecException(errorMessage, dataSet.getBytes()); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.codec; + +import java.util.List; + +import enedis.lab.codec.Codec; +import enedis.lab.codec.CodecException; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; +import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; +import enedis.lab.types.BytesArray; + +/** + * Codec for TIC Historic frames. + * + * This codec handles the encoding and decoding of TIC Historic frames, which are used + * in the French electricity meter communication protocol. Historic frames contain + * historical consumption data and are part of the TIC protocol specification. + * + * The codec processes byte arrays containing TIC Historic frame data and converts them + * to TICFrameHistoric objects, and vice versa. It validates frame structure, handles + * data set parsing, and ensures proper checksum validation. + * + * @author Enedis Smarties team + */ +public class CodecTICFrameHistoric implements Codec +{ + + /** + * Separator character used in TIC Historic frames. + * This is the space character (0x20) used to separate different parts + * of the frame structure in the TIC protocol. + */ + public static final byte SEPARATOR = 0x20; // SP + + + /** + * Decodes a byte array containing TIC Historic frame data into a TICFrameHistoric object. + * + * This method validates the frame structure by checking for proper beginning and end patterns, + * extracts individual data sets from the frame, and decodes each data set using the + * CodecTICFrameHistoricDataSet codec. If any data set fails to decode, the error is + * collected and thrown as a CodecException. + * + * @param bytesBuffer the byte array containing the TIC Historic frame data + * @return a TICFrameHistoric object containing the decoded frame data + * @throws CodecException if the frame structure is invalid or if any data set fails to decode + */ + @Override + public TICFrameHistoric decode(byte[] bytesBuffer) throws CodecException + { + String errorMessage = ""; + BytesArray rawDataSet = null; + TICFrameHistoric ticFrame = null; + CodecTICFrameHistoricDataSet codecTICFrameHistoricDataSet = new CodecTICFrameHistoricDataSet(); + + BytesArray bytesArray = new BytesArray(bytesBuffer); + + if ((true == bytesArray.startsWith(TICFrame.BEGINNING_PATTERN)) && (true == bytesArray.endsWith(TICFrame.END_PATTERN)) && (bytesArray.contains(TICFrame.EOT) == false)) + { + bytesArray.remove(0); + bytesArray.remove(bytesArray.size() - 1); + + ticFrame = new TICFrameHistoric(); + + List datasetList = bytesArray.slice(TICFrameDataSet.BEGINNING_PATTERN, TICFrameDataSet.END_PATTERN, BytesArray.CONTIGUOUS); + + if (datasetList.isEmpty() == false) + { + for (int i = 0; i < datasetList.size(); i++) + { + TICFrameHistoricDataSet dataSet = null; + rawDataSet = datasetList.get(i); + byte[] rawDataSetByte = rawDataSet.getBytes(); + + try + { + dataSet = codecTICFrameHistoricDataSet.decode(rawDataSetByte); + ticFrame.addDataSet(dataSet); + } + catch (CodecException exception) + { + errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; + } + } + } + } + + if (errorMessage.isEmpty()) + { + return ticFrame; + } + else + { + throw new CodecException(errorMessage, ticFrame); + } + + } + + /** + * Encodes a TICFrameHistoric object into a byte array representing a TIC Historic frame. + * + * This method takes a TICFrameHistoric object and converts it into the byte representation + * of a TIC Historic frame. It processes each data set in the frame, encodes them using the + * CodecTICFrameHistoricDataSet codec, and wraps the result with proper frame delimiters. + * + * @param ticFrameHistoric the TICFrameHistoric object to encode + * @return a byte array containing the encoded TIC Historic frame + * @throws CodecException if the frame is null, empty, or if any data set fails to encode + */ + @Override + public byte[] encode(TICFrameHistoric ticFrameHistoric) throws CodecException + { + String errorMessage = ""; + CodecTICFrameHistoricDataSet codec = new CodecTICFrameHistoricDataSet(); + BytesArray dataSet = new BytesArray(); + + List ticFrameHistoricList = ticFrameHistoric.getDataSetList(); + + if (ticFrameHistoric != null && !ticFrameHistoricList.isEmpty()) + { + List groups = ticFrameHistoric.getDataSetList(); + dataSet.add(TICFrame.BEGINNING_PATTERN); + for (int i = 0; i < groups.size(); i++) + { + try + { + byte[] buff = codec.encode((TICFrameHistoricDataSet) groups.get(i)); + dataSet.addAll(buff); + } + catch (CodecException e) + { + errorMessage += e.getMessage() + " : " + new String(ticFrameHistoric.getBytes()) + "\n"; + } + } + dataSet.add(TICFrame.END_PATTERN); + } + + else + { + return null; + } + + if (errorMessage.isEmpty()) + { + return dataSet.getBytes(); + } + else + { + throw new CodecException(errorMessage, dataSet.getBytes()); + } + } + + } \ No newline at end of file From 550e222d8ddffbd1307f42596afa8dfd7d89c713 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Fri, 3 Oct 2025 16:56:47 +0200 Subject: [PATCH 23/59] chore: format with spotless --- pom.xml | 68 +- .../enedis/lab/io/tic/TICPortFinderBase.java | 1 - .../lab/io/tic/TICPortPlugNotifier.java | 1 - .../enedis/lab/io/usb/USBPortFinderBase.java | 1 - .../tic/channels/ChannelTICSerialPort.java | 501 ++-- .../ChannelTICSerialPortConfiguration.java | 453 ++-- .../serialport/ChannelSerialPort.java | 2384 ++++++++--------- .../ChannelSerialPortErrorCode.java | 145 +- .../tic/codec/CodecTICFrameHistoric.java | 262 +- .../codec/CodecTICFrameHistoricDataSet.java | 245 +- .../tic/codec/CodecTICFrameStandard.java | 150 +- .../codec/CodecTICFrameStandardDataSet.java | 246 +- .../lab/protocol/tic/codec/TICCodec.java | 402 ++- .../tic/datastreams/TICInputStream.java | 293 +- .../datastreams/TICStreamConfiguration.java | 270 +- .../lab/protocol/tic/frame/TICError.java | 145 +- .../lab/protocol/tic/frame/TICFrame.java | 844 +++--- .../protocol/tic/frame/TICFrameDataSet.java | 460 ++-- .../tic/frame/historic/TICFrameHistoric.java | 122 +- .../historic/TICFrameHistoricDataSet.java | 219 +- .../tic/frame/standard/TICException.java | 110 +- .../tic/frame/standard/TICFrameStandard.java | 317 +-- .../standard/TICFrameStandardDataSet.java | 516 ++-- 23 files changed, 3797 insertions(+), 4358 deletions(-) diff --git a/pom.xml b/pom.xml index dacfd47..bd8b907 100644 --- a/pom.xml +++ b/pom.xml @@ -6,26 +6,25 @@ SPDX-License-Identifier: Apache-2.0 --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-v4_0_0.xsd"> 4.0.0 - + fr.enedis TIC2WebSocket 1.0.0 jar ${project.artifactId} - Tele Information Client (TIC) WebSocket Interface + Tele Information Client (TIC) WebSocket Interface Enedis http://www.enedis.fr/ - + Apache-2.0 @@ -35,7 +34,7 @@ SPDX-License-Identifier: Apache-2.0 - + Jehan BOUSCH @@ -61,25 +60,25 @@ SPDX-License-Identifier: Apache-2.0 - - + + - - + + - - + + - - + + - - + + - - + + - + UTF-8 UTF-8 @@ -90,10 +89,10 @@ SPDX-License-Identifier: Apache-2.0 ${project.name} ${project.version} ${project.description} - ./images/team + ./images/team - + junit @@ -143,7 +142,7 @@ SPDX-License-Identifier: Apache-2.0 - + @@ -318,19 +317,14 @@ SPDX-License-Identifier: Apache-2.0 2.43.0 - - src/main/java/enedis/lab/io/*.java - src/main/java/enedis/lab/io/*/*.java - src/main/java/enedis/lab/io/channels/serialport/*.java - - + - + @@ -341,13 +335,13 @@ SPDX-License-Identifier: Apache-2.0 - - + + - - + + - - + + - + \ No newline at end of file diff --git a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java b/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java index a861ab3..eb40b6b 100644 --- a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java +++ b/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java @@ -119,5 +119,4 @@ public TICPortDescriptor findNative(String portName) { return ticPortDescriptor; } - } diff --git a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java b/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java index 04f8f3f..1d229f9 100644 --- a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java +++ b/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java @@ -64,5 +64,4 @@ public TICPortPlugNotifier() { public TICPortPlugNotifier(long period, TICPortFinder finder) { super(period, finder); } - } diff --git a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java b/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java index e4fe0a9..b534761 100644 --- a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java +++ b/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java @@ -123,5 +123,4 @@ public DataList findAll() { LibUsb.exit(context); return usbPortList; } - } diff --git a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java index da15adb..336f5da 100644 --- a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java +++ b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java @@ -7,8 +7,6 @@ package enedis.lab.protocol.tic.channels; -import java.util.Arrays; - import enedis.lab.io.channels.ChannelConfiguration; import enedis.lab.io.channels.ChannelException; import enedis.lab.io.channels.serialport.ChannelSerialPort; @@ -17,283 +15,240 @@ import enedis.lab.types.BytesArray; import enedis.lab.types.DataDictionaryException; import enedis.lab.util.time.Time; +import java.util.Arrays; /** * A specialized serial port channel implementation for TIC (Teleinformation Client) communication. - * This channel extends the base serial port functionality to handle TIC protocol-specific - * frame detection, mode auto-detection, and TIC data stream processing. - * - *

    The channel supports both HISTORIC and STANDARD TIC modes, with automatic mode - * detection capabilities. It handles TIC frame parsing with proper start/end pattern - * recognition and provides timeout handling for reliable data reception. - * + * This channel extends the base serial port functionality to handle TIC protocol-specific frame + * detection, mode auto-detection, and TIC data stream processing. + * + *

    The channel supports both HISTORIC and STANDARD TIC modes, with automatic mode detection + * capabilities. It handles TIC frame parsing with proper start/end pattern recognition and provides + * timeout handling for reliable data reception. + * *

    Key features: + * *

      - *
    • Automatic TIC mode detection (HISTORIC vs STANDARD)
    • - *
    • TIC frame parsing with start/end pattern recognition
    • - *
    • Timeout handling for reliable data reception
    • - *
    • Port configuration and reconnection management
    • + *
    • Automatic TIC mode detection (HISTORIC vs STANDARD) + *
    • TIC frame parsing with start/end pattern recognition + *
    • Timeout handling for reliable data reception + *
    • Port configuration and reconnection management *
    - * + * * @author Enedis Smarties team */ -public class ChannelTICSerialPort extends ChannelSerialPort -{ - /** TIC frame start pattern (STX character) */ - private static final byte START_PATTERN = (byte) 0x02; - /** TIC frame end pattern (ETX character) */ - private static final byte END_PATTERN = (byte) 0x03; - /** Polling period in milliseconds for data reception */ - private static final int RECEIVE_DATA_POLLING_PERIOD = 100; - /** Historic mode buffer start pattern */ - private static final byte[] HISTORIC_BUFFER_START = { 2, 10, 65, 68, 67, 79 }; - /** Standard mode buffer start pattern */ - private static final byte[] STANDARD_BUFFER_START = { 2, 10, 65, 68, 83, 67 }; - - /** Current TIC mode detected during operation */ - private TICMode currentMode; - - /** - * Creates a new TIC serial port channel instance with the specified configuration. - * - * @param configuration the TIC serial port configuration containing port settings - * and TIC mode parameters - * @throws ChannelException if the configuration is invalid or channel creation fails - */ - public ChannelTICSerialPort(ChannelTICSerialPortConfiguration configuration) throws ChannelException - { - super(configuration); - this.currentMode = null; - } - - - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException - { - if (configuration == null) - { - ChannelException.raiseInvalidConfiguration("null"); - } - if (!(configuration instanceof ChannelTICSerialPortConfiguration)) - { - ChannelException.raiseInvalidConfigurationType(configuration, ChannelTICSerialPortConfiguration.class.getSimpleName()); - } - super.setup(configuration); - } - - @Override - public ChannelTICSerialPortConfiguration getConfiguration() - { - return (ChannelTICSerialPortConfiguration) this.configuration; - } - - /** - * Gets the current TIC mode being used by the channel. - * Returns the detected mode if available, otherwise returns the selected mode from configuration. - * - * @return the current TIC mode (HISTORIC, STANDARD, or AUTO) - */ - public TICMode getMode() - { - return (this.getCurrentMode() != null) ? this.getCurrentMode() : this.getSelectedMode(); - } - - /** - * Gets the TIC mode selected in the channel configuration. - * - * @return the selected TIC mode from configuration - */ - public TICMode getSelectedMode() - { - return this.getConfiguration().getTicMode(); - } - - /** - * Gets the currently detected TIC mode during operation. - * This may differ from the selected mode if auto-detection is enabled. - * - * @return the currently detected TIC mode, or null if not yet detected - */ - public TICMode getCurrentMode() - { - return this.currentMode; - } - - @Override - public byte[] read() throws ChannelException - { - long beginTime = System.currentTimeMillis(); - long elapsedTime = 0; - long timeout = this.getSyncReadTimeout(); - BytesArray buffer = new BytesArray(); - byte[] ticFrame = null; - int startOfFrame = -1, endOfFrame = -1; - - while (elapsedTime < timeout || timeout == 0) - { - if (this.available() > 0) - { - buffer.addAll(super.read(1)); - if (startOfFrame == -1) - { - startOfFrame = buffer.indexOf(START_PATTERN); - if (startOfFrame != -1) - { - endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); - if (endOfFrame != -1) - { - ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); - break; - } - } - } - else - { - endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); - if (endOfFrame != -1) - { - ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); - break; - } - } - } - else - { - Time.sleep(RECEIVE_DATA_POLLING_PERIOD); - elapsedTime = (System.currentTimeMillis() - beginTime); - } - } - - return ticFrame; - } - - - @Override - protected void receiveData() - { - if (this.currentMode == null) - { - if (this.autoDetectMode() == null) - { - this.onReadTimeout(); - return; - } - } - try - { - byte[] ticFrame = this.read(); - - if (ticFrame == null) - { - this.onReadTimeout(); - } - else - { - this.notifyOnDataRead(ticFrame); - } - } - catch (ChannelException e) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), "TIC read failed: " + e.getErrorInfo()); - } - } - - - private void onReadTimeout() - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_TIMEOUT.getCode(ERROR_CODE_OFFSET), "TIC read timeout"); - this.configurePort(); - this.currentMode = null; - } - - private void configurePort() - { - this.closePort(); - this.openPort(); - try - { - this.flush(); - } - catch (ChannelException e) - { - } - } - - private void setSelectedMode(TICMode ticMode) - { - try - { - this.getConfiguration().setTicMode(ticMode); - } - catch (DataDictionaryException e) - { - this.logger.error("Cannot set TIC mode " + ticMode, e); - } - } - - private boolean checkAndUpdateMode(TICMode ticMode) - { - boolean result = false; - this.setSelectedMode(ticMode); - this.configurePort(); - try - { - byte[] ticFrame = this.read(); - if (ticFrame != null && ticFrame.length > HISTORIC_BUFFER_START.length) - { - byte[] ticFrameStart = new byte[HISTORIC_BUFFER_START.length]; - System.arraycopy(ticFrame, 0, ticFrameStart, 0, ticFrameStart.length); - if (ticMode == TICMode.HISTORIC) - { - if (Arrays.equals(ticFrameStart, HISTORIC_BUFFER_START)) - { - result = true; - } - } - else if (ticMode == TICMode.STANDARD) - { - if (Arrays.equals(ticFrameStart, STANDARD_BUFFER_START)) - { - result = true; - } - } - } - } - catch (ChannelException e) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), "TIC read failed: " + e.getErrorInfo()); - } - if (result) - { - this.currentMode = ticMode; - } - this.setSelectedMode(TICMode.AUTO); - - return result; - } - - private TICMode autoDetectMode() - { - if (this.getSelectedMode() != TICMode.AUTO) - { - this.currentMode = this.getSelectedMode(); - return this.getSelectedMode(); - } - this.logger.debug("Auto detecting TIC Mode"); - if (!this.checkAndUpdateMode(TICMode.HISTORIC)) - { - if (!this.checkAndUpdateMode(TICMode.STANDARD)) - { - this.logger.debug("TIC Mode not detected"); - - return null; - } - this.logger.debug("TIC Mode STANDARD detected"); - - return TICMode.STANDARD; - } - this.logger.debug("TIC Mode HISTORIC detected"); - - return TICMode.HISTORIC; - } +public class ChannelTICSerialPort extends ChannelSerialPort { + /** TIC frame start pattern (STX character) */ + private static final byte START_PATTERN = (byte) 0x02; + + /** TIC frame end pattern (ETX character) */ + private static final byte END_PATTERN = (byte) 0x03; + + /** Polling period in milliseconds for data reception */ + private static final int RECEIVE_DATA_POLLING_PERIOD = 100; + + /** Historic mode buffer start pattern */ + private static final byte[] HISTORIC_BUFFER_START = {2, 10, 65, 68, 67, 79}; + + /** Standard mode buffer start pattern */ + private static final byte[] STANDARD_BUFFER_START = {2, 10, 65, 68, 83, 67}; + + /** Current TIC mode detected during operation */ + private TICMode currentMode; + + /** + * Creates a new TIC serial port channel instance with the specified configuration. + * + * @param configuration the TIC serial port configuration containing port settings and TIC mode + * parameters + * @throws ChannelException if the configuration is invalid or channel creation fails + */ + public ChannelTICSerialPort(ChannelTICSerialPortConfiguration configuration) + throws ChannelException { + super(configuration); + this.currentMode = null; + } + + @Override + public void setup(ChannelConfiguration configuration) throws ChannelException { + if (configuration == null) { + ChannelException.raiseInvalidConfiguration("null"); + } + if (!(configuration instanceof ChannelTICSerialPortConfiguration)) { + ChannelException.raiseInvalidConfigurationType( + configuration, ChannelTICSerialPortConfiguration.class.getSimpleName()); + } + super.setup(configuration); + } + + @Override + public ChannelTICSerialPortConfiguration getConfiguration() { + return (ChannelTICSerialPortConfiguration) this.configuration; + } + + /** + * Gets the current TIC mode being used by the channel. Returns the detected mode if available, + * otherwise returns the selected mode from configuration. + * + * @return the current TIC mode (HISTORIC, STANDARD, or AUTO) + */ + public TICMode getMode() { + return (this.getCurrentMode() != null) ? this.getCurrentMode() : this.getSelectedMode(); + } + + /** + * Gets the TIC mode selected in the channel configuration. + * + * @return the selected TIC mode from configuration + */ + public TICMode getSelectedMode() { + return this.getConfiguration().getTicMode(); + } + + /** + * Gets the currently detected TIC mode during operation. This may differ from the selected mode + * if auto-detection is enabled. + * + * @return the currently detected TIC mode, or null if not yet detected + */ + public TICMode getCurrentMode() { + return this.currentMode; + } + + @Override + public byte[] read() throws ChannelException { + long beginTime = System.currentTimeMillis(); + long elapsedTime = 0; + long timeout = this.getSyncReadTimeout(); + BytesArray buffer = new BytesArray(); + byte[] ticFrame = null; + int startOfFrame = -1, endOfFrame = -1; + + while (elapsedTime < timeout || timeout == 0) { + if (this.available() > 0) { + buffer.addAll(super.read(1)); + if (startOfFrame == -1) { + startOfFrame = buffer.indexOf(START_PATTERN); + if (startOfFrame != -1) { + endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); + if (endOfFrame != -1) { + ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); + break; + } + } + } else { + endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); + if (endOfFrame != -1) { + ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); + break; + } + } + } else { + Time.sleep(RECEIVE_DATA_POLLING_PERIOD); + elapsedTime = (System.currentTimeMillis() - beginTime); + } + } + + return ticFrame; + } + + @Override + protected void receiveData() { + if (this.currentMode == null) { + if (this.autoDetectMode() == null) { + this.onReadTimeout(); + return; + } + } + try { + byte[] ticFrame = this.read(); + + if (ticFrame == null) { + this.onReadTimeout(); + } else { + this.notifyOnDataRead(ticFrame); + } + } catch (ChannelException e) { + this.notifyOnErrorDetected( + ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), + "TIC read failed: " + e.getErrorInfo()); + } + } + + private void onReadTimeout() { + this.notifyOnErrorDetected( + ChannelSerialPortErrorCode.READ_TIMEOUT.getCode(ERROR_CODE_OFFSET), "TIC read timeout"); + this.configurePort(); + this.currentMode = null; + } + + private void configurePort() { + this.closePort(); + this.openPort(); + try { + this.flush(); + } catch (ChannelException e) { + } + } + + private void setSelectedMode(TICMode ticMode) { + try { + this.getConfiguration().setTicMode(ticMode); + } catch (DataDictionaryException e) { + this.logger.error("Cannot set TIC mode " + ticMode, e); + } + } + + private boolean checkAndUpdateMode(TICMode ticMode) { + boolean result = false; + this.setSelectedMode(ticMode); + this.configurePort(); + try { + byte[] ticFrame = this.read(); + if (ticFrame != null && ticFrame.length > HISTORIC_BUFFER_START.length) { + byte[] ticFrameStart = new byte[HISTORIC_BUFFER_START.length]; + System.arraycopy(ticFrame, 0, ticFrameStart, 0, ticFrameStart.length); + if (ticMode == TICMode.HISTORIC) { + if (Arrays.equals(ticFrameStart, HISTORIC_BUFFER_START)) { + result = true; + } + } else if (ticMode == TICMode.STANDARD) { + if (Arrays.equals(ticFrameStart, STANDARD_BUFFER_START)) { + result = true; + } + } + } + } catch (ChannelException e) { + this.notifyOnErrorDetected( + ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), + "TIC read failed: " + e.getErrorInfo()); + } + if (result) { + this.currentMode = ticMode; + } + this.setSelectedMode(TICMode.AUTO); + + return result; + } + + private TICMode autoDetectMode() { + if (this.getSelectedMode() != TICMode.AUTO) { + this.currentMode = this.getSelectedMode(); + return this.getSelectedMode(); + } + this.logger.debug("Auto detecting TIC Mode"); + if (!this.checkAndUpdateMode(TICMode.HISTORIC)) { + if (!this.checkAndUpdateMode(TICMode.STANDARD)) { + this.logger.debug("TIC Mode not detected"); + + return null; + } + this.logger.debug("TIC Mode STANDARD detected"); + + return TICMode.STANDARD; + } + this.logger.debug("TIC Mode HISTORIC detected"); + + return TICMode.HISTORIC; + } } diff --git a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java index bbd6e07..064013d 100644 --- a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java +++ b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java @@ -7,11 +7,6 @@ package enedis.lab.protocol.tic.channels; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.io.channels.serialport.ChannelSerialPortConfiguration; import enedis.lab.io.channels.serialport.Parity; import enedis.lab.protocol.tic.TICMode; @@ -19,244 +14,224 @@ import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * Configuration class for TIC serial port channels. - *

    - * This class extends {@link ChannelSerialPortConfiguration} to provide specific - * configuration for TIC protocol communication over serial ports. It handles - * TIC mode selection (HISTORIC, STANDARD, or AUTO) and automatically configures - * the appropriate serial port parameters based on the selected TIC mode. - *

    - * The class supports both manual TIC mode selection and automatic detection, - * with different baud rates for HISTORIC (1200 bps) and STANDARD (9600 bps) modes. + * + *

    This class extends {@link ChannelSerialPortConfiguration} to provide specific configuration + * for TIC protocol communication over serial ports. It handles TIC mode selection (HISTORIC, + * STANDARD, or AUTO) and automatically configures the appropriate serial port parameters based on + * the selected TIC mode. + * + *

    The class supports both manual TIC mode selection and automatic detection, with different baud + * rates for HISTORIC (1200 bps) and STANDARD (9600 bps) modes. * * @author Enedis Smarties team */ -public class ChannelTICSerialPortConfiguration extends ChannelSerialPortConfiguration -{ - - protected static final String KEY_TIC_MODE = "ticMode"; - - protected static final Number BAUDRATE_HISTORIC = 1200; - protected static final Number BAUDRATE_STANDARD = 9600; - protected static final Number[] BAUDRATE_ACCEPTED_VALUES = { BAUDRATE_HISTORIC, BAUDRATE_STANDARD }; - protected static final Parity PARITY_ACCEPTED_VALUE = Parity.EVEN; - protected static final Number DATA_BITS_ACCEPTED_VALUE = 7; - protected static final Number STOP_BITS_ACCEPTED_VALUE = 1.0d; - protected static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; - - - - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kTicMode; - - - protected ChannelTICSerialPortConfiguration() - { - super(); - this.loadKeyDescriptors(); - - this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); - this.kParity.setAcceptedValues(PARITY_ACCEPTED_VALUE); - this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUE); - this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUE); - } - - /** - * Creates a new TIC serial port configuration from a map of parameters. - *

    - * The map should contain configuration parameters that will be copied to this - * configuration object. The TIC mode and serial port parameters will be - * automatically configured based on the provided values. - * - * @param map the map containing configuration parameters - * @throws DataDictionaryException if the map contains invalid parameters - */ - public ChannelTICSerialPortConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Creates a new TIC serial port configuration by copying from another data dictionary. - *

    - * This constructor creates a new configuration object by copying all parameters - * from the provided data dictionary. The TIC mode and serial port parameters - * will be automatically configured based on the copied values. - * - * @param other the data dictionary to copy configuration from - * @throws DataDictionaryException if the data dictionary contains invalid parameters - */ - public ChannelTICSerialPortConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Creates a new TIC serial port configuration with the specified name and file. - *

    - * This constructor initializes the configuration with default values and associates - * it with the provided name and configuration file. The TIC mode will be set - * to AUTO by default, allowing automatic detection of the appropriate mode. - * - * @param name the configuration name - * @param file the configuration file - */ - public ChannelTICSerialPortConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Creates a new TIC serial port configuration with specific parameters. - *

    - * This constructor creates a configuration with the specified name, port name, - * and TIC mode. The serial port parameters (baud rate, parity, data bits, stop bits) - * will be automatically configured based on the selected TIC mode. - * - * @param name the configuration name - * @param portName the serial port name (e.g., "/dev/ttyUSB0" on Linux, "COM1" on Windows) - * @param ticMode the TIC mode (HISTORIC, STANDARD, or AUTO) - * @throws DataDictionaryException if the provided parameters are invalid - */ - public ChannelTICSerialPortConfiguration(String name, String portName, TICMode ticMode) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setPortName(portName); - this.setTicMode(ticMode); - - this.checkAndUpdate(); - } - - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TIC_MODE)) - { - this.setTicMode(TIC_MODE_DEFAULT_VALUE); - } - switch (this.getTicMode()) - { - case HISTORIC: - { - this.setBaudrate(BAUDRATE_HISTORIC); - this.setParity(PARITY_ACCEPTED_VALUE); - this.setDataBits(DATA_BITS_ACCEPTED_VALUE); - this.setStopBits(STOP_BITS_ACCEPTED_VALUE); - break; - } - case STANDARD: - case AUTO: - default: - { - this.setBaudrate(BAUDRATE_STANDARD); - this.setParity(PARITY_ACCEPTED_VALUE); - this.setDataBits(DATA_BITS_ACCEPTED_VALUE); - this.setStopBits(STOP_BITS_ACCEPTED_VALUE); - } - } - super.updateOptionalParameters(); - } - - - /** - * Gets the current TIC mode configuration. - *

    - * Returns the TIC mode that has been set for this configuration. The mode - * determines the communication protocol and serial port parameters used - * for TIC communication. - * - * @return the current TIC mode (HISTORIC, STANDARD, or AUTO) - */ - public TICMode getTicMode() - { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Sets the TIC mode for this configuration. - *

    - * When the TIC mode is set, the serial port parameters are automatically - * updated to match the requirements of the selected mode: - *

      - *
    • HISTORIC: 1200 bps, 7 data bits, even parity, 1 stop bit
    • - *
    • STANDARD: 9600 bps, 7 data bits, even parity, 1 stop bit
    • - *
    • AUTO: Automatically detects the appropriate mode
    • - *
    - * - * @param ticMode the TIC mode to set - * @throws DataDictionaryException if the TIC mode is invalid - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException - { - this.setTicMode((Object) ticMode); - } - - - /** - * Sets the TIC mode using an object value and automatically configures - * the baud rate based on the selected mode. - *

    - * This protected method is used internally to set the TIC mode from various - * object types and automatically adjusts the baud rate: - *

      - *
    • HISTORIC mode: sets baud rate to 1200 bps
    • - *
    • STANDARD or AUTO mode: sets baud rate to 9600 bps
    • - *
    - * - * @param ticMode the TIC mode object to set - * @throws DataDictionaryException if the TIC mode object is invalid - */ - protected void setTicMode(Object ticMode) throws DataDictionaryException - { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - switch (this.getTicMode()) - { - case HISTORIC: - this.setBaudrate(BAUDRATE_HISTORIC); - break; - case STANDARD: - case AUTO: - this.setBaudrate(BAUDRATE_STANDARD); - break; - default: - break; - } - } - - - /** - * Loads and initializes the key descriptors for TIC mode configuration. - *

    - * This private method sets up the key descriptor for the TIC mode parameter, - * which is used for validation and conversion of TIC mode values. It creates - * a key descriptor for the TICMode enum and adds it to the list of keys - * managed by this configuration object. - *

    - * If an error occurs during initialization, it wraps the DataDictionaryException - * in a RuntimeException to maintain the constructor's contract. - */ - private void loadKeyDescriptors() - { - try - { - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); - this.keys.add(this.kTicMode); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class ChannelTICSerialPortConfiguration extends ChannelSerialPortConfiguration { + + protected static final String KEY_TIC_MODE = "ticMode"; + + protected static final Number BAUDRATE_HISTORIC = 1200; + protected static final Number BAUDRATE_STANDARD = 9600; + protected static final Number[] BAUDRATE_ACCEPTED_VALUES = {BAUDRATE_HISTORIC, BAUDRATE_STANDARD}; + protected static final Parity PARITY_ACCEPTED_VALUE = Parity.EVEN; + protected static final Number DATA_BITS_ACCEPTED_VALUE = 7; + protected static final Number STOP_BITS_ACCEPTED_VALUE = 1.0d; + protected static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kTicMode; + + protected ChannelTICSerialPortConfiguration() { + super(); + this.loadKeyDescriptors(); + + this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); + this.kParity.setAcceptedValues(PARITY_ACCEPTED_VALUE); + this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUE); + this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUE); + } + + /** + * Creates a new TIC serial port configuration from a map of parameters. + * + *

    The map should contain configuration parameters that will be copied to this configuration + * object. The TIC mode and serial port parameters will be automatically configured based on the + * provided values. + * + * @param map the map containing configuration parameters + * @throws DataDictionaryException if the map contains invalid parameters + */ + public ChannelTICSerialPortConfiguration(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Creates a new TIC serial port configuration by copying from another data dictionary. + * + *

    This constructor creates a new configuration object by copying all parameters from the + * provided data dictionary. The TIC mode and serial port parameters will be automatically + * configured based on the copied values. + * + * @param other the data dictionary to copy configuration from + * @throws DataDictionaryException if the data dictionary contains invalid parameters + */ + public ChannelTICSerialPortConfiguration(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Creates a new TIC serial port configuration with the specified name and file. + * + *

    This constructor initializes the configuration with default values and associates it with + * the provided name and configuration file. The TIC mode will be set to AUTO by default, allowing + * automatic detection of the appropriate mode. + * + * @param name the configuration name + * @param file the configuration file + */ + public ChannelTICSerialPortConfiguration(String name, File file) { + this(); + this.init(name, file); + } + + /** + * Creates a new TIC serial port configuration with specific parameters. + * + *

    This constructor creates a configuration with the specified name, port name, and TIC mode. + * The serial port parameters (baud rate, parity, data bits, stop bits) will be automatically + * configured based on the selected TIC mode. + * + * @param name the configuration name + * @param portName the serial port name (e.g., "/dev/ttyUSB0" on Linux, "COM1" on Windows) + * @param ticMode the TIC mode (HISTORIC, STANDARD, or AUTO) + * @throws DataDictionaryException if the provided parameters are invalid + */ + public ChannelTICSerialPortConfiguration(String name, String portName, TICMode ticMode) + throws DataDictionaryException { + this(); + + this.setName(name); + this.setPortName(portName); + this.setTicMode(ticMode); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_TIC_MODE)) { + this.setTicMode(TIC_MODE_DEFAULT_VALUE); + } + switch (this.getTicMode()) { + case HISTORIC: + { + this.setBaudrate(BAUDRATE_HISTORIC); + this.setParity(PARITY_ACCEPTED_VALUE); + this.setDataBits(DATA_BITS_ACCEPTED_VALUE); + this.setStopBits(STOP_BITS_ACCEPTED_VALUE); + break; + } + case STANDARD: + case AUTO: + default: + { + this.setBaudrate(BAUDRATE_STANDARD); + this.setParity(PARITY_ACCEPTED_VALUE); + this.setDataBits(DATA_BITS_ACCEPTED_VALUE); + this.setStopBits(STOP_BITS_ACCEPTED_VALUE); + } + } + super.updateOptionalParameters(); + } + + /** + * Gets the current TIC mode configuration. + * + *

    Returns the TIC mode that has been set for this configuration. The mode determines the + * communication protocol and serial port parameters used for TIC communication. + * + * @return the current TIC mode (HISTORIC, STANDARD, or AUTO) + */ + public TICMode getTicMode() { + return (TICMode) this.data.get(KEY_TIC_MODE); + } + + /** + * Sets the TIC mode for this configuration. + * + *

    When the TIC mode is set, the serial port parameters are automatically updated to match the + * requirements of the selected mode: + * + *

      + *
    • HISTORIC: 1200 bps, 7 data bits, even parity, 1 stop bit + *
    • STANDARD: 9600 bps, 7 data bits, even parity, 1 stop bit + *
    • AUTO: Automatically detects the appropriate mode + *
    + * + * @param ticMode the TIC mode to set + * @throws DataDictionaryException if the TIC mode is invalid + */ + public void setTicMode(TICMode ticMode) throws DataDictionaryException { + this.setTicMode((Object) ticMode); + } + + /** + * Sets the TIC mode using an object value and automatically configures the baud rate based on the + * selected mode. + * + *

    This protected method is used internally to set the TIC mode from various object types and + * automatically adjusts the baud rate: + * + *

      + *
    • HISTORIC mode: sets baud rate to 1200 bps + *
    • STANDARD or AUTO mode: sets baud rate to 9600 bps + *
    + * + * @param ticMode the TIC mode object to set + * @throws DataDictionaryException if the TIC mode object is invalid + */ + protected void setTicMode(Object ticMode) throws DataDictionaryException { + this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); + switch (this.getTicMode()) { + case HISTORIC: + this.setBaudrate(BAUDRATE_HISTORIC); + break; + case STANDARD: + case AUTO: + this.setBaudrate(BAUDRATE_STANDARD); + break; + default: + break; + } + } + + /** + * Loads and initializes the key descriptors for TIC mode configuration. + * + *

    This private method sets up the key descriptor for the TIC mode parameter, which is used for + * validation and conversion of TIC mode values. It creates a key descriptor for the TICMode enum + * and adds it to the list of keys managed by this configuration object. + * + *

    If an error occurs during initialization, it wraps the DataDictionaryException in a + * RuntimeException to maintain the constructor's contract. + */ + private void loadKeyDescriptors() { + try { + this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); + this.keys.add(this.kTicMode); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java index cb319b2..8b45d44 100644 --- a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java +++ b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,12 +7,6 @@ package enedis.lab.io.channels.serialport; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -import org.apache.commons.lang3.SystemUtils; - import enedis.lab.io.channels.ChannelConfiguration; import enedis.lab.io.channels.ChannelException; import enedis.lab.io.channels.ChannelPhysical; @@ -21,32 +15,37 @@ import enedis.lab.io.serialport.SerialPortFinderBase; import enedis.lab.types.DataDictionaryException; import enedis.lab.util.SystemError; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; import jssc.SerialPort; import jssc.SerialPortEvent; import jssc.SerialPortEventListener; import jssc.SerialPortException; import jssc.SerialPortTimeoutException; +import org.apache.commons.lang3.SystemUtils; /** * Serial port communication channel implementation for TIC protocol. * - *

    This class extends {@link ChannelPhysical} and implements {@link SerialPortEventListener} to provide - * a robust serial port communication channel. It handles automatic port discovery, configuration, - * connection management, and real-time data transmission for TIC-compliant devices. + *

    This class extends {@link ChannelPhysical} and implements {@link SerialPortEventListener} to + * provide a robust serial port communication channel. It handles automatic port discovery, + * configuration, connection management, and real-time data transmission for TIC-compliant devices. * *

    Key features include: + * *

      - *
    • Automatic port detection and reconnection when devices are plugged/unplugged
    • - *
    • Support for both port ID and port name identification methods
    • - *
    • Configurable serial communication parameters (baud rate, parity, data bits, stop bits)
    • - *
    • Event-driven data reception with optional timeout handling
    • - *
    • Symbolic link resolution for Linux systems
    • - *
    • Error detection and automatic recovery mechanisms
    • + *
    • Automatic port detection and reconnection when devices are plugged/unplugged + *
    • Support for both port ID and port name identification methods + *
    • Configurable serial communication parameters (baud rate, parity, data bits, stop bits) + *
    • Event-driven data reception with optional timeout handling + *
    • Symbolic link resolution for Linux systems + *
    • Error detection and automatic recovery mechanisms *
    * *

    The channel operates in a periodic polling mode to monitor port availability and automatically - * handles port changes, busy conditions, and communication errors. It supports both synchronous - * and asynchronous data operations with proper exception handling. + * handles port changes, busy conditions, and communication errors. It supports both synchronous and + * asynchronous data operations with proper exception handling. * * @author Enedis Smarties team * @see ChannelPhysical @@ -55,1209 +54,1148 @@ * @see ChannelStatus * @see ChannelSerialPortErrorCode */ -public class ChannelSerialPort extends ChannelPhysical implements SerialPortEventListener -{ - /** Error code offset for serial port specific errors. */ - protected static final int ERROR_CODE_OFFSET = 1000; - - /** Default polling delay in milliseconds for port monitoring. */ - private static final int DEFAULT_DELAY = 500; - - /** Extended delay in milliseconds for port reopening attempts. */ - private static final int DELAY_REOPEN = DEFAULT_DELAY * 10; - - - /** - * Checks if a serial port is available on the system. - * - *

    This method queries the system to determine if a port with the specified identifier - * or name is currently available. The port ID takes precedence over port name if both - * are provided. - * - * @param portId the unique port identifier (may be null) - * @param portName the port name or path (may be null) - * @return true if the port is found and available, false otherwise - * @see SerialPortFinderBase#findByPortIdOrPortName(String, String) - */ - public static boolean isPortFound(String portId, String portName) - { - return SerialPortFinderBase.getInstance().findByPortIdOrPortName(portId, portName) != null; - } - - /** - * Finds the actual port name corresponding to the given port ID or port name. - * - *

    This method resolves the port identifier to its actual system port name. The port ID - * parameter takes priority over the port name parameter if both are provided. This is useful - * for converting abstract port identifiers to concrete system paths. - * - * @param portId the unique port identifier (has priority if not null) - * @param portName the port name or path (used as fallback if portId is null) - * @return the resolved port name, or null if no matching port is found - * @see SerialPortFinderBase#findByPortId(String) - * @see SerialPortFinderBase#findByPortName(String) - */ - public static String findPortName(String portId, String portName) - { - SerialPortDescriptor descriptor = null; - if (portId != null) - { - descriptor = SerialPortFinderBase.getInstance().findByPortId(portId); - } - else - { - descriptor = SerialPortFinderBase.getInstance().findByPortName(portName); - } - - return (descriptor != null) ? descriptor.getPortName() : null; - } - - /** - * Converts a {@link Parity} enumeration value to the corresponding JSSC library constant. - * - *

    This utility method maps the application-level parity enumeration to the underlying - * serial communication library constants required for port configuration. - * - * @param parity the parity setting from the configuration - * @return the corresponding JSSC SerialPort parity constant, or -1 for unsupported values - * @see SerialPort#PARITY_EVEN - * @see SerialPort#PARITY_ODD - * @see SerialPort#PARITY_NONE - * @see SerialPort#PARITY_MARK - * @see SerialPort#PARITY_SPACE - */ - public static int parityFromConfiguration(Parity parity) - { - int retVal = -1; - - switch (parity) - { - case EVEN: - retVal = SerialPort.PARITY_EVEN; - break; - case MARK: - retVal = SerialPort.PARITY_MARK; - break; - case NONE: - retVal = SerialPort.PARITY_NONE; - break; - case ODD: - retVal = SerialPort.PARITY_ODD; - break; - case SPACE: - retVal = SerialPort.PARITY_SPACE; - break; - default: /* Cas non atteignable */ - } - - return retVal; - } - - /** - * Converts a stop bits configuration value to the corresponding JSSC library constant. - * - *

    This utility method maps the configured stop bits value (1.0, 1.5, or 2.0) to the - * underlying serial communication library constants required for port configuration. - * - * @param stop_bits the stop bits value from the configuration (1.0, 1.5, or 2.0) - * @return the corresponding JSSC SerialPort stop bits constant, or -1 for unsupported values - * @see SerialPort#STOPBITS_1 - * @see SerialPort#STOPBITS_1_5 - * @see SerialPort#STOPBITS_2 - */ - public static int stopBitsFromConfiguration(float stop_bits) - { - int retVal = -1; - - if (1 == stop_bits) - { - retVal = SerialPort.STOPBITS_1; - } - - else if (1.5 == stop_bits) - { - retVal = SerialPort.STOPBITS_1_5; - } - - else if (2 == stop_bits) - { - retVal = SerialPort.STOPBITS_2; - } - - return retVal; - } - - /** JSSC SerialPort instance for handling low-level serial communication. */ - private SerialPort portHandler = null; - - /** Previously detected port name, used for change detection. */ - private String previousPortName; - - /** Currently detected port name, used for change detection. */ - private String currentPortName; - - /** - * Constructs a new serial port channel with the specified configuration. - * - *

    This constructor initializes the channel with the provided configuration and sets up - * the initial port detection. The channel will automatically resolve the port name based - * on the configuration's port ID or port name settings. - * - *

    The constructor sets up the periodic polling mechanism with a default delay and - * initializes the port change detection system. - * - * @param configuration the channel configuration containing port settings and parameters - * @throws ChannelException if the configuration is invalid or port cannot be resolved - * @throws IllegalArgumentException if configuration is null or not a ChannelSerialPortConfiguration - * @see ChannelSerialPortConfiguration - */ - public ChannelSerialPort(ChannelConfiguration configuration) throws ChannelException - { - super(configuration); - this.setPeriod(DEFAULT_DELAY); - - this.currentPortName = this.findPortName(); - this.previousPortName = this.currentPortName; - } - - - /** - * Starts the serial port channel and begins communication. - * - *

    This method initiates the channel by opening the serial port connection and starting - * the underlying periodic task. The port is opened with the configuration parameters - * specified during construction. - * - *

    If the port cannot be opened, the channel will enter an error state and attempt - * to reconnect periodically. - * - * @see #stop() - * @see #openPort() - */ - @Override - public void start() - { - this.logger.info("Channel " + this.getName() + " start (" + this.findPortName() + ")"); - this.openPort(); - super.start(); - } - - /** - * Stops the serial port channel and closes the connection. - * - *

    This method gracefully stops the channel by first stopping the underlying periodic task - * and then closing the serial port connection. All event listeners are removed and the - * port is properly released. - * - *

    The channel can be restarted by calling {@link #start()} again. - * - * @see #start() - * @see #closePort() - */ - @Override - public void stop() - { - this.logger.info("Channel " + this.getName() + " stop (" + this.findPortName() + ")"); - super.stop(); - this.closePort(); - } - - /** - * Configures the channel with a new configuration. - * - *

    This method validates and applies a new configuration to the channel. The configuration - * must be a valid {@link ChannelSerialPortConfiguration} instance. If the channel is currently - * started, it will be stopped and restarted with the new configuration. - * - *

    The configuration update includes port parameter changes, which may require reopening - * the serial port connection. - * - * @param configuration the new channel configuration to apply - * @throws ChannelException if the configuration is null, invalid, or not a serial port configuration - * @see ChannelSerialPortConfiguration - */ - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException - { - if (configuration == null) - { - ChannelException.raiseInvalidConfiguration("null"); - } - if (!(configuration instanceof ChannelSerialPortConfiguration)) - { - ChannelException.raiseInvalidConfigurationType(configuration, ChannelSerialPortConfiguration.class.getSimpleName()); - } - super.setup(configuration); - } - - /** - * Reads available data from the serial port. - * - *

    This method performs a non-blocking read operation to retrieve all available data - * from the serial port input buffer. The method returns immediately with whatever data - * is currently available, or an empty array if no data is present. - * - *

    The channel must be in a STARTED state and the port must be open for this operation - * to succeed. - * - * @return array of bytes read from the port, or empty array if no data available - * @throws ChannelException if the channel is not ready, port is not open, or read operation fails - * @see #read(int) - * @see #read(int, int) - */ - @Override - public byte[] read() throws ChannelException - { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - buffer = this.portHandler.readBytes(); - } - catch (SerialPortException exception) - { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - /** - * Writes data to the serial port. - * - *

    This method transmits the provided data bytes to the serial port. The operation - * is synchronous and will block until all data has been written to the port buffer. - * - *

    The channel must be in a STARTED state and the port must be open for this operation - * to succeed. - * - * @param data the byte array to write to the port (must not be null) - * @throws ChannelException if the channel is not ready, port is not open, or write operation fails - * @throws IllegalArgumentException if data parameter is null - */ - @Override - public void write(byte[] data) throws ChannelException - { - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - this.portHandler.writeBytes(data); - } - catch (SerialPortException exception) - { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot write bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - } - - /** - * Returns a copy of the current channel configuration. - * - *

    This method returns a defensive copy of the channel's configuration to prevent - * external modification of the internal state. The returned configuration contains - * all current settings including port identification, communication parameters, - * and timeout values. - * - * @return a copy of the current channel configuration - * @see ChannelSerialPortConfiguration - */ - @Override - public ChannelSerialPortConfiguration getConfiguration() - { - return (ChannelSerialPortConfiguration) this.configuration.clone(); - } - - - /** - * Sets up the channel with updated configuration. - * - *

    This protected method handles the internal setup process when the channel configuration - * is updated. It saves the current status, stops the channel, updates the real path for - * symbolic links on Linux systems, creates a new port handler if needed, and restores - * the previous state. - * - *

    This method ensures that configuration changes are applied atomically and that the - * channel state is properly maintained throughout the update process. - * - * @throws ChannelException if the setup process fails or configuration is invalid - * @see #updateRealPath() - */ - @Override - protected void setup() throws ChannelException - { - /* 1. Save current state */ - ChannelStatus currentStatus = this.status; - /* 2. Stop serial port */ - this.stop(); - /* 3. update path if symbolic link */ - this.updateRealPath(); - /* 4. Create serial port handler */ - if ((null == this.portHandler) || (false == this.getPortName().equals(this.portHandler.getPortName()))) - { - this.portHandler = new SerialPort(this.getPortName()); - } - /* 5. Restore current state */ - if (ChannelStatus.STARTED == currentStatus) - { - this.start(); - } - } - - - /** - * Handles serial port events from the JSSC library. - * - *

    This method is called by the JSSC library when serial port events occur. Currently, - * it only handles RXCHAR events (data received) by triggering the data reception process. - * Other event types are ignored as they are not relevant for TIC communication. - * - *

    The event handling is designed to be lightweight and delegates the actual data - * processing to the {@link #receiveData()} method. - * - * @param event the serial port event that occurred - * @see SerialPortEvent#RXCHAR - * @see #receiveData() - */ - @Override - public void serialEvent(SerialPortEvent event) - { - switch (event.getEventType()) - { - case SerialPortEvent.RXCHAR: - this.receiveData(); - break; - default: /* No action for other event types */ - } - } - - - /** - * Main processing loop for the serial port channel. - * - *

    This method is called periodically to monitor the port status and handle various - * scenarios including port detection, connection management, and data reception. - * - *

    When the channel is STARTED, it: - *

      - *
    • Checks if the port is still available and handles port not found scenarios
    • - *
    • Detects port name changes (e.g., due to USB device reconnection)
    • - *
    • Receives data if no event listener is configured (polling mode)
    • - *
    - * - *

    When the channel is not STARTED, it attempts to open the port if it becomes - * available, with extended delay on error conditions. - * - *

    The processing delay is dynamically adjusted based on the current state and - * error conditions to optimize performance and responsiveness. - * - * @see #onPortNotFound() - * @see #onPortNameChanged() - * @see #receiveData() - * @see #openPort() - */ - @Override - protected void process() - { - int delay = DEFAULT_DELAY; - - if (this.status == ChannelStatus.STARTED) - { - if (!this.isPortFound()) - { - this.onPortNotFound(); - } - else if (this.hasPortChanged()) - { - this.onPortNameChanged(); - } - else if (!this.hasEventListener()) - { - this.receiveData(); - } - } - else - { - if (this.isPortFound()) - { - this.openPort(); - if (this.status == ChannelStatus.ERROR) - { - delay = DELAY_REOPEN; - } - } - } - this.setPeriod(delay); - } - - - /** - * Performs a blocking read operation for a specific number of bytes. - * - *

    This method reads exactly the specified number of bytes from the serial port. - * The operation will block until the requested number of bytes is available or - * until the operation times out. - * - *

    The channel must be in a STARTED state and the port must be open for this - * operation to succeed. - * - * @param bytesCount the exact number of bytes to read - * @return array containing the read bytes, or null if the read operation fails - * @throws ChannelException if the channel is not ready, port is not open, or read operation fails - * @throws IllegalArgumentException if bytesCount is negative or zero - * @see #read() - * @see #read(int, int) - */ - public byte[] read(int bytesCount) throws ChannelException - { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - buffer = this.portHandler.readBytes(bytesCount); - } - catch (SerialPortException exception) - { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - /** - * Performs a blocking read operation with a specified timeout. - * - *

    This method reads exactly the specified number of bytes from the serial port - * within the given timeout period. If the timeout expires before the requested - * number of bytes is available, a timeout exception is thrown. - * - *

    The channel must be in a STARTED state and the port must be open for this - * operation to succeed. - * - * @param bytesCount the exact number of bytes to read - * @param timeout the timeout in milliseconds for the read operation - * @return array containing the read bytes, or null if the read operation fails - * @throws ChannelException if the channel is not ready, port is not open, read operation fails, or timeout occurs - * @throws IllegalArgumentException if bytesCount is negative or zero, or timeout is negative - * @see #read() - * @see #read(int) - */ - public byte[] read(int bytesCount, int timeout) throws ChannelException - { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - buffer = this.portHandler.readBytes(bytesCount, timeout); - } - catch (SerialPortException exception) - { - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - catch (SerialPortTimeoutException exception) - { - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - /** - * Returns the number of bytes available for reading from the serial port. - * - *

    This method queries the serial port input buffer to determine how many bytes - * are currently available for reading. This information is useful for determining - * whether data is available before performing a read operation. - * - *

    The channel must be in a STARTED state and the port must be open for this - * operation to succeed. - * - * @return the number of bytes available in the input buffer - * @throws ChannelException if the channel is not ready, port is not open, or the operation fails - * @see #read() - */ - public int available() throws ChannelException - { - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - return this.portHandler.getInputBufferBytesCount(); - } - catch (SerialPortException exception) - { - this.logger.error("Cannot get bytes available", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return 0; - } - - /** - * Flushes all input and output buffers of the serial port. - * - *

    This method clears both the input and output buffers of the serial port, - * discarding any pending data. This is useful for ensuring a clean communication - * state, particularly after errors or before starting a new communication session. - * - *

    The flush operation includes clearing receive buffers, transmit buffers, - * and aborting any pending operations. - * - *

    The channel must be in a STARTED state and the port must be open for this - * operation to succeed. - * - * @throws ChannelException if the channel is not ready, port is not open, or flush operation fails - * @see SerialPort#PURGE_RXCLEAR - * @see SerialPort#PURGE_TXCLEAR - */ - public void flush() throws ChannelException - { - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - int mask = SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_RXABORT | SerialPort.PURGE_TXCLEAR | SerialPort.PURGE_TXABORT; - if (!this.portHandler.purgePort(mask)) - { - this.logger.error("Channel " + this.getName() + " failed to purge port " + this.getPortName() + " (" + SystemError.getMessage() + ") "); - } - } - catch (SerialPortException exception) - { - this.logger.error("Cannot purge port", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - } - - /** - * Returns the port identifier from the channel configuration. - * - *

    The port ID is an alternative way to identify serial ports, typically used when - * the system provides unique identifiers for ports. This identifier takes precedence - * over the port name when both are specified. - * - * @return the port identifier, or null if not set in the configuration - * @see ChannelSerialPortConfiguration#getPortId() - * @see #getPortName() - */ - public String getPortId() - { - return this.getChannelConfiguration().getPortId(); - } - - /** - * Returns the port name from the channel configuration. - * - *

    The port name is the system path or name used to identify the serial port. - * This is typically a device path like "/dev/ttyUSB0" on Linux or "COM1" on Windows. - * - * @return the port name, or null if not set in the configuration - * @see ChannelSerialPortConfiguration#getPortName() - * @see #getPortId() - */ - public String getPortName() - { - return this.getChannelConfiguration().getPortName(); - } - - /** - * Returns the name of the currently opened serial port. - * - *

    This method returns the actual port name that is currently opened by the JSSC - * library. This may differ from the configured port name if symbolic links were - * resolved or if the port was dynamically assigned. - * - * @return the name of the opened port, or null if no port is currently open - * @see #getPortName() - * @see #getPortId() - */ - public String getPortNameOpened() - { - return (this.portHandler != null) ? this.portHandler.getPortName() : null; - } - - /** - * Finds the port name associated with the current channel configuration. - * - *

    This method resolves the port name based on the configuration's port ID or - * port name settings. It uses the same resolution logic as the static - * {@link #findPortName(String, String)} method but operates on the current - * configuration. - * - * @return the resolved port name, or null if no matching port is found - * @see #findPortName(String, String) - * @see #getPortId() - * @see #getPortName() - */ - public String findPortName() - { - return findPortName(this.getPortId(), this.getPortName()); - } - - /** - * Returns the current baud rate configured for the serial port. - * - *

    The baud rate determines the speed of serial communication in bits per second. - * - * @return the configured baud rate value - * @see ChannelSerialPortConfiguration#getBaudrate() - */ - public int getBaudrate() - { - return this.getChannelConfiguration().getBaudrate().intValue(); - } - - /** - * Returns the number of data bits configured for serial communication. - * - *

    Data bits define the number of bits used to represent each character transmitted. - * Standard values are 7 (for ASCII) or 8 (for binary data). TIC protocol typically - * uses 7 data bits. - * - * @return the number of data bits (5, 6, 7, or 8) - * @see ChannelSerialPortConfiguration#getDataBits() - */ - public int getDataBits() - { - return this.getChannelConfiguration().getDataBits().intValue(); - } - - /** - * Returns the number of stop bits configured for serial communication. - * - *

    Stop bits mark the end of each data frame. Standard values are 1.0, 1.5, or 2.0. - * Most serial communications use 1.0 stop bits. - * - * @return the number of stop bits (1.0, 1.5, or 2.0) - * @see ChannelSerialPortConfiguration#getStopBits() - */ - public float getStopBits() - { - return this.getChannelConfiguration().getStopBits().floatValue(); - } - - /** - * Returns the parity setting configured for serial communication. - * - *

    Parity is used for error detection by adding an extra bit to each data frame. - * TIC protocol typically uses EVEN parity for reliable data transmission. - * - * @return the parity setting from the configuration - * @see Parity - * @see ChannelSerialPortConfiguration#getParity() - */ - public Parity getParity() - { - return this.getChannelConfiguration().getParity(); - } - - /** - * Returns the timeout value for synchronous read operations. - * - *

    This timeout determines how long a synchronous read operation will wait for - * data before timing out. The value is specified in milliseconds. - * - * @return the timeout value in milliseconds for synchronous read operations - * @see ChannelSerialPortConfiguration#getSyncReadTimeout() - * @see #read(int, int) - */ - public int getSyncReadTimeout() - { - return this.getChannelConfiguration().getSyncReadTimeout().intValue(); - } - - - /** - * Determines if the channel has an event listener configured. - * - *

    This method indicates whether the channel is configured to use event-driven - * data reception (via SerialPortEventListener) or polling-based reception. - * - *

    The base implementation returns false, indicating polling mode. Subclasses - * can override this method to enable event-driven mode. - * - * @return true if event listener is configured, false for polling mode - * @see #addEventListener() - * @see #removeEventListener() - */ - protected boolean hasEventListener() - { - return false; - } - - /** - * Adds a serial port event listener for asynchronous data reception. - * - *

    This method configures the serial port to generate events when data is received, - * control signals change, or other port events occur. It sets up the event mask to - * listen for RXCHAR (data received), CTS (clear to send), and DSR (data set ready) - * events. - * - *

    The channel instance is registered as the event listener, so {@link #serialEvent(SerialPortEvent)} - * will be called when events occur. - * - * @return true if the event listener was successfully added, false otherwise - * @see #removeEventListener() - * @see #serialEvent(SerialPortEvent) - * @see SerialPort#MASK_RXCHAR - */ - protected boolean addEventListener() - { - if (this.portHandler == null) - { - return false; - } - int mask = SerialPort.MASK_RXCHAR + SerialPort.MASK_CTS + SerialPort.MASK_DSR; - try - { - if (!this.portHandler.setEventsMask(mask)) - { - this.logger.error("Channel " + this.getName() + " failed to set event mask on port " + this.getPortName() + " (" + SystemError.getMessage() + ") "); - return false; - } - } - catch (SerialPortException exception) - { - this.logger.error("Channel " + this.getName() + " failed to set event mask on port " + this.getPortName() + " : " + exception.getMessage()); - return false; - } - try - { - this.portHandler.addEventListener(this); - } - catch (SerialPortException e) - { - this.logger.error("Channel " + this.getName() + " failed to add listener :" + e.getMessage(), e); - return false; - } - - return true; - } - - /** - * Removes the serial port event listener. - * - *

    This method unregisters the channel instance as an event listener from the - * serial port. After calling this method, the channel will no longer receive - * asynchronous events and will operate in polling mode. - * - *

    This method is typically called when stopping the channel or switching to - * polling-based data reception. - * - * @return true if the event listener was successfully removed, false otherwise - * @see #addEventListener() - * @see #hasEventListener() - */ - protected boolean removeEventListener() - { - if (this.portHandler == null) - { - return false; - } - try - { - if (!this.portHandler.removeEventListener()) - { - this.logger.error("Channel " + this.getName() + " failed to remove listener"); - return false; - } - } - catch (SerialPortException e) - { - this.logger.error("Channel " + this.getName() + " failed to remove listener :" + e.getMessage(), e); - return false; - } - - return true; - } - - /** - * Receives and processes data from the serial port. - * - *

    This method checks for available data in the serial port input buffer and - * reads it if present. The received data is then passed to registered listeners - * via the notification system. - * - *

    If no data is available, the method returns immediately without error. - * If a read error occurs, it notifies listeners with an appropriate error code. - * - *

    This method is called both from the periodic processing loop (polling mode) - * and from the serial event handler (event-driven mode). - * - * @see #read() - * @see #available() - * @see #notifyOnDataRead(byte[]) - * @see ChannelSerialPortErrorCode#READ_FAILED - */ - protected void receiveData() - { - try - { - if (this.available() > 0) - { - byte[] buffer = this.read(); - this.notifyOnDataRead(buffer); - } - } - catch (ChannelException exception) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), exception.getMessage()); - } - } - - - /** - * Checks if the port name has changed since the last check. - * - *

    This method compares the currently detected port name with the previously - * detected port name to determine if the port has changed. This typically occurs - * when a USB device is unplugged and plugged back in, causing the system to assign - * a new device path. - * - *

    The comparison is case-insensitive to handle variations in how the system - * reports port names. - * - * @return true if the port name has changed, false otherwise - * @see #onPortNameChanged() - */ - private boolean hasPortChanged() - { - boolean portChanged = false; - - this.currentPortName = this.findPortName(); - - if (this.currentPortName != null && !this.currentPortName.equalsIgnoreCase(this.previousPortName)) - { - portChanged = true; - } - - return portChanged; - } - - /** - * Handles the event when the port name has changed. - * - *

    This method is called when a port name change is detected. It logs the change, - * sets the channel status to ERROR, forcibly closes the current port connection, - * notifies listeners of the error, and updates the previous port name for future - * comparisons. - * - *

    Port name changes typically occur when USB devices are reconnected and the - * system assigns a new device path (e.g., /dev/ttyUSB0 → /dev/ttyUSB1). - * - * @see #hasPortChanged() - * @see ChannelSerialPortErrorCode#PORT_NAME_HAS_CHANGED - */ - private void onPortNameChanged() - { - String errorMessage = "Port name has changed ( " + this.previousPortName + " --> " + this.currentPortName + " )"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_NAME_HAS_CHANGED.getCode(ERROR_CODE_OFFSET), errorMessage); - this.previousPortName = this.currentPortName; - } - - /** - * Updates the port name to its real path by resolving symbolic links. - * - *

    This method is only executed on Linux systems to resolve symbolic links to their - * actual device paths. This is important for USB devices that may be represented - * by symbolic links in /dev/disk/by-id/ or similar locations. - * - *

    The method uses the system 'realpath' command to resolve the symbolic link - * and updates the channel configuration with the resolved path. - * - * @throws ChannelException if the realpath resolution fails or configuration update fails - * @see SystemUtils#IS_OS_LINUX - */ - private void updateRealPath() throws ChannelException - { - if (SystemUtils.IS_OS_LINUX) - { - String realPortName; - try - { - Process p = Runtime.getRuntime().exec("realpath " + this.getPortName()); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream())); - realPortName = stdInput.readLine(); - this.getChannelConfiguration().setPortName(realPortName); - } - catch (IOException | DataDictionaryException e) - { - throw new ChannelException(ChannelException.ERRCODE_INVALID_CONFIGURATION, "Cannot resolve realpath (" + e.getMessage() + ")"); - } - } - } - - /** - * Returns the channel configuration cast to the specific serial port configuration type. - * - *

    This helper method provides type-safe access to the serial port specific - * configuration without requiring explicit casting throughout the code. - * - * @return the channel configuration as a ChannelSerialPortConfiguration instance - */ - private ChannelSerialPortConfiguration getChannelConfiguration() - { - return (ChannelSerialPortConfiguration) this.configuration; - } - - /** - * Opens and configures the serial port connection. - * - *

    This method performs the complete process of opening a serial port connection: - *

      - *
    1. Checks if the port is already open to avoid duplicate operations
    2. - *
    3. Creates a new SerialPort handler if needed
    4. - *
    5. Opens the port using the resolved port name
    6. - *
    7. Configures the port with the specified communication parameters
    8. - *
    9. Adds event listener if configured for event-driven mode
    10. - *
    11. Sets the channel status to STARTED
    12. - *
    - * - *

    If any step fails, the method logs the error, handles specific error conditions - * (like PORT_BUSY), and ensures the channel enters an appropriate error state. - * - * @see #closePort() - * @see #findPortName() - * @see #hasEventListener() - * @see #addEventListener() - * @see ChannelSerialPortErrorCode#PORT_BUSY - */ - protected void openPort() - { - /* 1. Check if serial port is already opened */ - if (this.portHandler != null && this.portHandler.isOpened()) - { - return; - } - /* 2. Open serial port */ - String portNameFound = this.findPortName(); - try - { - this.logger.info("Channel " + this.getName() + " opening port " + portNameFound); - if (this.portHandler == null) - { - this.portHandler = new SerialPort(portNameFound); - } - if (!this.portHandler.openPort()) - { - this.logger.error("Channel " + this.getName() + " failed to open port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); - return; - } - this.logger.info("Channel " + this.getName() + " configuring port " + portNameFound); - if (!this.portHandler.setParams(this.getBaudrate(), this.getDataBits(), stopBitsFromConfiguration(this.getStopBits()), parityFromConfiguration(this.getParity()))) - { - this.logger.error("Channel " + this.getName() + " failed to configure port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); - return; - } - } - catch (SerialPortException exception) - { - String errorMessage = "Channel " + this.getName() + " failed to open port " + this.getPortNameOpened() + " : " + exception.getMessage(); - this.logger.error(errorMessage); - if (exception.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_BUSY.getCode(ERROR_CODE_OFFSET), errorMessage); - } - this.closePortForced(); - return; - } - /* 3. Add serial event listener */ - if (this.hasEventListener()) - { - if (!this.addEventListener()) - { - return; - } - } - this.setStatus(ChannelStatus.STARTED); - } - - /** - * Closes the serial port connection gracefully. - * - *

    This method performs the complete process of closing a serial port connection: - *

      - *
    1. Checks if the port is already closed to avoid duplicate operations
    2. - *
    3. Removes event listener if one was configured
    4. - *
    5. Closes the port and releases system resources
    6. - *
    7. Sets the channel status to STOPPED
    8. - *
    - * - *

    If any step fails, the method logs the error but continues with the remaining - * cleanup steps to ensure the channel is in a consistent state. - * - * @see #openPort() - * @see #closePortForced() - * @see #hasEventListener() - * @see #removeEventListener() - */ - protected void closePort() - { - /* 1. Check if serial port is already closed */ - if (this.portHandler == null || !this.portHandler.isOpened()) - { - return; - } - /* 2. Remove serial event listener */ - if (this.hasEventListener()) - { - this.removeEventListener(); - } - /* 3. Close serial port */ - try - { - this.logger.info("Channel " + this.getName() + " closing port " + this.getPortNameOpened()); - if (!this.portHandler.closePort()) - { - this.logger.error("Channel " + this.getName() + " failed to close port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); - return; - } - } - catch (SerialPortException exception) - { - this.logger.error("Channel " + this.getName() + " failed to close port " + this.getPortNameOpened() + " : " + exception.getMessage()); - return; - } - this.setStatus(ChannelStatus.STOPPED); - } - - /** - * Forcefully closes the serial port and resets the port handler. - * - *

    This method performs an emergency closure of the serial port connection without - * the normal cleanup procedures. It attempts to close the port and then creates a - * new SerialPort handler instance for future use. - * - *

    This method is typically used in error conditions where the normal close - * procedure may fail or when the port needs to be forcibly reset. - * - *

    Unlike {@link #closePort()}, this method does not remove event listeners or - * update the channel status, making it suitable for error recovery scenarios. - * - * @see #closePort() - * @see #findPortName() - */ - protected void closePortForced() - { - try - { - this.portHandler.closePort(); - } - catch (SerialPortException exception) - { - this.logger.error("Channel " + this.getName() + " fail on close : close " + this.getPortNameOpened() + " failed due to " + exception.getMessage()); - } - this.portHandler = new SerialPort(this.findPortName()); - } - - /** - * Checks if the configured port is currently available on the system. - * - *

    This method uses the current channel configuration to determine if the - * specified port is available. It delegates to the static {@link #isPortFound(String, String)} - * method with the current port ID and port name. - * - * @return true if the configured port is found and available, false otherwise - * @see #isPortFound(String, String) - * @see #getPortId() - * @see #getPortName() - */ - protected boolean isPortFound() - { - return isPortFound(this.getPortId(), this.getPortName()); - } - - /** - * Checks if the currently opened port matches the expected port. - * - *

    This method compares the name of the currently opened port with the port name - * that should be opened according to the current configuration. This is useful - * for detecting situations where the port has changed or become unavailable. - * - * @return true if the opened port matches the expected port, false otherwise - * @see #getPortNameOpened() - * @see #findPortName() - */ - protected boolean isPortOpenedFound() - { - String portNameOpened = this.getPortNameOpened(); - if (portNameOpened == null) - { - return false; - } - String portNameFound = this.findPortName(); - - return portNameOpened.equals(portNameFound); - } - - /** - * Handles the event when the configured port is not found. - * - *

    This method is called when the system cannot locate the configured serial port. - * It logs the error, sets the channel status to ERROR, forcibly closes any existing - * connection, and notifies listeners of the error condition. - * - *

    This typically occurs when a USB device is unplugged or the device driver - * is not properly installed. - * - * @see #isPortFound() - * @see ChannelSerialPortErrorCode#PORT_NOT_FOUND - */ - protected void onPortNotFound() - { - String errorMessage = "Channel " + this.getName() + " : serial port " + this.findPortName() + " not found"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); - } - - /** - * Handles the event when the currently opened port is no longer found. - * - *

    This method is called when the system can no longer locate the port that was - * previously opened by the channel. This indicates that the device has been - * physically disconnected or the system has lost access to it. - * - *

    It logs the error, sets the channel status to ERROR, forcibly closes the - * connection, and notifies listeners of the error condition. - * - * @see #isPortOpenedFound() - * @see ChannelSerialPortErrorCode#PORT_OPENED_NOT_FOUND - */ - protected void onPortOpenedNotFound() - { - String errorMessage = "Channel " + this.getName() + " : serial port " + this.getPortNameOpened() + " opened not found"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_OPENED_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); - } +public class ChannelSerialPort extends ChannelPhysical implements SerialPortEventListener { + /** Error code offset for serial port specific errors. */ + protected static final int ERROR_CODE_OFFSET = 1000; + + /** Default polling delay in milliseconds for port monitoring. */ + private static final int DEFAULT_DELAY = 500; + + /** Extended delay in milliseconds for port reopening attempts. */ + private static final int DELAY_REOPEN = DEFAULT_DELAY * 10; + + /** + * Checks if a serial port is available on the system. + * + *

    This method queries the system to determine if a port with the specified identifier or name + * is currently available. The port ID takes precedence over port name if both are provided. + * + * @param portId the unique port identifier (may be null) + * @param portName the port name or path (may be null) + * @return true if the port is found and available, false otherwise + * @see SerialPortFinderBase#findByPortIdOrPortName(String, String) + */ + public static boolean isPortFound(String portId, String portName) { + return SerialPortFinderBase.getInstance().findByPortIdOrPortName(portId, portName) != null; + } + + /** + * Finds the actual port name corresponding to the given port ID or port name. + * + *

    This method resolves the port identifier to its actual system port name. The port ID + * parameter takes priority over the port name parameter if both are provided. This is useful for + * converting abstract port identifiers to concrete system paths. + * + * @param portId the unique port identifier (has priority if not null) + * @param portName the port name or path (used as fallback if portId is null) + * @return the resolved port name, or null if no matching port is found + * @see SerialPortFinderBase#findByPortId(String) + * @see SerialPortFinderBase#findByPortName(String) + */ + public static String findPortName(String portId, String portName) { + SerialPortDescriptor descriptor = null; + if (portId != null) { + descriptor = SerialPortFinderBase.getInstance().findByPortId(portId); + } else { + descriptor = SerialPortFinderBase.getInstance().findByPortName(portName); + } + + return (descriptor != null) ? descriptor.getPortName() : null; + } + + /** + * Converts a {@link Parity} enumeration value to the corresponding JSSC library constant. + * + *

    This utility method maps the application-level parity enumeration to the underlying serial + * communication library constants required for port configuration. + * + * @param parity the parity setting from the configuration + * @return the corresponding JSSC SerialPort parity constant, or -1 for unsupported values + * @see SerialPort#PARITY_EVEN + * @see SerialPort#PARITY_ODD + * @see SerialPort#PARITY_NONE + * @see SerialPort#PARITY_MARK + * @see SerialPort#PARITY_SPACE + */ + public static int parityFromConfiguration(Parity parity) { + int retVal = -1; + + switch (parity) { + case EVEN: + retVal = SerialPort.PARITY_EVEN; + break; + case MARK: + retVal = SerialPort.PARITY_MARK; + break; + case NONE: + retVal = SerialPort.PARITY_NONE; + break; + case ODD: + retVal = SerialPort.PARITY_ODD; + break; + case SPACE: + retVal = SerialPort.PARITY_SPACE; + break; + default: /* Cas non atteignable */ + } + + return retVal; + } + + /** + * Converts a stop bits configuration value to the corresponding JSSC library constant. + * + *

    This utility method maps the configured stop bits value (1.0, 1.5, or 2.0) to the underlying + * serial communication library constants required for port configuration. + * + * @param stop_bits the stop bits value from the configuration (1.0, 1.5, or 2.0) + * @return the corresponding JSSC SerialPort stop bits constant, or -1 for unsupported values + * @see SerialPort#STOPBITS_1 + * @see SerialPort#STOPBITS_1_5 + * @see SerialPort#STOPBITS_2 + */ + public static int stopBitsFromConfiguration(float stop_bits) { + int retVal = -1; + + if (1 == stop_bits) { + retVal = SerialPort.STOPBITS_1; + } else if (1.5 == stop_bits) { + retVal = SerialPort.STOPBITS_1_5; + } else if (2 == stop_bits) { + retVal = SerialPort.STOPBITS_2; + } + + return retVal; + } + + /** JSSC SerialPort instance for handling low-level serial communication. */ + private SerialPort portHandler = null; + + /** Previously detected port name, used for change detection. */ + private String previousPortName; + + /** Currently detected port name, used for change detection. */ + private String currentPortName; + + /** + * Constructs a new serial port channel with the specified configuration. + * + *

    This constructor initializes the channel with the provided configuration and sets up the + * initial port detection. The channel will automatically resolve the port name based on the + * configuration's port ID or port name settings. + * + *

    The constructor sets up the periodic polling mechanism with a default delay and initializes + * the port change detection system. + * + * @param configuration the channel configuration containing port settings and parameters + * @throws ChannelException if the configuration is invalid or port cannot be resolved + * @throws IllegalArgumentException if configuration is null or not a + * ChannelSerialPortConfiguration + * @see ChannelSerialPortConfiguration + */ + public ChannelSerialPort(ChannelConfiguration configuration) throws ChannelException { + super(configuration); + this.setPeriod(DEFAULT_DELAY); + + this.currentPortName = this.findPortName(); + this.previousPortName = this.currentPortName; + } + + /** + * Starts the serial port channel and begins communication. + * + *

    This method initiates the channel by opening the serial port connection and starting the + * underlying periodic task. The port is opened with the configuration parameters specified during + * construction. + * + *

    If the port cannot be opened, the channel will enter an error state and attempt to reconnect + * periodically. + * + * @see #stop() + * @see #openPort() + */ + @Override + public void start() { + this.logger.info("Channel " + this.getName() + " start (" + this.findPortName() + ")"); + this.openPort(); + super.start(); + } + + /** + * Stops the serial port channel and closes the connection. + * + *

    This method gracefully stops the channel by first stopping the underlying periodic task and + * then closing the serial port connection. All event listeners are removed and the port is + * properly released. + * + *

    The channel can be restarted by calling {@link #start()} again. + * + * @see #start() + * @see #closePort() + */ + @Override + public void stop() { + this.logger.info("Channel " + this.getName() + " stop (" + this.findPortName() + ")"); + super.stop(); + this.closePort(); + } + + /** + * Configures the channel with a new configuration. + * + *

    This method validates and applies a new configuration to the channel. The configuration must + * be a valid {@link ChannelSerialPortConfiguration} instance. If the channel is currently + * started, it will be stopped and restarted with the new configuration. + * + *

    The configuration update includes port parameter changes, which may require reopening the + * serial port connection. + * + * @param configuration the new channel configuration to apply + * @throws ChannelException if the configuration is null, invalid, or not a serial port + * configuration + * @see ChannelSerialPortConfiguration + */ + @Override + public void setup(ChannelConfiguration configuration) throws ChannelException { + if (configuration == null) { + ChannelException.raiseInvalidConfiguration("null"); + } + if (!(configuration instanceof ChannelSerialPortConfiguration)) { + ChannelException.raiseInvalidConfigurationType( + configuration, ChannelSerialPortConfiguration.class.getSimpleName()); + } + super.setup(configuration); + } + + /** + * Reads available data from the serial port. + * + *

    This method performs a non-blocking read operation to retrieve all available data from the + * serial port input buffer. The method returns immediately with whatever data is currently + * available, or an empty array if no data is present. + * + *

    The channel must be in a STARTED state and the port must be open for this operation to + * succeed. + * + * @return array of bytes read from the port, or empty array if no data available + * @throws ChannelException if the channel is not ready, port is not open, or read operation fails + * @see #read(int) + * @see #read(int, int) + */ + @Override + public byte[] read() throws ChannelException { + byte[] buffer = null; + + if (this.portHandler == null || !this.portHandler.isOpened()) { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try { + buffer = this.portHandler.readBytes(); + } catch (SerialPortException exception) { + this.setStatus(ChannelStatus.ERROR); + this.logger.error("Cannot read bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + + return buffer; + } + + /** + * Writes data to the serial port. + * + *

    This method transmits the provided data bytes to the serial port. The operation is + * synchronous and will block until all data has been written to the port buffer. + * + *

    The channel must be in a STARTED state and the port must be open for this operation to + * succeed. + * + * @param data the byte array to write to the port (must not be null) + * @throws ChannelException if the channel is not ready, port is not open, or write operation + * fails + * @throws IllegalArgumentException if data parameter is null + */ + @Override + public void write(byte[] data) throws ChannelException { + if (this.portHandler == null || !this.portHandler.isOpened()) { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try { + this.portHandler.writeBytes(data); + } catch (SerialPortException exception) { + this.setStatus(ChannelStatus.ERROR); + this.logger.error("Cannot write bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + } + + /** + * Returns a copy of the current channel configuration. + * + *

    This method returns a defensive copy of the channel's configuration to prevent external + * modification of the internal state. The returned configuration contains all current settings + * including port identification, communication parameters, and timeout values. + * + * @return a copy of the current channel configuration + * @see ChannelSerialPortConfiguration + */ + @Override + public ChannelSerialPortConfiguration getConfiguration() { + return (ChannelSerialPortConfiguration) this.configuration.clone(); + } + + /** + * Sets up the channel with updated configuration. + * + *

    This protected method handles the internal setup process when the channel configuration is + * updated. It saves the current status, stops the channel, updates the real path for symbolic + * links on Linux systems, creates a new port handler if needed, and restores the previous state. + * + *

    This method ensures that configuration changes are applied atomically and that the channel + * state is properly maintained throughout the update process. + * + * @throws ChannelException if the setup process fails or configuration is invalid + * @see #updateRealPath() + */ + @Override + protected void setup() throws ChannelException { + /* 1. Save current state */ + ChannelStatus currentStatus = this.status; + /* 2. Stop serial port */ + this.stop(); + /* 3. update path if symbolic link */ + this.updateRealPath(); + /* 4. Create serial port handler */ + if ((null == this.portHandler) + || (false == this.getPortName().equals(this.portHandler.getPortName()))) { + this.portHandler = new SerialPort(this.getPortName()); + } + /* 5. Restore current state */ + if (ChannelStatus.STARTED == currentStatus) { + this.start(); + } + } + + /** + * Handles serial port events from the JSSC library. + * + *

    This method is called by the JSSC library when serial port events occur. Currently, it only + * handles RXCHAR events (data received) by triggering the data reception process. Other event + * types are ignored as they are not relevant for TIC communication. + * + *

    The event handling is designed to be lightweight and delegates the actual data processing to + * the {@link #receiveData()} method. + * + * @param event the serial port event that occurred + * @see SerialPortEvent#RXCHAR + * @see #receiveData() + */ + @Override + public void serialEvent(SerialPortEvent event) { + switch (event.getEventType()) { + case SerialPortEvent.RXCHAR: + this.receiveData(); + break; + default: /* No action for other event types */ + } + } + + /** + * Main processing loop for the serial port channel. + * + *

    This method is called periodically to monitor the port status and handle various scenarios + * including port detection, connection management, and data reception. + * + *

    When the channel is STARTED, it: + * + *

      + *
    • Checks if the port is still available and handles port not found scenarios + *
    • Detects port name changes (e.g., due to USB device reconnection) + *
    • Receives data if no event listener is configured (polling mode) + *
    + * + *

    When the channel is not STARTED, it attempts to open the port if it becomes available, with + * extended delay on error conditions. + * + *

    The processing delay is dynamically adjusted based on the current state and error conditions + * to optimize performance and responsiveness. + * + * @see #onPortNotFound() + * @see #onPortNameChanged() + * @see #receiveData() + * @see #openPort() + */ + @Override + protected void process() { + int delay = DEFAULT_DELAY; + + if (this.status == ChannelStatus.STARTED) { + if (!this.isPortFound()) { + this.onPortNotFound(); + } else if (this.hasPortChanged()) { + this.onPortNameChanged(); + } else if (!this.hasEventListener()) { + this.receiveData(); + } + } else { + if (this.isPortFound()) { + this.openPort(); + if (this.status == ChannelStatus.ERROR) { + delay = DELAY_REOPEN; + } + } + } + this.setPeriod(delay); + } + + /** + * Performs a blocking read operation for a specific number of bytes. + * + *

    This method reads exactly the specified number of bytes from the serial port. The operation + * will block until the requested number of bytes is available or until the operation times out. + * + *

    The channel must be in a STARTED state and the port must be open for this operation to + * succeed. + * + * @param bytesCount the exact number of bytes to read + * @return array containing the read bytes, or null if the read operation fails + * @throws ChannelException if the channel is not ready, port is not open, or read operation fails + * @throws IllegalArgumentException if bytesCount is negative or zero + * @see #read() + * @see #read(int, int) + */ + public byte[] read(int bytesCount) throws ChannelException { + byte[] buffer = null; + + if (this.portHandler == null || !this.portHandler.isOpened()) { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try { + buffer = this.portHandler.readBytes(bytesCount); + } catch (SerialPortException exception) { + this.setStatus(ChannelStatus.ERROR); + this.logger.error("Cannot read bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + + return buffer; + } + + /** + * Performs a blocking read operation with a specified timeout. + * + *

    This method reads exactly the specified number of bytes from the serial port within the + * given timeout period. If the timeout expires before the requested number of bytes is available, + * a timeout exception is thrown. + * + *

    The channel must be in a STARTED state and the port must be open for this operation to + * succeed. + * + * @param bytesCount the exact number of bytes to read + * @param timeout the timeout in milliseconds for the read operation + * @return array containing the read bytes, or null if the read operation fails + * @throws ChannelException if the channel is not ready, port is not open, read operation fails, + * or timeout occurs + * @throws IllegalArgumentException if bytesCount is negative or zero, or timeout is negative + * @see #read() + * @see #read(int) + */ + public byte[] read(int bytesCount, int timeout) throws ChannelException { + byte[] buffer = null; + + if (this.portHandler == null || !this.portHandler.isOpened()) { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try { + buffer = this.portHandler.readBytes(bytesCount, timeout); + } catch (SerialPortException exception) { + this.logger.error("Cannot read bytes", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } catch (SerialPortTimeoutException exception) { + ChannelException.raiseInternalError(exception.getMessage()); + } + + return buffer; + } + + /** + * Returns the number of bytes available for reading from the serial port. + * + *

    This method queries the serial port input buffer to determine how many bytes are currently + * available for reading. This information is useful for determining whether data is available + * before performing a read operation. + * + *

    The channel must be in a STARTED state and the port must be open for this operation to + * succeed. + * + * @return the number of bytes available in the input buffer + * @throws ChannelException if the channel is not ready, port is not open, or the operation fails + * @see #read() + */ + public int available() throws ChannelException { + if (this.portHandler == null || !this.portHandler.isOpened()) { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try { + return this.portHandler.getInputBufferBytesCount(); + } catch (SerialPortException exception) { + this.logger.error("Cannot get bytes available", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + + return 0; + } + + /** + * Flushes all input and output buffers of the serial port. + * + *

    This method clears both the input and output buffers of the serial port, discarding any + * pending data. This is useful for ensuring a clean communication state, particularly after + * errors or before starting a new communication session. + * + *

    The flush operation includes clearing receive buffers, transmit buffers, and aborting any + * pending operations. + * + *

    The channel must be in a STARTED state and the port must be open for this operation to + * succeed. + * + * @throws ChannelException if the channel is not ready, port is not open, or flush operation + * fails + * @see SerialPort#PURGE_RXCLEAR + * @see SerialPort#PURGE_TXCLEAR + */ + public void flush() throws ChannelException { + if (this.portHandler == null || !this.portHandler.isOpened()) { + ChannelException.raiseChannelNotReady(this.getPortName()); + } + try { + int mask = + SerialPort.PURGE_RXCLEAR + | SerialPort.PURGE_RXABORT + | SerialPort.PURGE_TXCLEAR + | SerialPort.PURGE_TXABORT; + if (!this.portHandler.purgePort(mask)) { + this.logger.error( + "Channel " + + this.getName() + + " failed to purge port " + + this.getPortName() + + " (" + + SystemError.getMessage() + + ") "); + } + } catch (SerialPortException exception) { + this.logger.error("Cannot purge port", exception); + ChannelException.raiseInternalError(exception.getMessage()); + } + } + + /** + * Returns the port identifier from the channel configuration. + * + *

    The port ID is an alternative way to identify serial ports, typically used when the system + * provides unique identifiers for ports. This identifier takes precedence over the port name when + * both are specified. + * + * @return the port identifier, or null if not set in the configuration + * @see ChannelSerialPortConfiguration#getPortId() + * @see #getPortName() + */ + public String getPortId() { + return this.getChannelConfiguration().getPortId(); + } + + /** + * Returns the port name from the channel configuration. + * + *

    The port name is the system path or name used to identify the serial port. This is typically + * a device path like "/dev/ttyUSB0" on Linux or "COM1" on Windows. + * + * @return the port name, or null if not set in the configuration + * @see ChannelSerialPortConfiguration#getPortName() + * @see #getPortId() + */ + public String getPortName() { + return this.getChannelConfiguration().getPortName(); + } + + /** + * Returns the name of the currently opened serial port. + * + *

    This method returns the actual port name that is currently opened by the JSSC library. This + * may differ from the configured port name if symbolic links were resolved or if the port was + * dynamically assigned. + * + * @return the name of the opened port, or null if no port is currently open + * @see #getPortName() + * @see #getPortId() + */ + public String getPortNameOpened() { + return (this.portHandler != null) ? this.portHandler.getPortName() : null; + } + + /** + * Finds the port name associated with the current channel configuration. + * + *

    This method resolves the port name based on the configuration's port ID or port name + * settings. It uses the same resolution logic as the static {@link #findPortName(String, String)} + * method but operates on the current configuration. + * + * @return the resolved port name, or null if no matching port is found + * @see #findPortName(String, String) + * @see #getPortId() + * @see #getPortName() + */ + public String findPortName() { + return findPortName(this.getPortId(), this.getPortName()); + } + + /** + * Returns the current baud rate configured for the serial port. + * + *

    The baud rate determines the speed of serial communication in bits per second. + * + * @return the configured baud rate value + * @see ChannelSerialPortConfiguration#getBaudrate() + */ + public int getBaudrate() { + return this.getChannelConfiguration().getBaudrate().intValue(); + } + + /** + * Returns the number of data bits configured for serial communication. + * + *

    Data bits define the number of bits used to represent each character transmitted. Standard + * values are 7 (for ASCII) or 8 (for binary data). TIC protocol typically uses 7 data bits. + * + * @return the number of data bits (5, 6, 7, or 8) + * @see ChannelSerialPortConfiguration#getDataBits() + */ + public int getDataBits() { + return this.getChannelConfiguration().getDataBits().intValue(); + } + + /** + * Returns the number of stop bits configured for serial communication. + * + *

    Stop bits mark the end of each data frame. Standard values are 1.0, 1.5, or 2.0. Most serial + * communications use 1.0 stop bits. + * + * @return the number of stop bits (1.0, 1.5, or 2.0) + * @see ChannelSerialPortConfiguration#getStopBits() + */ + public float getStopBits() { + return this.getChannelConfiguration().getStopBits().floatValue(); + } + + /** + * Returns the parity setting configured for serial communication. + * + *

    Parity is used for error detection by adding an extra bit to each data frame. TIC protocol + * typically uses EVEN parity for reliable data transmission. + * + * @return the parity setting from the configuration + * @see Parity + * @see ChannelSerialPortConfiguration#getParity() + */ + public Parity getParity() { + return this.getChannelConfiguration().getParity(); + } + + /** + * Returns the timeout value for synchronous read operations. + * + *

    This timeout determines how long a synchronous read operation will wait for data before + * timing out. The value is specified in milliseconds. + * + * @return the timeout value in milliseconds for synchronous read operations + * @see ChannelSerialPortConfiguration#getSyncReadTimeout() + * @see #read(int, int) + */ + public int getSyncReadTimeout() { + return this.getChannelConfiguration().getSyncReadTimeout().intValue(); + } + + /** + * Determines if the channel has an event listener configured. + * + *

    This method indicates whether the channel is configured to use event-driven data reception + * (via SerialPortEventListener) or polling-based reception. + * + *

    The base implementation returns false, indicating polling mode. Subclasses can override this + * method to enable event-driven mode. + * + * @return true if event listener is configured, false for polling mode + * @see #addEventListener() + * @see #removeEventListener() + */ + protected boolean hasEventListener() { + return false; + } + + /** + * Adds a serial port event listener for asynchronous data reception. + * + *

    This method configures the serial port to generate events when data is received, control + * signals change, or other port events occur. It sets up the event mask to listen for RXCHAR + * (data received), CTS (clear to send), and DSR (data set ready) events. + * + *

    The channel instance is registered as the event listener, so {@link + * #serialEvent(SerialPortEvent)} will be called when events occur. + * + * @return true if the event listener was successfully added, false otherwise + * @see #removeEventListener() + * @see #serialEvent(SerialPortEvent) + * @see SerialPort#MASK_RXCHAR + */ + protected boolean addEventListener() { + if (this.portHandler == null) { + return false; + } + int mask = SerialPort.MASK_RXCHAR + SerialPort.MASK_CTS + SerialPort.MASK_DSR; + try { + if (!this.portHandler.setEventsMask(mask)) { + this.logger.error( + "Channel " + + this.getName() + + " failed to set event mask on port " + + this.getPortName() + + " (" + + SystemError.getMessage() + + ") "); + return false; + } + } catch (SerialPortException exception) { + this.logger.error( + "Channel " + + this.getName() + + " failed to set event mask on port " + + this.getPortName() + + " : " + + exception.getMessage()); + return false; + } + try { + this.portHandler.addEventListener(this); + } catch (SerialPortException e) { + this.logger.error( + "Channel " + this.getName() + " failed to add listener :" + e.getMessage(), e); + return false; + } + + return true; + } + + /** + * Removes the serial port event listener. + * + *

    This method unregisters the channel instance as an event listener from the serial port. + * After calling this method, the channel will no longer receive asynchronous events and will + * operate in polling mode. + * + *

    This method is typically called when stopping the channel or switching to polling-based data + * reception. + * + * @return true if the event listener was successfully removed, false otherwise + * @see #addEventListener() + * @see #hasEventListener() + */ + protected boolean removeEventListener() { + if (this.portHandler == null) { + return false; + } + try { + if (!this.portHandler.removeEventListener()) { + this.logger.error("Channel " + this.getName() + " failed to remove listener"); + return false; + } + } catch (SerialPortException e) { + this.logger.error( + "Channel " + this.getName() + " failed to remove listener :" + e.getMessage(), e); + return false; + } + + return true; + } + + /** + * Receives and processes data from the serial port. + * + *

    This method checks for available data in the serial port input buffer and reads it if + * present. The received data is then passed to registered listeners via the notification system. + * + *

    If no data is available, the method returns immediately without error. If a read error + * occurs, it notifies listeners with an appropriate error code. + * + *

    This method is called both from the periodic processing loop (polling mode) and from the + * serial event handler (event-driven mode). + * + * @see #read() + * @see #available() + * @see #notifyOnDataRead(byte[]) + * @see ChannelSerialPortErrorCode#READ_FAILED + */ + protected void receiveData() { + try { + if (this.available() > 0) { + byte[] buffer = this.read(); + this.notifyOnDataRead(buffer); + } + } catch (ChannelException exception) { + this.notifyOnErrorDetected( + ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), + exception.getMessage()); + } + } + + /** + * Checks if the port name has changed since the last check. + * + *

    This method compares the currently detected port name with the previously detected port name + * to determine if the port has changed. This typically occurs when a USB device is unplugged and + * plugged back in, causing the system to assign a new device path. + * + *

    The comparison is case-insensitive to handle variations in how the system reports port + * names. + * + * @return true if the port name has changed, false otherwise + * @see #onPortNameChanged() + */ + private boolean hasPortChanged() { + boolean portChanged = false; + + this.currentPortName = this.findPortName(); + + if (this.currentPortName != null + && !this.currentPortName.equalsIgnoreCase(this.previousPortName)) { + portChanged = true; + } + + return portChanged; + } + + /** + * Handles the event when the port name has changed. + * + *

    This method is called when a port name change is detected. It logs the change, sets the + * channel status to ERROR, forcibly closes the current port connection, notifies listeners of the + * error, and updates the previous port name for future comparisons. + * + *

    Port name changes typically occur when USB devices are reconnected and the system assigns a + * new device path (e.g., /dev/ttyUSB0 → /dev/ttyUSB1). + * + * @see #hasPortChanged() + * @see ChannelSerialPortErrorCode#PORT_NAME_HAS_CHANGED + */ + private void onPortNameChanged() { + String errorMessage = + "Port name has changed ( " + this.previousPortName + " --> " + this.currentPortName + " )"; + this.logger.error(errorMessage); + this.setStatus(ChannelStatus.ERROR); + this.closePortForced(); + this.notifyOnErrorDetected( + ChannelSerialPortErrorCode.PORT_NAME_HAS_CHANGED.getCode(ERROR_CODE_OFFSET), errorMessage); + this.previousPortName = this.currentPortName; + } + + /** + * Updates the port name to its real path by resolving symbolic links. + * + *

    This method is only executed on Linux systems to resolve symbolic links to their actual + * device paths. This is important for USB devices that may be represented by symbolic links in + * /dev/disk/by-id/ or similar locations. + * + *

    The method uses the system 'realpath' command to resolve the symbolic link and updates the + * channel configuration with the resolved path. + * + * @throws ChannelException if the realpath resolution fails or configuration update fails + * @see SystemUtils#IS_OS_LINUX + */ + private void updateRealPath() throws ChannelException { + if (SystemUtils.IS_OS_LINUX) { + String realPortName; + try { + Process p = Runtime.getRuntime().exec("realpath " + this.getPortName()); + BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream())); + realPortName = stdInput.readLine(); + this.getChannelConfiguration().setPortName(realPortName); + } catch (IOException | DataDictionaryException e) { + throw new ChannelException( + ChannelException.ERRCODE_INVALID_CONFIGURATION, + "Cannot resolve realpath (" + e.getMessage() + ")"); + } + } + } + + /** + * Returns the channel configuration cast to the specific serial port configuration type. + * + *

    This helper method provides type-safe access to the serial port specific configuration + * without requiring explicit casting throughout the code. + * + * @return the channel configuration as a ChannelSerialPortConfiguration instance + */ + private ChannelSerialPortConfiguration getChannelConfiguration() { + return (ChannelSerialPortConfiguration) this.configuration; + } + + /** + * Opens and configures the serial port connection. + * + *

    This method performs the complete process of opening a serial port connection: + * + *

      + *
    1. Checks if the port is already open to avoid duplicate operations + *
    2. Creates a new SerialPort handler if needed + *
    3. Opens the port using the resolved port name + *
    4. Configures the port with the specified communication parameters + *
    5. Adds event listener if configured for event-driven mode + *
    6. Sets the channel status to STARTED + *
    + * + *

    If any step fails, the method logs the error, handles specific error conditions (like + * PORT_BUSY), and ensures the channel enters an appropriate error state. + * + * @see #closePort() + * @see #findPortName() + * @see #hasEventListener() + * @see #addEventListener() + * @see ChannelSerialPortErrorCode#PORT_BUSY + */ + protected void openPort() { + /* 1. Check if serial port is already opened */ + if (this.portHandler != null && this.portHandler.isOpened()) { + return; + } + /* 2. Open serial port */ + String portNameFound = this.findPortName(); + try { + this.logger.info("Channel " + this.getName() + " opening port " + portNameFound); + if (this.portHandler == null) { + this.portHandler = new SerialPort(portNameFound); + } + if (!this.portHandler.openPort()) { + this.logger.error( + "Channel " + + this.getName() + + " failed to open port " + + this.getPortNameOpened() + + " (" + + SystemError.getMessage() + + ") "); + return; + } + this.logger.info("Channel " + this.getName() + " configuring port " + portNameFound); + if (!this.portHandler.setParams( + this.getBaudrate(), + this.getDataBits(), + stopBitsFromConfiguration(this.getStopBits()), + parityFromConfiguration(this.getParity()))) { + this.logger.error( + "Channel " + + this.getName() + + " failed to configure port " + + this.getPortNameOpened() + + " (" + + SystemError.getMessage() + + ") "); + return; + } + } catch (SerialPortException exception) { + String errorMessage = + "Channel " + + this.getName() + + " failed to open port " + + this.getPortNameOpened() + + " : " + + exception.getMessage(); + this.logger.error(errorMessage); + if (exception.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) { + this.notifyOnErrorDetected( + ChannelSerialPortErrorCode.PORT_BUSY.getCode(ERROR_CODE_OFFSET), errorMessage); + } + this.closePortForced(); + return; + } + /* 3. Add serial event listener */ + if (this.hasEventListener()) { + if (!this.addEventListener()) { + return; + } + } + this.setStatus(ChannelStatus.STARTED); + } + + /** + * Closes the serial port connection gracefully. + * + *

    This method performs the complete process of closing a serial port connection: + * + *

      + *
    1. Checks if the port is already closed to avoid duplicate operations + *
    2. Removes event listener if one was configured + *
    3. Closes the port and releases system resources + *
    4. Sets the channel status to STOPPED + *
    + * + *

    If any step fails, the method logs the error but continues with the remaining cleanup steps + * to ensure the channel is in a consistent state. + * + * @see #openPort() + * @see #closePortForced() + * @see #hasEventListener() + * @see #removeEventListener() + */ + protected void closePort() { + /* 1. Check if serial port is already closed */ + if (this.portHandler == null || !this.portHandler.isOpened()) { + return; + } + /* 2. Remove serial event listener */ + if (this.hasEventListener()) { + this.removeEventListener(); + } + /* 3. Close serial port */ + try { + this.logger.info("Channel " + this.getName() + " closing port " + this.getPortNameOpened()); + if (!this.portHandler.closePort()) { + this.logger.error( + "Channel " + + this.getName() + + " failed to close port " + + this.getPortNameOpened() + + " (" + + SystemError.getMessage() + + ") "); + return; + } + } catch (SerialPortException exception) { + this.logger.error( + "Channel " + + this.getName() + + " failed to close port " + + this.getPortNameOpened() + + " : " + + exception.getMessage()); + return; + } + this.setStatus(ChannelStatus.STOPPED); + } + + /** + * Forcefully closes the serial port and resets the port handler. + * + *

    This method performs an emergency closure of the serial port connection without the normal + * cleanup procedures. It attempts to close the port and then creates a new SerialPort handler + * instance for future use. + * + *

    This method is typically used in error conditions where the normal close procedure may fail + * or when the port needs to be forcibly reset. + * + *

    Unlike {@link #closePort()}, this method does not remove event listeners or update the + * channel status, making it suitable for error recovery scenarios. + * + * @see #closePort() + * @see #findPortName() + */ + protected void closePortForced() { + try { + this.portHandler.closePort(); + } catch (SerialPortException exception) { + this.logger.error( + "Channel " + + this.getName() + + " fail on close : close " + + this.getPortNameOpened() + + " failed due to " + + exception.getMessage()); + } + this.portHandler = new SerialPort(this.findPortName()); + } + + /** + * Checks if the configured port is currently available on the system. + * + *

    This method uses the current channel configuration to determine if the specified port is + * available. It delegates to the static {@link #isPortFound(String, String)} method with the + * current port ID and port name. + * + * @return true if the configured port is found and available, false otherwise + * @see #isPortFound(String, String) + * @see #getPortId() + * @see #getPortName() + */ + protected boolean isPortFound() { + return isPortFound(this.getPortId(), this.getPortName()); + } + + /** + * Checks if the currently opened port matches the expected port. + * + *

    This method compares the name of the currently opened port with the port name that should be + * opened according to the current configuration. This is useful for detecting situations where + * the port has changed or become unavailable. + * + * @return true if the opened port matches the expected port, false otherwise + * @see #getPortNameOpened() + * @see #findPortName() + */ + protected boolean isPortOpenedFound() { + String portNameOpened = this.getPortNameOpened(); + if (portNameOpened == null) { + return false; + } + String portNameFound = this.findPortName(); + + return portNameOpened.equals(portNameFound); + } + + /** + * Handles the event when the configured port is not found. + * + *

    This method is called when the system cannot locate the configured serial port. It logs the + * error, sets the channel status to ERROR, forcibly closes any existing connection, and notifies + * listeners of the error condition. + * + *

    This typically occurs when a USB device is unplugged or the device driver is not properly + * installed. + * + * @see #isPortFound() + * @see ChannelSerialPortErrorCode#PORT_NOT_FOUND + */ + protected void onPortNotFound() { + String errorMessage = + "Channel " + this.getName() + " : serial port " + this.findPortName() + " not found"; + this.logger.error(errorMessage); + this.setStatus(ChannelStatus.ERROR); + this.closePortForced(); + this.notifyOnErrorDetected( + ChannelSerialPortErrorCode.PORT_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); + } + + /** + * Handles the event when the currently opened port is no longer found. + * + *

    This method is called when the system can no longer locate the port that was previously + * opened by the channel. This indicates that the device has been physically disconnected or the + * system has lost access to it. + * + *

    It logs the error, sets the channel status to ERROR, forcibly closes the connection, and + * notifies listeners of the error condition. + * + * @see #isPortOpenedFound() + * @see ChannelSerialPortErrorCode#PORT_OPENED_NOT_FOUND + */ + protected void onPortOpenedNotFound() { + String errorMessage = + "Channel " + + this.getName() + + " : serial port " + + this.getPortNameOpened() + + " opened not found"; + this.logger.error(errorMessage); + this.setStatus(ChannelStatus.ERROR); + this.closePortForced(); + this.notifyOnErrorDetected( + ChannelSerialPortErrorCode.PORT_OPENED_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); + } } diff --git a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java index 2a53bdb..20adc5c 100644 --- a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java +++ b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -9,91 +9,86 @@ /** * Enumeration of error codes for serial port channel operations. - * + * *

    This enum defines specific error codes that can occur during serial port communication - * operations. Each error code is associated with a unique numeric identifier that can be used - * for error reporting, logging, and debugging purposes. - * + * operations. Each error code is associated with a unique numeric identifier that can be used for + * error reporting, logging, and debugging purposes. + * *

    The error codes are designed to be used with an offset mechanism, allowing different - * components or modules to have their own error code ranges while maintaining uniqueness - * across the entire system. - * + * components or modules to have their own error code ranges while maintaining uniqueness across the + * entire system. + * * @author Enedis Smarties team */ -public enum ChannelSerialPortErrorCode -{ - /** - * Error code indicating that the specified serial port was not found on the system. - * This typically occurs when the port identifier or name does not correspond to any - * available serial port. - */ - PORT_NOT_FOUND(1), +public enum ChannelSerialPortErrorCode { + /** + * Error code indicating that the specified serial port was not found on the system. This + * typically occurs when the port identifier or name does not correspond to any available serial + * port. + */ + PORT_NOT_FOUND(1), - /** - * Error code indicating that a previously opened serial port is no longer available. - * This can happen when the port is disconnected or becomes unavailable after being opened. - */ - PORT_OPENED_NOT_FOUND(2), + /** + * Error code indicating that a previously opened serial port is no longer available. This can + * happen when the port is disconnected or becomes unavailable after being opened. + */ + PORT_OPENED_NOT_FOUND(2), - /** - * Error code indicating that the serial port name has changed during operation. - * This typically occurs when the port is reconnected and gets assigned a different name. - */ - PORT_NAME_HAS_CHANGED(3), + /** + * Error code indicating that the serial port name has changed during operation. This typically + * occurs when the port is reconnected and gets assigned a different name. + */ + PORT_NAME_HAS_CHANGED(3), - /** - * Error code indicating that the serial port is currently busy and cannot be accessed. - * This typically occurs when another application or process is already using the port. - */ - PORT_BUSY(4), + /** + * Error code indicating that the serial port is currently busy and cannot be accessed. This + * typically occurs when another application or process is already using the port. + */ + PORT_BUSY(4), - /** - * Error code indicating that a read operation on the serial port has failed. - * This can be due to various reasons such as hardware issues, communication errors, - * or port configuration problems. - */ - READ_FAILED(5), + /** + * Error code indicating that a read operation on the serial port has failed. This can be due to + * various reasons such as hardware issues, communication errors, or port configuration problems. + */ + READ_FAILED(5), - /** - * Error code indicating that a read operation on the serial port has timed out. - * This occurs when no data is received within the specified timeout period. - */ - READ_TIMEOUT(6); + /** + * Error code indicating that a read operation on the serial port has timed out. This occurs when + * no data is received within the specified timeout period. + */ + READ_TIMEOUT(6); - private int code; + private int code; - /** - * Constructs a new error code with the specified numeric identifier. - * - * @param code the numeric identifier for this error code - */ - private ChannelSerialPortErrorCode(int code) - { - this.code = code; - } + /** + * Constructs a new error code with the specified numeric identifier. + * + * @param code the numeric identifier for this error code + */ + private ChannelSerialPortErrorCode(int code) { + this.code = code; + } - /** - * Returns the base error code without any offset applied. - * - * @return the base error code value - */ - public int getCode() - { - return this.code; - } + /** + * Returns the base error code without any offset applied. + * + * @return the base error code value + */ + public int getCode() { + return this.code; + } - /** - * Returns the error code with the specified offset added. - * - *

    This method allows different components or modules to have their own error code - * ranges by applying an offset to the base error code. This helps maintain uniqueness - * across the entire system while allowing for organized error code management. - * - * @param offset the offset to add to the base error code - * @return the error code with the specified offset applied - */ - public int getCode(int offset) - { - return offset + this.code; - } + /** + * Returns the error code with the specified offset added. + * + *

    This method allows different components or modules to have their own error code ranges by + * applying an offset to the base error code. This helps maintain uniqueness across the entire + * system while allowing for organized error code management. + * + * @param offset the offset to add to the base error code + * @return the error code with the specified offset applied + */ + public int getCode(int offset) { + return offset + this.code; + } } diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java index 4dda3b3..c4d4a28 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,8 +7,6 @@ package enedis.lab.protocol.tic.codec; -import java.util.List; - import enedis.lab.codec.Codec; import enedis.lab.codec.CodecException; import enedis.lab.protocol.tic.frame.TICFrame; @@ -16,147 +14,127 @@ import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; import enedis.lab.types.BytesArray; +import java.util.List; /** * Codec for TIC Historic frames. - * - * This codec handles the encoding and decoding of TIC Historic frames, which are used - * in the French electricity meter communication protocol. Historic frames contain - * historical consumption data and are part of the TIC protocol specification. - * - * The codec processes byte arrays containing TIC Historic frame data and converts them - * to TICFrameHistoric objects, and vice versa. It validates frame structure, handles - * data set parsing, and ensures proper checksum validation. - * + * + *

    This codec handles the encoding and decoding of TIC Historic frames, which are used in the + * French electricity meter communication protocol. Historic frames contain historical consumption + * data and are part of the TIC protocol specification. + * + *

    The codec processes byte arrays containing TIC Historic frame data and converts them to + * TICFrameHistoric objects, and vice versa. It validates frame structure, handles data set parsing, + * and ensures proper checksum validation. + * * @author Enedis Smarties team */ -public class CodecTICFrameHistoric implements Codec -{ - - /** - * Separator character used in TIC Historic frames. - * This is the space character (0x20) used to separate different parts - * of the frame structure in the TIC protocol. - */ - public static final byte SEPARATOR = 0x20; // SP - - - /** - * Decodes a byte array containing TIC Historic frame data into a TICFrameHistoric object. - * - * This method validates the frame structure by checking for proper beginning and end patterns, - * extracts individual data sets from the frame, and decodes each data set using the - * CodecTICFrameHistoricDataSet codec. If any data set fails to decode, the error is - * collected and thrown as a CodecException. - * - * @param bytesBuffer the byte array containing the TIC Historic frame data - * @return a TICFrameHistoric object containing the decoded frame data - * @throws CodecException if the frame structure is invalid or if any data set fails to decode - */ - @Override - public TICFrameHistoric decode(byte[] bytesBuffer) throws CodecException - { - String errorMessage = ""; - BytesArray rawDataSet = null; - TICFrameHistoric ticFrame = null; - CodecTICFrameHistoricDataSet codecTICFrameHistoricDataSet = new CodecTICFrameHistoricDataSet(); - - BytesArray bytesArray = new BytesArray(bytesBuffer); - - if ((true == bytesArray.startsWith(TICFrame.BEGINNING_PATTERN)) && (true == bytesArray.endsWith(TICFrame.END_PATTERN)) && (bytesArray.contains(TICFrame.EOT) == false)) - { - bytesArray.remove(0); - bytesArray.remove(bytesArray.size() - 1); - - ticFrame = new TICFrameHistoric(); - - List datasetList = bytesArray.slice(TICFrameDataSet.BEGINNING_PATTERN, TICFrameDataSet.END_PATTERN, BytesArray.CONTIGUOUS); - - if (datasetList.isEmpty() == false) - { - for (int i = 0; i < datasetList.size(); i++) - { - TICFrameHistoricDataSet dataSet = null; - rawDataSet = datasetList.get(i); - byte[] rawDataSetByte = rawDataSet.getBytes(); - - try - { - dataSet = codecTICFrameHistoricDataSet.decode(rawDataSetByte); - ticFrame.addDataSet(dataSet); - } - catch (CodecException exception) - { - errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; - } - } - } - } - - if (errorMessage.isEmpty()) - { - return ticFrame; - } - else - { - throw new CodecException(errorMessage, ticFrame); - } - - } - - /** - * Encodes a TICFrameHistoric object into a byte array representing a TIC Historic frame. - * - * This method takes a TICFrameHistoric object and converts it into the byte representation - * of a TIC Historic frame. It processes each data set in the frame, encodes them using the - * CodecTICFrameHistoricDataSet codec, and wraps the result with proper frame delimiters. - * - * @param ticFrameHistoric the TICFrameHistoric object to encode - * @return a byte array containing the encoded TIC Historic frame - * @throws CodecException if the frame is null, empty, or if any data set fails to encode - */ - @Override - public byte[] encode(TICFrameHistoric ticFrameHistoric) throws CodecException - { - String errorMessage = ""; - CodecTICFrameHistoricDataSet codec = new CodecTICFrameHistoricDataSet(); - BytesArray dataSet = new BytesArray(); - - List ticFrameHistoricList = ticFrameHistoric.getDataSetList(); - - if (ticFrameHistoric != null && !ticFrameHistoricList.isEmpty()) - { - List groups = ticFrameHistoric.getDataSetList(); - dataSet.add(TICFrame.BEGINNING_PATTERN); - for (int i = 0; i < groups.size(); i++) - { - try - { - byte[] buff = codec.encode((TICFrameHistoricDataSet) groups.get(i)); - dataSet.addAll(buff); - } - catch (CodecException e) - { - errorMessage += e.getMessage() + " : " + new String(ticFrameHistoric.getBytes()) + "\n"; - } - } - dataSet.add(TICFrame.END_PATTERN); - } - - else - { - return null; - } - - if (errorMessage.isEmpty()) - { - return dataSet.getBytes(); - } - else - { - throw new CodecException(errorMessage, dataSet.getBytes()); - } - } - - -} \ No newline at end of file +public class CodecTICFrameHistoric implements Codec { + + /** + * Separator character used in TIC Historic frames. This is the space character (0x20) used to + * separate different parts of the frame structure in the TIC protocol. + */ + public static final byte SEPARATOR = 0x20; // SP + + /** + * Decodes a byte array containing TIC Historic frame data into a TICFrameHistoric object. + * + *

    This method validates the frame structure by checking for proper beginning and end patterns, + * extracts individual data sets from the frame, and decodes each data set using the + * CodecTICFrameHistoricDataSet codec. If any data set fails to decode, the error is collected and + * thrown as a CodecException. + * + * @param bytesBuffer the byte array containing the TIC Historic frame data + * @return a TICFrameHistoric object containing the decoded frame data + * @throws CodecException if the frame structure is invalid or if any data set fails to decode + */ + @Override + public TICFrameHistoric decode(byte[] bytesBuffer) throws CodecException { + String errorMessage = ""; + BytesArray rawDataSet = null; + TICFrameHistoric ticFrame = null; + CodecTICFrameHistoricDataSet codecTICFrameHistoricDataSet = new CodecTICFrameHistoricDataSet(); + + BytesArray bytesArray = new BytesArray(bytesBuffer); + + if ((true == bytesArray.startsWith(TICFrame.BEGINNING_PATTERN)) + && (true == bytesArray.endsWith(TICFrame.END_PATTERN)) + && (bytesArray.contains(TICFrame.EOT) == false)) { + bytesArray.remove(0); + bytesArray.remove(bytesArray.size() - 1); + + ticFrame = new TICFrameHistoric(); + + List datasetList = + bytesArray.slice( + TICFrameDataSet.BEGINNING_PATTERN, + TICFrameDataSet.END_PATTERN, + BytesArray.CONTIGUOUS); + + if (datasetList.isEmpty() == false) { + for (int i = 0; i < datasetList.size(); i++) { + TICFrameHistoricDataSet dataSet = null; + rawDataSet = datasetList.get(i); + byte[] rawDataSetByte = rawDataSet.getBytes(); + + try { + dataSet = codecTICFrameHistoricDataSet.decode(rawDataSetByte); + ticFrame.addDataSet(dataSet); + } catch (CodecException exception) { + errorMessage += + exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; + } + } + } + } + + if (errorMessage.isEmpty()) { + return ticFrame; + } else { + throw new CodecException(errorMessage, ticFrame); + } + } + + /** + * Encodes a TICFrameHistoric object into a byte array representing a TIC Historic frame. + * + *

    This method takes a TICFrameHistoric object and converts it into the byte representation of + * a TIC Historic frame. It processes each data set in the frame, encodes them using the + * CodecTICFrameHistoricDataSet codec, and wraps the result with proper frame delimiters. + * + * @param ticFrameHistoric the TICFrameHistoric object to encode + * @return a byte array containing the encoded TIC Historic frame + * @throws CodecException if the frame is null, empty, or if any data set fails to encode + */ + @Override + public byte[] encode(TICFrameHistoric ticFrameHistoric) throws CodecException { + String errorMessage = ""; + CodecTICFrameHistoricDataSet codec = new CodecTICFrameHistoricDataSet(); + BytesArray dataSet = new BytesArray(); + + List ticFrameHistoricList = ticFrameHistoric.getDataSetList(); + + if (ticFrameHistoric != null && !ticFrameHistoricList.isEmpty()) { + List groups = ticFrameHistoric.getDataSetList(); + dataSet.add(TICFrame.BEGINNING_PATTERN); + for (int i = 0; i < groups.size(); i++) { + try { + byte[] buff = codec.encode((TICFrameHistoricDataSet) groups.get(i)); + dataSet.addAll(buff); + } catch (CodecException e) { + errorMessage += e.getMessage() + " : " + new String(ticFrameHistoric.getBytes()) + "\n"; + } + } + dataSet.add(TICFrame.END_PATTERN); + } else { + return null; + } + + if (errorMessage.isEmpty()) { + return dataSet.getBytes(); + } else { + throw new CodecException(errorMessage, dataSet.getBytes()); + } + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java index fa78822..069efb3 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java @@ -7,145 +7,120 @@ package enedis.lab.protocol.tic.codec; -import java.util.List; - import enedis.lab.codec.Codec; import enedis.lab.codec.CodecException; import enedis.lab.protocol.tic.frame.TICFrameDataSet; import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; import enedis.lab.types.BytesArray; +import java.util.List; -/** - * Codec TIC Frame Historic Data Set - * - */ -public class CodecTICFrameHistoricDataSet implements Codec -{ - - @Override - public byte[] encode(TICFrameHistoricDataSet ticFrameHistoricDataSet) throws CodecException - { - BytesArray dataSet = new BytesArray(); - - if ((ticFrameHistoricDataSet.getLabel() != null) && (ticFrameHistoricDataSet.getData() != null)) - { - dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); - dataSet.addAll(ticFrameHistoricDataSet.getLabel().getBytes()); - - dataSet.add(TICFrameHistoricDataSet.SEPARATOR); - dataSet.addAll(ticFrameHistoricDataSet.getData().getBytes()); - - dataSet.add(TICFrameHistoricDataSet.SEPARATOR); - - if (!ticFrameHistoricDataSet.isValid()) - { - throw new CodecException("Invalid Checksum value"); - } - dataSet.add(ticFrameHistoricDataSet.getChecksum()); - dataSet.add(TICFrameDataSet.END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public TICFrameHistoricDataSet decode(byte[] bytes) throws CodecException - { - BytesArray bytesArray = new BytesArray(bytes); - TICFrameHistoricDataSet dataSet = null; - - if (this.isStartStopDelimiterPresent(bytesArray)) - { - this.removeStartStopDelimiter(bytesArray); - - } - - List parts = this.splitFrame(bytesArray); - - // Structure "LABEL/DATA/CHECKSUM" : - if (this.isStructureLabelDataChecksum(parts)) - { - - if (this.isChecksumInOneByte(parts.get(2))) - - { - dataSet = new TICFrameHistoricDataSet(); - dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); - - } - else - { - throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); - } - } - else - { - throw new CodecException("Invalid format of TICFrameHistoricDataSet"); - } - - if (dataSet.isValid() == false) - { - throw new CodecException("Invalid Checksum value - Historic"); - } - - return dataSet; - } - - private boolean isStartStopDelimiterPresent(BytesArray bytes) - { - return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); - } - - private void removeStartStopDelimiter(BytesArray bytes) - - { - bytes.remove(0); - bytes.remove(bytes.size() - 1); - } - - private TICFrameHistoricDataSet createDataSetLabelDataChecksum(List parts, TICFrameHistoricDataSet dataSet) - { - - dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(2).get(0)); - return dataSet; - } - - private boolean isChecksumInOneByte(BytesArray part) - { - return part.size() == 1; - } - - private boolean isStructureLabelDataChecksum(List parts) - { - return parts.size() == 3; - } - - private List splitFrame(BytesArray bytesArray) throws CodecException - { - List parts; - - if (bytesArray.size() < 5) - { - throw new CodecException("Not enough bytes in TICFrameHistoricDataSet"); - } - if (bytesArray.get(bytesArray.size() - 1) == TICFrameHistoricDataSet.SEPARATOR) - { - BytesArray subList = bytesArray.subList(0, bytesArray.size() - 2); - parts = subList.split(TICFrameHistoricDataSet.SEPARATOR); - if (parts.size() < 1) - { - throw new CodecException("Invalid format of TICFrameHistoricDataSet"); - } - BytesArray checksum = parts.get(parts.size() - 1); - checksum.addAll(new byte[] { TICFrameHistoricDataSet.SEPARATOR }); - } - else - { - parts = bytesArray.split(TICFrameHistoricDataSet.SEPARATOR); - } - - return parts; - } - +/** Codec TIC Frame Historic Data Set */ +public class CodecTICFrameHistoricDataSet implements Codec { + + @Override + public byte[] encode(TICFrameHistoricDataSet ticFrameHistoricDataSet) throws CodecException { + BytesArray dataSet = new BytesArray(); + + if ((ticFrameHistoricDataSet.getLabel() != null) + && (ticFrameHistoricDataSet.getData() != null)) { + dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); + dataSet.addAll(ticFrameHistoricDataSet.getLabel().getBytes()); + + dataSet.add(TICFrameHistoricDataSet.SEPARATOR); + dataSet.addAll(ticFrameHistoricDataSet.getData().getBytes()); + + dataSet.add(TICFrameHistoricDataSet.SEPARATOR); + + if (!ticFrameHistoricDataSet.isValid()) { + throw new CodecException("Invalid Checksum value"); + } + dataSet.add(ticFrameHistoricDataSet.getChecksum()); + dataSet.add(TICFrameDataSet.END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public TICFrameHistoricDataSet decode(byte[] bytes) throws CodecException { + BytesArray bytesArray = new BytesArray(bytes); + TICFrameHistoricDataSet dataSet = null; + + if (this.isStartStopDelimiterPresent(bytesArray)) { + this.removeStartStopDelimiter(bytesArray); + } + + List parts = this.splitFrame(bytesArray); + + // Structure "LABEL/DATA/CHECKSUM" : + if (this.isStructureLabelDataChecksum(parts)) { + + if (this.isChecksumInOneByte(parts.get(2))) { + + dataSet = new TICFrameHistoricDataSet(); + dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); + + } else { + throw new CodecException( + "Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + + "The Label part must not exceed 8 bytes "); + } + } else { + throw new CodecException("Invalid format of TICFrameHistoricDataSet"); + } + + if (dataSet.isValid() == false) { + throw new CodecException("Invalid Checksum value - Historic"); + } + + return dataSet; + } + + private boolean isStartStopDelimiterPresent(BytesArray bytes) { + return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) + && bytes.endsWith(TICFrameDataSet.END_PATTERN); + } + + private void removeStartStopDelimiter(BytesArray bytes) { + + bytes.remove(0); + bytes.remove(bytes.size() - 1); + } + + private TICFrameHistoricDataSet createDataSetLabelDataChecksum( + List parts, TICFrameHistoricDataSet dataSet) { + + dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); + dataSet.setChecksum(parts.get(2).get(0)); + return dataSet; + } + + private boolean isChecksumInOneByte(BytesArray part) { + return part.size() == 1; + } + + private boolean isStructureLabelDataChecksum(List parts) { + return parts.size() == 3; + } + + private List splitFrame(BytesArray bytesArray) throws CodecException { + List parts; + + if (bytesArray.size() < 5) { + throw new CodecException("Not enough bytes in TICFrameHistoricDataSet"); + } + if (bytesArray.get(bytesArray.size() - 1) == TICFrameHistoricDataSet.SEPARATOR) { + BytesArray subList = bytesArray.subList(0, bytesArray.size() - 2); + parts = subList.split(TICFrameHistoricDataSet.SEPARATOR); + if (parts.size() < 1) { + throw new CodecException("Invalid format of TICFrameHistoricDataSet"); + } + BytesArray checksum = parts.get(parts.size() - 1); + checksum.addAll(new byte[] {TICFrameHistoricDataSet.SEPARATOR}); + } else { + parts = bytesArray.split(TICFrameHistoricDataSet.SEPARATOR); + } + + return parts; + } } diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java index 8b9d987..c07fd08 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,8 +7,6 @@ package enedis.lab.protocol.tic.codec; -import java.util.List; - import enedis.lab.codec.Codec; import enedis.lab.codec.CodecException; import enedis.lab.protocol.tic.frame.TICFrame; @@ -16,92 +14,82 @@ import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; import enedis.lab.types.BytesArray; +import java.util.List; -/** - * Codec TIC Frame Standard - * - */ -public class CodecTICFrameStandard implements Codec -{ - @Override - public TICFrameStandard decode(byte[] bytes) throws CodecException - { - String errorMessage = ""; - BytesArray rawDataSet = null; - TICFrameStandard ticFrame = null; - CodecTICFrameStandardDataSet codecTICFrameStandardDataSet = new CodecTICFrameStandardDataSet(); +/** Codec TIC Frame Standard */ +public class CodecTICFrameStandard implements Codec { + @Override + public TICFrameStandard decode(byte[] bytes) throws CodecException { + String errorMessage = ""; + BytesArray rawDataSet = null; + TICFrameStandard ticFrame = null; + CodecTICFrameStandardDataSet codecTICFrameStandardDataSet = new CodecTICFrameStandardDataSet(); - BytesArray bytesArray = new BytesArray(bytes); + BytesArray bytesArray = new BytesArray(bytes); - // Extract the body of the frame - // NB: the presence of an EOT character makes the content of a frame invalid - if ((bytesArray.startsWith(TICFrame.BEGINNING_PATTERN) == true) && (bytesArray.endsWith(TICFrame.END_PATTERN) == true) && (bytesArray.contains(TICFrame.EOT) == false)) - { - bytesArray.remove(0); - bytesArray.remove(bytesArray.size() - 1); + // Extract the body of the frame + // NB: the presence of an EOT character makes the content of a frame invalid + if ((bytesArray.startsWith(TICFrame.BEGINNING_PATTERN) == true) + && (bytesArray.endsWith(TICFrame.END_PATTERN) == true) + && (bytesArray.contains(TICFrame.EOT) == false)) { + bytesArray.remove(0); + bytesArray.remove(bytesArray.size() - 1); - ticFrame = new TICFrameStandard(); + ticFrame = new TICFrameStandard(); - // Isolate each memory area supposed to correspond to an Information Group - // (DataSet) - List datasetList = bytesArray.slice(TICFrameDataSet.BEGINNING_PATTERN, TICFrameDataSet.END_PATTERN, BytesArray.CONTIGUOUS); + // Isolate each memory area supposed to correspond to an Information Group + // (DataSet) + List datasetList = + bytesArray.slice( + TICFrameDataSet.BEGINNING_PATTERN, + TICFrameDataSet.END_PATTERN, + BytesArray.CONTIGUOUS); - // Analyze each memory area to extract the controls of the Group of information - // If the format of a zone is invalid, then the browse is interrupted and the whole frame is invalid - // (null) - if (datasetList.isEmpty() == false) - { - for (int i = 0; i < datasetList.size(); i++) - { - TICFrameStandardDataSet dataSet = null; - rawDataSet = datasetList.get(i); - byte[] rawDataSetByte = rawDataSet.getBytes(); - try - { - dataSet = codecTICFrameStandardDataSet.decode(rawDataSetByte); - ticFrame.addDataSet(dataSet); - } - catch (CodecException exception) - { - errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; - } - } - } - } + // Analyze each memory area to extract the controls of the Group of information + // If the format of a zone is invalid, then the browse is interrupted and the whole frame is + // invalid + // (null) + if (datasetList.isEmpty() == false) { + for (int i = 0; i < datasetList.size(); i++) { + TICFrameStandardDataSet dataSet = null; + rawDataSet = datasetList.get(i); + byte[] rawDataSetByte = rawDataSet.getBytes(); + try { + dataSet = codecTICFrameStandardDataSet.decode(rawDataSetByte); + ticFrame.addDataSet(dataSet); + } catch (CodecException exception) { + errorMessage += + exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; + } + } + } + } - if (errorMessage.isEmpty()) - { - return ticFrame; - } - else - { - throw new CodecException(errorMessage, ticFrame); - } - } + if (errorMessage.isEmpty()) { + return ticFrame; + } else { + throw new CodecException(errorMessage, ticFrame); + } + } - @Override - public byte[] encode(TICFrameStandard ticFrameStandard) throws CodecException - { - CodecTICFrameStandardDataSet codec = new CodecTICFrameStandardDataSet(); - BytesArray dataSet = new BytesArray(); + @Override + public byte[] encode(TICFrameStandard ticFrameStandard) throws CodecException { + CodecTICFrameStandardDataSet codec = new CodecTICFrameStandardDataSet(); + BytesArray dataSet = new BytesArray(); - List ticFrameStandardList = ticFrameStandard.getDataSetList(); + List ticFrameStandardList = ticFrameStandard.getDataSetList(); - if (ticFrameStandard != null && !ticFrameStandardList.isEmpty()) - { - List groups = ticFrameStandard.getDataSetList(); - dataSet.add(TICFrame.BEGINNING_PATTERN); - for (int i = 0; i < groups.size(); i++) - { - byte[] buff = codec.encode((TICFrameStandardDataSet) groups.get(i)); - dataSet.addAll(buff); - } - dataSet.add(TICFrame.END_PATTERN); - return dataSet.getBytes(); - } - else - { - return null; - } - } + if (ticFrameStandard != null && !ticFrameStandardList.isEmpty()) { + List groups = ticFrameStandard.getDataSetList(); + dataSet.add(TICFrame.BEGINNING_PATTERN); + for (int i = 0; i < groups.size(); i++) { + byte[] buff = codec.encode((TICFrameStandardDataSet) groups.get(i)); + dataSet.addAll(buff); + } + dataSet.add(TICFrame.END_PATTERN); + return dataSet.getBytes(); + } else { + return null; + } + } } diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java index dc06499..05f0b30 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,142 +7,120 @@ package enedis.lab.protocol.tic.codec; -import java.util.List; - import enedis.lab.codec.Codec; import enedis.lab.codec.CodecException; import enedis.lab.protocol.tic.frame.TICFrameDataSet; import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; import enedis.lab.types.BytesArray; +import java.util.List; -/** - * Codec TIC Frame Standard Data Set - * - */ -public class CodecTICFrameStandardDataSet implements Codec -{ - @Override - public byte[] encode(TICFrameStandardDataSet ticFrameStandardDataSet) throws CodecException - { - BytesArray dataSet = new BytesArray(); - - if ((null != ticFrameStandardDataSet.getLabel()) && (null != ticFrameStandardDataSet.getData())) - { - dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); - dataSet.addAll(ticFrameStandardDataSet.getLabel().getBytes()); - - if (ticFrameStandardDataSet.checkDateTime() == true) - { - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - dataSet.addAll(ticFrameStandardDataSet.getDateTime().getBytes()); - } - - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - dataSet.addAll(ticFrameStandardDataSet.getData().getBytes()); - - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - if (!ticFrameStandardDataSet.isValid()) - { - throw new CodecException("Invalid Checksum value"); - } - - dataSet.add(ticFrameStandardDataSet.getChecksum()); - dataSet.add(TICFrameDataSet.END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public TICFrameStandardDataSet decode(byte[] bytes) throws CodecException - { - BytesArray bytesArray = new BytesArray(bytes); - TICFrameStandardDataSet dataSet = null; - - if (this.isStartStopDelimiterPresent(bytesArray)) - { - this.removeStartStopDelimiter(bytesArray); - } - - List parts = bytesArray.split(TICFrameStandardDataSet.SEPARATOR); - - // Structure "LABEL/DATA/CHECKSUM" : - if (this.isStructureLabelDataChecksum(parts)) - { - if (this.isChecksumInOneByte(parts.get(2))) - { - dataSet = new TICFrameStandardDataSet(); - dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); - } - else - { - throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); - } - } - // Structure "LABEL/DATETIME/DATA/CHECKSUM" : - else if (this.isStructureLabelDateTimeDataChecksum(parts)) - { - if (this.isChecksumInOneByte(parts.get(3))) - { - dataSet = new TICFrameStandardDataSet(); - dataSet = this.createDataSetLabelDatetimeDataChecksum(parts, dataSet); - } - else - { - throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); - } - } - else - { - throw new CodecException("Invalid bytes for decode - Standard"); - } - - if (dataSet.isValid() == false) - { - throw new CodecException("Invalid Checksum value"); - } - - return dataSet; - } - - private boolean isStructureLabelDateTimeDataChecksum(List parts) - { - return parts.size() == 4; - } - - private boolean isStructureLabelDataChecksum(List parts) - { - return parts.size() == 3; - } - - private boolean isChecksumInOneByte(BytesArray part) - { - return part.size() == 1; - } - - private void removeStartStopDelimiter(BytesArray bytes) - { - bytes.remove(0); - bytes.remove(bytes.size() - 1); - } - - private boolean isStartStopDelimiterPresent(BytesArray bytes) - { - return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); - } - - private TICFrameStandardDataSet createDataSetLabelDataChecksum(List parts, TICFrameStandardDataSet dataSet) - { - dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(2).get(0)); - return dataSet; - } - - private TICFrameStandardDataSet createDataSetLabelDatetimeDataChecksum(List parts, TICFrameStandardDataSet dataSet) - { - dataSet.setup(parts.get(0).getBytes(), parts.get(2).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(3).get(0)); - return dataSet; - } - -} \ No newline at end of file +/** Codec TIC Frame Standard Data Set */ +public class CodecTICFrameStandardDataSet implements Codec { + @Override + public byte[] encode(TICFrameStandardDataSet ticFrameStandardDataSet) throws CodecException { + BytesArray dataSet = new BytesArray(); + + if ((null != ticFrameStandardDataSet.getLabel()) + && (null != ticFrameStandardDataSet.getData())) { + dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); + dataSet.addAll(ticFrameStandardDataSet.getLabel().getBytes()); + + if (ticFrameStandardDataSet.checkDateTime() == true) { + dataSet.add(TICFrameStandardDataSet.SEPARATOR); + dataSet.addAll(ticFrameStandardDataSet.getDateTime().getBytes()); + } + + dataSet.add(TICFrameStandardDataSet.SEPARATOR); + dataSet.addAll(ticFrameStandardDataSet.getData().getBytes()); + + dataSet.add(TICFrameStandardDataSet.SEPARATOR); + if (!ticFrameStandardDataSet.isValid()) { + throw new CodecException("Invalid Checksum value"); + } + + dataSet.add(ticFrameStandardDataSet.getChecksum()); + dataSet.add(TICFrameDataSet.END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public TICFrameStandardDataSet decode(byte[] bytes) throws CodecException { + BytesArray bytesArray = new BytesArray(bytes); + TICFrameStandardDataSet dataSet = null; + + if (this.isStartStopDelimiterPresent(bytesArray)) { + this.removeStartStopDelimiter(bytesArray); + } + + List parts = bytesArray.split(TICFrameStandardDataSet.SEPARATOR); + + // Structure "LABEL/DATA/CHECKSUM" : + if (this.isStructureLabelDataChecksum(parts)) { + if (this.isChecksumInOneByte(parts.get(2))) { + dataSet = new TICFrameStandardDataSet(); + dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); + } else { + throw new CodecException( + "Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + + "The Label part must not exceed 8 bytes "); + } + } + // Structure "LABEL/DATETIME/DATA/CHECKSUM" : + else if (this.isStructureLabelDateTimeDataChecksum(parts)) { + if (this.isChecksumInOneByte(parts.get(3))) { + dataSet = new TICFrameStandardDataSet(); + dataSet = this.createDataSetLabelDatetimeDataChecksum(parts, dataSet); + } else { + throw new CodecException( + "Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + + "The Label part must not exceed 8 bytes "); + } + } else { + throw new CodecException("Invalid bytes for decode - Standard"); + } + + if (dataSet.isValid() == false) { + throw new CodecException("Invalid Checksum value"); + } + + return dataSet; + } + + private boolean isStructureLabelDateTimeDataChecksum(List parts) { + return parts.size() == 4; + } + + private boolean isStructureLabelDataChecksum(List parts) { + return parts.size() == 3; + } + + private boolean isChecksumInOneByte(BytesArray part) { + return part.size() == 1; + } + + private void removeStartStopDelimiter(BytesArray bytes) { + bytes.remove(0); + bytes.remove(bytes.size() - 1); + } + + private boolean isStartStopDelimiterPresent(BytesArray bytes) { + return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) + && bytes.endsWith(TICFrameDataSet.END_PATTERN); + } + + private TICFrameStandardDataSet createDataSetLabelDataChecksum( + List parts, TICFrameStandardDataSet dataSet) { + dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); + dataSet.setChecksum(parts.get(2).get(0)); + return dataSet; + } + + private TICFrameStandardDataSet createDataSetLabelDatetimeDataChecksum( + List parts, TICFrameStandardDataSet dataSet) { + dataSet.setup(parts.get(0).getBytes(), parts.get(2).getBytes(), parts.get(1).getBytes()); + dataSet.setChecksum(parts.get(3).get(0)); + return dataSet; + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java index ed28431..543e108 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -16,225 +16,183 @@ import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; import enedis.lab.types.BytesArray; -/** - * Codec TIC - */ -public class TICCodec implements Codec -{ - private static final TICMode DEFAULT_MODE = TICMode.STANDARD; - - private BytesArray Buffer; - private TICMode mode = TICMode.UNKNOWN; - private TICMode currentMode = TICMode.UNKNOWN; - - /** - * Default constructor - */ - public TICCodec() - { - this.mode = TICMode.UNKNOWN; - this.currentMode = TICMode.UNKNOWN; - this.Buffer = new BytesArray(); - } - - @Override - public TICFrame decode(byte[] newData) throws CodecException - { - CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); - CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); - TICFrameStandard ticFrameStandard = null; - TICFrameHistoric ticFrameHistoric = null; - - TICFrame ticFrame = null; - TICMode currentTICMode = null; - - try - { - switch (this.currentMode) - { - case STANDARD: - { - ticFrameStandard = codecStandard.decode(newData); - ticFrame = ticFrameStandard; - break; - } - case HISTORIC: - { - ticFrameHistoric = codecHistoric.decode(newData); - ticFrame = ticFrameHistoric; - break; - } - - case AUTO: - { - try - { - currentTICMode = TICMode.findModeFromFrameBuffer(newData); - } - catch (TICException exception) - { - throw new CodecException("can't determinated TIC Mode"); - } - - if (currentTICMode == TICMode.STANDARD) - { - ticFrameStandard = codecStandard.decode(newData); - ticFrame = ticFrameStandard; - break; - } - else if (currentTICMode == TICMode.HISTORIC) - { - ticFrameHistoric = codecHistoric.decode(newData); - ticFrame = ticFrameHistoric; - } - else - { - throw new CodecException("can't decode TIC, unable to find TIC Modem"); - } - - } - - default: - { - /**/ - } - } - } - catch (CodecException exception) - { - throw new CodecException(exception.getMessage(), exception.getData()); - } - return ticFrame; - } - - @Override - public byte[] encode(TICFrame ticFrame) throws CodecException - { - CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); - CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); - - byte[] bytesBuffer = new byte[0]; - - try - { - switch (ticFrame.getMode()) - { - case STANDARD: - { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } - case HISTORIC: - { - bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); - break; - } - default: - { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } - } - } - catch (CodecException exception) - { - throw new CodecException("Can't encode TICFrame" + exception.getMessage()); - } - return bytesBuffer; - } - - /** - * Reset buffer - */ - public void reset() - { - this.Buffer.clear(); - } - - /** - * Append data to buffer - * - * @param data - */ - public void append(byte[] data) - { - this.Buffer.addAll(data); - } - - /** - * Append data to buffer - * - * @param data - */ - public void append(byte data) - { - this.Buffer.add(data); - } - - /** - * Append data to buffer - * - * @param data - */ - public void append(BytesArray data) - { - this.Buffer.addAll(data.getBytes()); - } - - /** - * Get mode - * - * @return mode - */ - public TICMode getMode() - { - return this.mode; - } - - /** - * Set mode - * - * @param mode - */ - public void setMode(TICMode mode) - { - if (this.mode != mode) - { - this.mode = mode; - - if (TICMode.AUTO != mode) - { - this.setCurrentMode(mode); - } - - else - { - this.setCurrentMode(DEFAULT_MODE); - } - } - } - - /** - * Get current mode - * - * @return current mode - */ - public TICMode getCurrentMode() - { - return this.currentMode; - } - - /** - * Set current mode - * - * @param currentMode - */ - public void setCurrentMode(TICMode currentMode) - { - if (currentMode != this.currentMode) - { - this.currentMode = currentMode; - } - } - +/** Codec TIC */ +public class TICCodec implements Codec { + private static final TICMode DEFAULT_MODE = TICMode.STANDARD; + + private BytesArray Buffer; + private TICMode mode = TICMode.UNKNOWN; + private TICMode currentMode = TICMode.UNKNOWN; + + /** Default constructor */ + public TICCodec() { + this.mode = TICMode.UNKNOWN; + this.currentMode = TICMode.UNKNOWN; + this.Buffer = new BytesArray(); + } + + @Override + public TICFrame decode(byte[] newData) throws CodecException { + CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); + CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); + TICFrameStandard ticFrameStandard = null; + TICFrameHistoric ticFrameHistoric = null; + + TICFrame ticFrame = null; + TICMode currentTICMode = null; + + try { + switch (this.currentMode) { + case STANDARD: + { + ticFrameStandard = codecStandard.decode(newData); + ticFrame = ticFrameStandard; + break; + } + case HISTORIC: + { + ticFrameHistoric = codecHistoric.decode(newData); + ticFrame = ticFrameHistoric; + break; + } + + case AUTO: + { + try { + currentTICMode = TICMode.findModeFromFrameBuffer(newData); + } catch (TICException exception) { + throw new CodecException("can't determinated TIC Mode"); + } + + if (currentTICMode == TICMode.STANDARD) { + ticFrameStandard = codecStandard.decode(newData); + ticFrame = ticFrameStandard; + break; + } else if (currentTICMode == TICMode.HISTORIC) { + ticFrameHistoric = codecHistoric.decode(newData); + ticFrame = ticFrameHistoric; + } else { + throw new CodecException("can't decode TIC, unable to find TIC Modem"); + } + } + + default: + { + /**/ + } + } + } catch (CodecException exception) { + throw new CodecException(exception.getMessage(), exception.getData()); + } + return ticFrame; + } + + @Override + public byte[] encode(TICFrame ticFrame) throws CodecException { + CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); + CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); + + byte[] bytesBuffer = new byte[0]; + + try { + switch (ticFrame.getMode()) { + case STANDARD: + { + bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); + break; + } + case HISTORIC: + { + bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); + break; + } + default: + { + bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); + break; + } + } + } catch (CodecException exception) { + throw new CodecException("Can't encode TICFrame" + exception.getMessage()); + } + return bytesBuffer; + } + + /** Reset buffer */ + public void reset() { + this.Buffer.clear(); + } + + /** + * Append data to buffer + * + * @param data + */ + public void append(byte[] data) { + this.Buffer.addAll(data); + } + + /** + * Append data to buffer + * + * @param data + */ + public void append(byte data) { + this.Buffer.add(data); + } + + /** + * Append data to buffer + * + * @param data + */ + public void append(BytesArray data) { + this.Buffer.addAll(data.getBytes()); + } + + /** + * Get mode + * + * @return mode + */ + public TICMode getMode() { + return this.mode; + } + + /** + * Set mode + * + * @param mode + */ + public void setMode(TICMode mode) { + if (this.mode != mode) { + this.mode = mode; + + if (TICMode.AUTO != mode) { + this.setCurrentMode(mode); + } else { + this.setCurrentMode(DEFAULT_MODE); + } + } + } + + /** + * Get current mode + * + * @return current mode + */ + public TICMode getCurrentMode() { + return this.currentMode; + } + + /** + * Set current mode + * + * @param currentMode + */ + public void setCurrentMode(TICMode currentMode) { + if (currentMode != this.currentMode) { + this.currentMode = currentMode; + } + } } diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java index abf35e0..15a1a9e 100644 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java +++ b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java @@ -20,162 +20,139 @@ import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; -/** - * TIC input stream - */ -public class TICInputStream extends DataInputStream -{ - /** Key timestamp */ - public static final String KEY_TIMESTAMP = "timestamp"; - /** Key channel */ - public static final String KEY_CHANNEL = "channel"; - /** Key data */ - public static final String KEY_DATA = "data"; - - protected TICCodec codec; - - /** - * Constructor using configuration - * - * @param configuration - * @throws DataStreamException - */ - public TICInputStream(TICStreamConfiguration configuration) throws DataStreamException - { - super(configuration); - this.codec = new TICCodec(); - this.codec.setCurrentMode(configuration.getTicMode()); - } - - @Override - public DataDictionary read() throws DataStreamException - { - return null; - } - - @Override - public DataStreamType getType() - { - return DataStreamType.TIC; - } - - @Override - public void onDataRead(String channelName, byte[] data) - { - if (!this.notifier.getSubscribers().isEmpty()) - { - DataDictionary ticFrame = null; - try - { - ticFrame = this.decodeTICFrame(data); - if (ticFrame != null) - { - this.notifyOnDataReceived(ticFrame); - } - } - catch (DataDictionaryException exception) - { - this.logger.error(exception.getMessage(), exception); - } - catch (CodecException exception) - { - DataDictionaryBase errorDataDictionary = new DataDictionaryBase(); - - String KEY_PARTIAL_FRAME = "partialTICFrame"; - - errorDataDictionary.addKey(KEY_PARTIAL_FRAME); - - try - { - Object exceptionData = exception.getData(); - if (exceptionData != null && exceptionData instanceof TICFrame) - { - try - { - DataDictionary frameDataDictionary = ((TICFrame) exceptionData).getDataDictionary(); - if (frameDataDictionary != null) - { - errorDataDictionary.set(KEY_PARTIAL_FRAME, frameDataDictionary); - } - else - { - this.logger.warn("Frame data dictionary is null"); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } - catch (DataDictionaryException frameDataDictionaryException) - { - this.logger.warn("Can't get TICFrame data dictionary: " + frameDataDictionaryException.getMessage()); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } - else - { - this.logger.error("No frame data available"); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } - catch (DataDictionaryException dataDictionaryException) - { - this.logger.error("Can't convert TICFrame to DataDictonary" + dataDictionaryException.getMessage()); - } - - this.logger.error(exception.getMessage() + errorDataDictionary.toString()); - this.onErrorDetected(channelName, TICError.TIC_READER_READ_FRAME_CHECKSUM_INVALID.getValue(), exception.getMessage(), errorDataDictionary); - } - } - } - - @Override - public void onDataWritten(String channelName, byte[] data) - { - // Not used - } - - @Override - public void onErrorDetected(String channelName, int errorCode, String errorMessage) - { - this.notifyOnErrorDetected(errorCode, errorMessage, null); - } - - @Override - public void onErrorDetected(String channelName, int errorCode, String errorMessage, DataDictionary errorData) - { - this.notifyOnErrorDetected(errorCode, errorMessage, errorData); - } - /** - * Get Mode - * - * @return TIC Mode - */ - public TICMode getMode() - { - ChannelTICSerialPort channelTIC = (ChannelTICSerialPort) this.channel; - - return channelTIC.getMode(); - } - - protected DataDictionary decodeTICFrame(byte[] data) throws DataDictionaryException, CodecException - { - TICFrame ticFrame; - try - { - ticFrame = this.codec.decode(data); - } - catch (CodecException exception) - { - throw new CodecException(exception.getMessage(), exception.getData()); - } - - long timestamp = System.currentTimeMillis(); - - DataDictionaryBase decodedTICFrame = new DataDictionaryBase(); - - decodedTICFrame.set(KEY_TIMESTAMP, timestamp); - decodedTICFrame.set(KEY_CHANNEL, this.getConfiguration().getChannelName()); - decodedTICFrame.set(KEY_DATA, ticFrame); - - return decodedTICFrame; - } - +/** TIC input stream */ +public class TICInputStream extends DataInputStream { + /** Key timestamp */ + public static final String KEY_TIMESTAMP = "timestamp"; + + /** Key channel */ + public static final String KEY_CHANNEL = "channel"; + + /** Key data */ + public static final String KEY_DATA = "data"; + + protected TICCodec codec; + + /** + * Constructor using configuration + * + * @param configuration + * @throws DataStreamException + */ + public TICInputStream(TICStreamConfiguration configuration) throws DataStreamException { + super(configuration); + this.codec = new TICCodec(); + this.codec.setCurrentMode(configuration.getTicMode()); + } + + @Override + public DataDictionary read() throws DataStreamException { + return null; + } + + @Override + public DataStreamType getType() { + return DataStreamType.TIC; + } + + @Override + public void onDataRead(String channelName, byte[] data) { + if (!this.notifier.getSubscribers().isEmpty()) { + DataDictionary ticFrame = null; + try { + ticFrame = this.decodeTICFrame(data); + if (ticFrame != null) { + this.notifyOnDataReceived(ticFrame); + } + } catch (DataDictionaryException exception) { + this.logger.error(exception.getMessage(), exception); + } catch (CodecException exception) { + DataDictionaryBase errorDataDictionary = new DataDictionaryBase(); + + String KEY_PARTIAL_FRAME = "partialTICFrame"; + + errorDataDictionary.addKey(KEY_PARTIAL_FRAME); + + try { + Object exceptionData = exception.getData(); + if (exceptionData != null && exceptionData instanceof TICFrame) { + try { + DataDictionary frameDataDictionary = ((TICFrame) exceptionData).getDataDictionary(); + if (frameDataDictionary != null) { + errorDataDictionary.set(KEY_PARTIAL_FRAME, frameDataDictionary); + } else { + this.logger.warn("Frame data dictionary is null"); + errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); + } + } catch (DataDictionaryException frameDataDictionaryException) { + this.logger.warn( + "Can't get TICFrame data dictionary: " + + frameDataDictionaryException.getMessage()); + errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); + } + } else { + this.logger.error("No frame data available"); + errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); + } + } catch (DataDictionaryException dataDictionaryException) { + this.logger.error( + "Can't convert TICFrame to DataDictonary" + dataDictionaryException.getMessage()); + } + + this.logger.error(exception.getMessage() + errorDataDictionary.toString()); + this.onErrorDetected( + channelName, + TICError.TIC_READER_READ_FRAME_CHECKSUM_INVALID.getValue(), + exception.getMessage(), + errorDataDictionary); + } + } + } + + @Override + public void onDataWritten(String channelName, byte[] data) { + // Not used + } + + @Override + public void onErrorDetected(String channelName, int errorCode, String errorMessage) { + this.notifyOnErrorDetected(errorCode, errorMessage, null); + } + + @Override + public void onErrorDetected( + String channelName, int errorCode, String errorMessage, DataDictionary errorData) { + this.notifyOnErrorDetected(errorCode, errorMessage, errorData); + } + + /** + * Get Mode + * + * @return TIC Mode + */ + public TICMode getMode() { + ChannelTICSerialPort channelTIC = (ChannelTICSerialPort) this.channel; + + return channelTIC.getMode(); + } + + protected DataDictionary decodeTICFrame(byte[] data) + throws DataDictionaryException, CodecException { + TICFrame ticFrame; + try { + ticFrame = this.codec.decode(data); + } catch (CodecException exception) { + throw new CodecException(exception.getMessage(), exception.getData()); + } + + long timestamp = System.currentTimeMillis(); + + DataDictionaryBase decodedTICFrame = new DataDictionaryBase(); + + decodedTICFrame.set(KEY_TIMESTAMP, timestamp); + decodedTICFrame.set(KEY_CHANNEL, this.getConfiguration().getChannelName()); + decodedTICFrame.set(KEY_DATA, ticFrame); + + return decodedTICFrame; + } } diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java index 04abc64..b201d36 100644 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java +++ b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java @@ -7,11 +7,6 @@ package enedis.lab.protocol.tic.datastreams; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.io.datastreams.DataStreamConfiguration; import enedis.lab.io.datastreams.DataStreamDirection; import enedis.lab.io.datastreams.DataStreamType; @@ -20,146 +15,135 @@ import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorEnum; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * TICStreamConfiguration class - * - * Generated + * + *

    Generated */ -public class TICStreamConfiguration extends DataStreamConfiguration -{ - protected static final String KEY_TIC_MODE = "ticMode"; - - private static final DataStreamType TYPE_ACCEPTED_VALUE = DataStreamType.TIC; - private static final DataStreamDirection[] DIRECTION_ACCEPTED_VALUES = { DataStreamDirection.OUTPUT, DataStreamDirection.INPUT }; - private static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kTicMode; - - protected TICStreamConfiguration() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUES); - this.kChannelName.setMandatory(true); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICStreamConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICStreamConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public TICStreamConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param direction - * @param channelName - * @param ticMode - * @throws DataDictionaryException - */ - public TICStreamConfiguration(String name, DataStreamDirection direction, String channelName, TICMode ticMode) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setDirection(direction); - this.setChannelName(channelName); - this.setTicMode(ticMode); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - if (!this.exists(KEY_TIC_MODE)) - { - this.setTicMode(TIC_MODE_DEFAULT_VALUE); - } - super.updateOptionalParameters(); - } - - /** - * Get tic mode - * - * @return the tic mode - */ - public TICMode getTicMode() - { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Set tic mode - * - * @param ticMode - * @throws DataDictionaryException - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException - { - this.setTicMode((Object) ticMode); - } - - protected void setTicMode(Object ticMode) throws DataDictionaryException - { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - } - - private void loadKeyDescriptors() - { - try - { - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); - this.keys.add(this.kTicMode); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class TICStreamConfiguration extends DataStreamConfiguration { + protected static final String KEY_TIC_MODE = "ticMode"; + + private static final DataStreamType TYPE_ACCEPTED_VALUE = DataStreamType.TIC; + private static final DataStreamDirection[] DIRECTION_ACCEPTED_VALUES = { + DataStreamDirection.OUTPUT, DataStreamDirection.INPUT + }; + private static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kTicMode; + + protected TICStreamConfiguration() { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUES); + this.kChannelName.setMandatory(true); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICStreamConfiguration(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICStreamConfiguration(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting configuration name/file and parameters to default values + * + * @param name the configuration name + * @param file the configuration file + */ + public TICStreamConfiguration(String name, File file) { + this(); + this.init(name, file); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param direction + * @param channelName + * @param ticMode + * @throws DataDictionaryException + */ + public TICStreamConfiguration( + String name, DataStreamDirection direction, String channelName, TICMode ticMode) + throws DataDictionaryException { + this(); + + this.setName(name); + this.setDirection(direction); + this.setChannelName(channelName); + this.setTicMode(ticMode); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_TYPE)) { + this.setType(TYPE_ACCEPTED_VALUE); + } + if (!this.exists(KEY_TIC_MODE)) { + this.setTicMode(TIC_MODE_DEFAULT_VALUE); + } + super.updateOptionalParameters(); + } + + /** + * Get tic mode + * + * @return the tic mode + */ + public TICMode getTicMode() { + return (TICMode) this.data.get(KEY_TIC_MODE); + } + + /** + * Set tic mode + * + * @param ticMode + * @throws DataDictionaryException + */ + public void setTicMode(TICMode ticMode) throws DataDictionaryException { + this.setTicMode((Object) ticMode); + } + + protected void setTicMode(Object ticMode) throws DataDictionaryException { + this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); + } + + private void loadKeyDescriptors() { + try { + this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); + this.keys.add(this.kTicMode); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java index db44f6e..a2ed66a 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -9,82 +9,77 @@ import jssc.SerialPortException; -/** - * TICClient errors definition - */ -public enum TICError -{ - /** No error */ - NO_ERROR(0), - /** Serial port not found */ - SERIAL_PORT_NOT_FOUND(17), - /** Serial port not found */ - SERIAL_PORT_INCORRECT_SERIAL_PORT(18), - /** Serial port not found */ - SERIAL_PORT_NULL_NOT_PERMITTED(19), - /** Serial port not found */ - SERIAL_PORT_ALREADY_OPENED(20), - /** Serial port not found */ - SERIAL_PORT_SERIAL_PORT_BUSY(21), - /** Serial port not found */ - SERIAL_PORT_CONFIGURATION_FAILURE(22), - /** TIC reader default error */ - TIC_READER_DEFAULT_ERROR(23), - /** TIC reader label name not found */ - TIC_READER_LABEL_NAME_NOT_FOUND(24), - /** TIC reader frame decode failed */ - TIC_READER_FRAME_DECODE_FAILED(25), - /** TIC reader read frame timeout */ - TIC_READER_READ_FRAME_TIMEOUT(26), - /** TIC reader read frame with checksum invalid */ - TIC_READER_READ_FRAME_CHECKSUM_INVALID(27),; - - private int value; +/** TICClient errors definition */ +public enum TICError { + /** No error */ + NO_ERROR(0), + /** Serial port not found */ + SERIAL_PORT_NOT_FOUND(17), + /** Serial port not found */ + SERIAL_PORT_INCORRECT_SERIAL_PORT(18), + /** Serial port not found */ + SERIAL_PORT_NULL_NOT_PERMITTED(19), + /** Serial port not found */ + SERIAL_PORT_ALREADY_OPENED(20), + /** Serial port not found */ + SERIAL_PORT_SERIAL_PORT_BUSY(21), + /** Serial port not found */ + SERIAL_PORT_CONFIGURATION_FAILURE(22), + /** TIC reader default error */ + TIC_READER_DEFAULT_ERROR(23), + /** TIC reader label name not found */ + TIC_READER_LABEL_NAME_NOT_FOUND(24), + /** TIC reader frame decode failed */ + TIC_READER_FRAME_DECODE_FAILED(25), + /** TIC reader read frame timeout */ + TIC_READER_READ_FRAME_TIMEOUT(26), + /** TIC reader read frame with checksum invalid */ + TIC_READER_READ_FRAME_CHECKSUM_INVALID(27), + ; - private TICError(int value) - { - this.value = value; - } + private int value; - /** - * Get error value - * - * @return the value - */ - public int getValue() - { - return this.value; - } + private TICError(int value) { + this.value = value; + } - /** - * Get serial port error - * - * @param serialPortException - * @return TICError - */ - public static TICError getSerialPortError(SerialPortException serialPortException) - { - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) - { - return TICError.SERIAL_PORT_SERIAL_PORT_BUSY; - } - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_INCORRECT_SERIAL_PORT)) - { - return TICError.SERIAL_PORT_INCORRECT_SERIAL_PORT; - } - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_NULL_NOT_PERMITTED)) - { - return TICError.SERIAL_PORT_NULL_NOT_PERMITTED; - } - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_NOT_FOUND)) - { - return TICError.SERIAL_PORT_NOT_FOUND; - } - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_ALREADY_OPENED)) - { - return TICError.SERIAL_PORT_ALREADY_OPENED; - } - return null; - } + /** + * Get error value + * + * @return the value + */ + public int getValue() { + return this.value; + } + /** + * Get serial port error + * + * @param serialPortException + * @return TICError + */ + public static TICError getSerialPortError(SerialPortException serialPortException) { + if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) { + return TICError.SERIAL_PORT_SERIAL_PORT_BUSY; + } + if (serialPortException + .getExceptionType() + .equals(SerialPortException.TYPE_INCORRECT_SERIAL_PORT)) { + return TICError.SERIAL_PORT_INCORRECT_SERIAL_PORT; + } + if (serialPortException + .getExceptionType() + .equals(SerialPortException.TYPE_NULL_NOT_PERMITTED)) { + return TICError.SERIAL_PORT_NULL_NOT_PERMITTED; + } + if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_NOT_FOUND)) { + return TICError.SERIAL_PORT_NOT_FOUND; + } + if (serialPortException + .getExceptionType() + .equals(SerialPortException.TYPE_PORT_ALREADY_OPENED)) { + return TICError.SERIAL_PORT_ALREADY_OPENED; + } + return null; + } } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java index d39dd68..307a934 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,462 +7,400 @@ package enedis.lab.protocol.tic.frame; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import org.json.JSONObject; - import enedis.lab.protocol.tic.TICMode; import enedis.lab.types.BytesArray; import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.json.JSONObject; -/** - * TICFrame - */ -public abstract class TICFrame -{ - /** Beginning pattern */ - public static final byte BEGINNING_PATTERN = 0x02; // STX - /** End pattern */ - public static final byte END_PATTERN = 0x03; // ETX - /** End of trame pattern */ - public static final byte EOT = 0x04; // EOT - - // Options - /** No specific options */ - public static final int EXPLICIT = 0x0000; - /** No checksum options */ - public static final int NOCHECKSUM = 0x0001; - /** No date time options */ - public static final int NODATETIME = 0x0002; - /** No tic mode options */ - public static final int NOTICMODE = 0x0004; - /** No invalid options */ - public static final int NOINVALID = 0x0008; - /** Trimmed options */ - public static final int TRIMMED = NOCHECKSUM | NODATETIME | NOINVALID; - - /** Key tic frame */ - public static final String KEY_TICFRAME = "ticFrame"; - /** Key tic mode */ - public static final String KEY_TICMODE = "ticMode"; - /** Key label */ - public static final String KEY_LABEL = "label"; - /** Key data */ - public static final String KEY_DATA = "data"; - /** Key checksum */ - public static final String KEY_CHECKSUM = "checksum"; - /** Key date time */ - public static final String KEY_DATETIME = "dateTime"; - - protected List DataSetList; - - /** - * Default constructor - */ - public TICFrame() - { - this.DataSetList = new ArrayList(); - } - - @Override - public int hashCode() - { - return Objects.hash(this.DataSetList); - } - - @Override - public boolean equals(Object obj) - { - if (this == obj) - return true; - if (obj == null) - return false; - if (this.getClass() != obj.getClass()) - return false; - TICFrame other = (TICFrame) obj; - return Objects.equals(this.DataSetList, other.DataSetList); - } - - /** - * Return the list of DataSet - * - * @return the list of DataSet - */ - public List getDataSetList() - { - return this.DataSetList; - } - - /** - * Set the list of DataSet - * - * @param dataSetList - */ - public void setDataSetList(List dataSetList) - { - this.DataSetList = dataSetList; - } - - /** - * Return the index of the given DataSet - * - * @param label - * Label of the DataSet - * @return the index of the given DataSet or '-1' if it does not exist - */ - public int indexOf(String label) - { - int index = -1; - - for (int i = 0; i < this.DataSetList.size(); i++) - { - if (this.DataSetList.get(i).getLabel().equals(label)) - { - index = i; - break; - } - } - - return index; - } - - /** - * Return the given DataSet - * - * @param label - * Label of the DataSet - * @return the given DataSet or 'null' if it does not exist - */ - public TICFrameDataSet getDataSet(String label) - { - TICFrameDataSet dataSet = null; - - for (int i = 0; i < this.DataSetList.size(); i++) - { - if (this.DataSetList.get(i).getLabel().equals(label)) - { - dataSet = this.DataSetList.get(i); - break; - } - } - - return dataSet; - } - - /** - * Return the data field of the given DataSet - * - * @param label - * Label of the DataSet - * @return the data field of the given DataSet - */ - public String getData(String label) - { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) - { - return dataSet.getData(); - } - - else - { - return null; - } - } - - /** - * Add the dataSet at the end of the list. If the dataSet already existed, its data and checksum are just set - * - * @param dataSet - */ - public void addDataSet(TICFrameDataSet dataSet) - { - TICFrameDataSet oldDataSet = this.getDataSet(dataSet.getLabel()); - - if (oldDataSet == null) - { - this.DataSetList.add(dataSet); - } - - else - { - oldDataSet.setData(dataSet.getData()); - oldDataSet.setChecksum(dataSet.getChecksum()); - } - } - - /** - * Add the dataSet at the given index of the list. If the dataSet already existed, its data and checksum are set and - * it is moved at the given index - * - * @param index - * @param dataSet - */ - public void addDataSet(int index, TICFrameDataSet dataSet) - { - int dataSetIndex = this.indexOf(dataSet.getLabel()); - - if (dataSetIndex < 0) - { - this.DataSetList.add(index, dataSet); - } - - else - { - if (dataSetIndex != index) - { - this.removeDataSet(dataSetIndex); - this.DataSetList.add(index, dataSet); - } - - else - { - this.DataSetList.get(index).setData(dataSet.getData()); - this.DataSetList.get(index).setChecksum(dataSet.getChecksum()); - } - } - } - - /** - * Add data set - * - * @param label - * @param data - * @return tic frame data set - */ - public abstract TICFrameDataSet addDataSet(String label, String data); - - /** - * Add data set - * - * @param index - * @param label - * @param data - * @return tic frame data set - */ - public abstract TICFrameDataSet addDataSet(int index, String label, String data); - - /** - * Move the DataSet at the given index. - * - * @param label - * @param index - * @return true if the operation has been done, else return false - */ - public boolean moveDataSet(String label, int index) - { - int previous_index = this.indexOf(label); - - // Si l'ensemble des conditions suivantes sont vérifiées : - // - le Groupe d'Information existe, - // - l'index donné est cohérent - // - l'index donné diffère de l'index actuel - if ((previous_index >= 0) && (index >= 0) && (index < this.DataSetList.size()) && (index != previous_index)) - { - // Alors déplacer le Groupe d'Information à l'index donné: - - TICFrameDataSet dataSet = this.DataSetList.remove(previous_index); - - if (index < this.DataSetList.size()) - { - this.DataSetList.add(index, dataSet); - } - - else - { - this.DataSetList.add(dataSet); - } - - return true; - } - - else - { - // Sinon, l'opération n'est pas effectuée - return false; - } - } - - /** - * Set data set - * - * @param label - * @param data - * @return true if succeed - */ - public boolean setDataSet(String label, String data) - { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) - { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - return true; - } - - else - { - return false; - } - } - - /** - * Set data set - * - * @param label - * @param data - * @param checksum - * @return true if succeed - */ - public boolean setDataSet(String label, String data, byte checksum) - { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) - { - dataSet.setData(data); - dataSet.setChecksum(checksum); - - return true; - } - - else - { - return false; - } - } - - /** - * Remove data set - * - * @param label - * @return tic frame data set - */ - public TICFrameDataSet removeDataSet(String label) - { - TICFrameDataSet dataSet = null; - - int index = this.indexOf(label); - - if (index >= 0) - { - dataSet = this.DataSetList.remove(index); - } - - return dataSet; - } - - /** - * Remove data set - * - * @param index - * @return tic frame data set - */ - public TICFrameDataSet removeDataSet(int index) - { - TICFrameDataSet dataSet = null; - - if ((index >= 0) && (index < this.DataSetList.size())) - { - dataSet = this.DataSetList.remove(index); - } - - return dataSet; - } - - /** - * Get bytes - * - * @return bytes - */ - public byte[] getBytes() - { - BytesArray bytesFrame = new BytesArray(); - - bytesFrame.add(BEGINNING_PATTERN); - - for (int i = 0; i < this.DataSetList.size(); i++) - { - bytesFrame.addAll(this.DataSetList.get(i).getBytes()); - } - - bytesFrame.add(END_PATTERN); - - return bytesFrame.getBytes(); - } - - /** - * Get data dictionary - * - * @return data dictionary - * @throws DataDictionaryException - */ - public DataDictionary getDataDictionary() throws DataDictionaryException - { - return this.getDataDictionary(0); - } - - /** - * Return a DataDictionary object corresponding to the TIC frame - * - * @param options - * options to DataDictionary render (use like bits fields) : - EXPLICIT : frame is detailed - NOCHECKSUM - * : DataSet checksum are hidden - NODATETIME : DataSet dateTime fields are hidden - NOTICMODE : Tic mode - * is hidden; - TRIMMED : NOCHECKSUM | NODATETIME - FILTER_INVALID : Invalid DataSet are filtered - * - * - * @return data dictionary - * @throws DataDictionaryException - */ - public DataDictionary getDataDictionary(int options) throws DataDictionaryException - { - DataDictionaryBase dictionary = new DataDictionaryBase(); - - JSONObject jsonObject = new JSONObject(); - - if (0 == (options & NOTICMODE)) - { - dictionary.addKey(KEY_TICMODE); - dictionary.set(KEY_TICMODE, this.getMode().name()); - } - - for (int i = 0; i < this.DataSetList.size(); i++) - { - TICFrameDataSet dataSet = this.DataSetList.get(i); - - // Si le DataSet n'est pas valid et que l'option "filtrer les - // groupes invalides" est activée, - if ((false == dataSet.isValid()) && ((options & NOINVALID) > 0)) - { - // Ignorer le DataSet - } - - // Sinon, - else - { - jsonObject.append(KEY_TICFRAME, dataSet.toJSON(options)); - } - - } - - dictionary.addKey(KEY_TICFRAME); - dictionary.set(KEY_TICFRAME, jsonObject.get(KEY_TICFRAME)); - - return dictionary; - } - - /** - * Get mode - * - * @return mode - */ - public abstract TICMode getMode(); - +/** TICFrame */ +public abstract class TICFrame { + /** Beginning pattern */ + public static final byte BEGINNING_PATTERN = 0x02; // STX + + /** End pattern */ + public static final byte END_PATTERN = 0x03; // ETX + + /** End of trame pattern */ + public static final byte EOT = 0x04; // EOT + + // Options + /** No specific options */ + public static final int EXPLICIT = 0x0000; + + /** No checksum options */ + public static final int NOCHECKSUM = 0x0001; + + /** No date time options */ + public static final int NODATETIME = 0x0002; + + /** No tic mode options */ + public static final int NOTICMODE = 0x0004; + + /** No invalid options */ + public static final int NOINVALID = 0x0008; + + /** Trimmed options */ + public static final int TRIMMED = NOCHECKSUM | NODATETIME | NOINVALID; + + /** Key tic frame */ + public static final String KEY_TICFRAME = "ticFrame"; + + /** Key tic mode */ + public static final String KEY_TICMODE = "ticMode"; + + /** Key label */ + public static final String KEY_LABEL = "label"; + + /** Key data */ + public static final String KEY_DATA = "data"; + + /** Key checksum */ + public static final String KEY_CHECKSUM = "checksum"; + + /** Key date time */ + public static final String KEY_DATETIME = "dateTime"; + + protected List DataSetList; + + /** Default constructor */ + public TICFrame() { + this.DataSetList = new ArrayList(); + } + + @Override + public int hashCode() { + return Objects.hash(this.DataSetList); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; + TICFrame other = (TICFrame) obj; + return Objects.equals(this.DataSetList, other.DataSetList); + } + + /** + * Return the list of DataSet + * + * @return the list of DataSet + */ + public List getDataSetList() { + return this.DataSetList; + } + + /** + * Set the list of DataSet + * + * @param dataSetList + */ + public void setDataSetList(List dataSetList) { + this.DataSetList = dataSetList; + } + + /** + * Return the index of the given DataSet + * + * @param label Label of the DataSet + * @return the index of the given DataSet or '-1' if it does not exist + */ + public int indexOf(String label) { + int index = -1; + + for (int i = 0; i < this.DataSetList.size(); i++) { + if (this.DataSetList.get(i).getLabel().equals(label)) { + index = i; + break; + } + } + + return index; + } + + /** + * Return the given DataSet + * + * @param label Label of the DataSet + * @return the given DataSet or 'null' if it does not exist + */ + public TICFrameDataSet getDataSet(String label) { + TICFrameDataSet dataSet = null; + + for (int i = 0; i < this.DataSetList.size(); i++) { + if (this.DataSetList.get(i).getLabel().equals(label)) { + dataSet = this.DataSetList.get(i); + break; + } + } + + return dataSet; + } + + /** + * Return the data field of the given DataSet + * + * @param label Label of the DataSet + * @return the data field of the given DataSet + */ + public String getData(String label) { + TICFrameDataSet dataSet = this.getDataSet(label); + + if (dataSet != null) { + return dataSet.getData(); + } else { + return null; + } + } + + /** + * Add the dataSet at the end of the list. If the dataSet already existed, its data and checksum + * are just set + * + * @param dataSet + */ + public void addDataSet(TICFrameDataSet dataSet) { + TICFrameDataSet oldDataSet = this.getDataSet(dataSet.getLabel()); + + if (oldDataSet == null) { + this.DataSetList.add(dataSet); + } else { + oldDataSet.setData(dataSet.getData()); + oldDataSet.setChecksum(dataSet.getChecksum()); + } + } + + /** + * Add the dataSet at the given index of the list. If the dataSet already existed, its data and + * checksum are set and it is moved at the given index + * + * @param index + * @param dataSet + */ + public void addDataSet(int index, TICFrameDataSet dataSet) { + int dataSetIndex = this.indexOf(dataSet.getLabel()); + + if (dataSetIndex < 0) { + this.DataSetList.add(index, dataSet); + } else { + if (dataSetIndex != index) { + this.removeDataSet(dataSetIndex); + this.DataSetList.add(index, dataSet); + } else { + this.DataSetList.get(index).setData(dataSet.getData()); + this.DataSetList.get(index).setChecksum(dataSet.getChecksum()); + } + } + } + + /** + * Add data set + * + * @param label + * @param data + * @return tic frame data set + */ + public abstract TICFrameDataSet addDataSet(String label, String data); + + /** + * Add data set + * + * @param index + * @param label + * @param data + * @return tic frame data set + */ + public abstract TICFrameDataSet addDataSet(int index, String label, String data); + + /** + * Move the DataSet at the given index. + * + * @param label + * @param index + * @return true if the operation has been done, else return false + */ + public boolean moveDataSet(String label, int index) { + int previous_index = this.indexOf(label); + + // Si l'ensemble des conditions suivantes sont vérifiées : + // - le Groupe d'Information existe, + // - l'index donné est cohérent + // - l'index donné diffère de l'index actuel + if ((previous_index >= 0) + && (index >= 0) + && (index < this.DataSetList.size()) + && (index != previous_index)) { + // Alors déplacer le Groupe d'Information à l'index donné: + + TICFrameDataSet dataSet = this.DataSetList.remove(previous_index); + + if (index < this.DataSetList.size()) { + this.DataSetList.add(index, dataSet); + } else { + this.DataSetList.add(dataSet); + } + + return true; + } else { + // Sinon, l'opération n'est pas effectuée + return false; + } + } + + /** + * Set data set + * + * @param label + * @param data + * @return true if succeed + */ + public boolean setDataSet(String label, String data) { + TICFrameDataSet dataSet = this.getDataSet(label); + + if (dataSet != null) { + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + return true; + } else { + return false; + } + } + + /** + * Set data set + * + * @param label + * @param data + * @param checksum + * @return true if succeed + */ + public boolean setDataSet(String label, String data, byte checksum) { + TICFrameDataSet dataSet = this.getDataSet(label); + + if (dataSet != null) { + dataSet.setData(data); + dataSet.setChecksum(checksum); + + return true; + } else { + return false; + } + } + + /** + * Remove data set + * + * @param label + * @return tic frame data set + */ + public TICFrameDataSet removeDataSet(String label) { + TICFrameDataSet dataSet = null; + + int index = this.indexOf(label); + + if (index >= 0) { + dataSet = this.DataSetList.remove(index); + } + + return dataSet; + } + + /** + * Remove data set + * + * @param index + * @return tic frame data set + */ + public TICFrameDataSet removeDataSet(int index) { + TICFrameDataSet dataSet = null; + + if ((index >= 0) && (index < this.DataSetList.size())) { + dataSet = this.DataSetList.remove(index); + } + + return dataSet; + } + + /** + * Get bytes + * + * @return bytes + */ + public byte[] getBytes() { + BytesArray bytesFrame = new BytesArray(); + + bytesFrame.add(BEGINNING_PATTERN); + + for (int i = 0; i < this.DataSetList.size(); i++) { + bytesFrame.addAll(this.DataSetList.get(i).getBytes()); + } + + bytesFrame.add(END_PATTERN); + + return bytesFrame.getBytes(); + } + + /** + * Get data dictionary + * + * @return data dictionary + * @throws DataDictionaryException + */ + public DataDictionary getDataDictionary() throws DataDictionaryException { + return this.getDataDictionary(0); + } + + /** + * Return a DataDictionary object corresponding to the TIC frame + * + * @param options options to DataDictionary render (use like bits fields) : - EXPLICIT : frame is + * detailed - NOCHECKSUM : DataSet checksum are hidden - NODATETIME : DataSet dateTime fields + * are hidden - NOTICMODE : Tic mode is hidden; - TRIMMED : NOCHECKSUM | NODATETIME - + * FILTER_INVALID : Invalid DataSet are filtered + * @return data dictionary + * @throws DataDictionaryException + */ + public DataDictionary getDataDictionary(int options) throws DataDictionaryException { + DataDictionaryBase dictionary = new DataDictionaryBase(); + + JSONObject jsonObject = new JSONObject(); + + if (0 == (options & NOTICMODE)) { + dictionary.addKey(KEY_TICMODE); + dictionary.set(KEY_TICMODE, this.getMode().name()); + } + + for (int i = 0; i < this.DataSetList.size(); i++) { + TICFrameDataSet dataSet = this.DataSetList.get(i); + + // Si le DataSet n'est pas valid et que l'option "filtrer les + // groupes invalides" est activée, + if ((false == dataSet.isValid()) && ((options & NOINVALID) > 0)) { + // Ignorer le DataSet + } + + // Sinon, + else { + jsonObject.append(KEY_TICFRAME, dataSet.toJSON(options)); + } + } + + dictionary.addKey(KEY_TICFRAME); + dictionary.set(KEY_TICFRAME, jsonObject.get(KEY_TICFRAME)); + + return dictionary; + } + + /** + * Get mode + * + * @return mode + */ + public abstract TICMode getMode(); } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java index 576b060..942f4e4 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,253 +7,219 @@ package enedis.lab.protocol.tic.frame; +import enedis.lab.types.BytesArray; import java.util.Objects; - import org.json.JSONObject; -import enedis.lab.types.BytesArray; - -/** - * TIC frame data set - */ -public abstract class TICFrameDataSet -{ - /** Beginning pattern */ - public static final byte BEGINNING_PATTERN = 0x0A; // LF - /** End pattern */ - public static final byte END_PATTERN = 0x0D; // CR - - protected BytesArray label = null; - protected BytesArray data = null; - protected byte checksum; - - /** - * Default constructor - */ - public TICFrameDataSet() - { - this.init(); - } - - @Override - public int hashCode() - { - return Objects.hash(this.checksum, this.data, this.label); - } - - @Override - public boolean equals(Object obj) - { - if (this == obj) - return true; - if (obj == null) - return false; - if (this.getClass() != obj.getClass()) - return false; - TICFrameDataSet other = (TICFrameDataSet) obj; - return this.checksum == other.checksum && Objects.equals(this.data, other.data) && Objects.equals(this.label, other.label); - } - - /** - * Setup from string label and data - * - * @param label - * @param data - */ - public void setup(String label, String data) - { - this.setup(label.getBytes(), data.getBytes()); - } - - /** - * Setup from byte[] label and data - * - * @param label - * @param data - */ - public void setup(byte[] label, byte[] data) - { - this.label = new BytesArray(label); - this.data = new BytesArray(data); - } - - /** - * Get label - * - * @return label - */ - public String getLabel() - { - return new String(this.label.getBytes()); - } - - /** - * Set the Label - * - * @param label - */ - public void setLabel(String label) - { - this.setLabel(label.getBytes()); - } - - /** - * Set the Label - * - * @param label - */ - public void setLabel(byte[] label) - { - this.setLabel(new BytesArray(label)); - } - - /** - * Set the Label - * - * @param label - */ - public void setLabel(BytesArray label) - { - this.label = label; - this.setChecksum(); - } - - /** - * Get data - * - * @return data - */ - public String getData() - { - return new String(this.data.getBytes()); - } - - /** - * Set data - * - * @param data - */ - public void setData(String data) - { - this.setData(data.getBytes()); - } - - /** - * Set data - * - * @param data - */ - public void setData(byte[] data) - { - this.setData(new BytesArray(data)); - } - - /** - * Set data - * - * @param data - */ - public void setData(BytesArray data) - { - this.data = data; - this.setChecksum(); - } - - /** - * Get checksum - * - * @return checksum - */ - public byte getChecksum() - { - - return this.checksum; - } - - /** - * Compute and set the checksum (NB: Label and Data shall have been set) - * - * @return true if the operation succeeds, else, return false - */ - public boolean setChecksum() - { - Byte checksum = this.getConsistentChecksum(); - - if (checksum != null) - { - this.checksum = checksum; - return true; - } - - else - { - return false; - } - } - - /** - * Set the checksum at a given value - * - * @param checksum - */ - public void setChecksum(byte checksum) - { - this.checksum = checksum; - - } - - /** - * - * @return true if fields are consistent with checksum - */ - public boolean isValid() - { - Byte consistentChecksum = this.getConsistentChecksum(); - - if ((consistentChecksum != null) && (consistentChecksum.byteValue() == (this.checksum & (byte) 0xFF))) - { - return true; - } - - else - { - return false; - } - } - - /** - * Get bytes - * - * @return bytes - */ - public abstract byte[] getBytes(); - - /** - * Conver to JSON - * - * @return json object - */ - public abstract JSONObject toJSON(); - - /** - * Conver to JSON - * - * @param option - * @return json object - */ - public abstract JSONObject toJSON(int option); - - /** - * Return the consistent checksum of the DataSet - * - * @return the consistent checksum of the DataSet - */ - protected abstract Byte getConsistentChecksum(); - - private void init() - { - this.label = null; - this.data = null; - this.checksum = -1; - } +/** TIC frame data set */ +public abstract class TICFrameDataSet { + /** Beginning pattern */ + public static final byte BEGINNING_PATTERN = 0x0A; // LF + + /** End pattern */ + public static final byte END_PATTERN = 0x0D; // CR + + protected BytesArray label = null; + protected BytesArray data = null; + protected byte checksum; + + /** Default constructor */ + public TICFrameDataSet() { + this.init(); + } + + @Override + public int hashCode() { + return Objects.hash(this.checksum, this.data, this.label); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; + TICFrameDataSet other = (TICFrameDataSet) obj; + return this.checksum == other.checksum + && Objects.equals(this.data, other.data) + && Objects.equals(this.label, other.label); + } + + /** + * Setup from string label and data + * + * @param label + * @param data + */ + public void setup(String label, String data) { + this.setup(label.getBytes(), data.getBytes()); + } + + /** + * Setup from byte[] label and data + * + * @param label + * @param data + */ + public void setup(byte[] label, byte[] data) { + this.label = new BytesArray(label); + this.data = new BytesArray(data); + } + + /** + * Get label + * + * @return label + */ + public String getLabel() { + return new String(this.label.getBytes()); + } + + /** + * Set the Label + * + * @param label + */ + public void setLabel(String label) { + this.setLabel(label.getBytes()); + } + + /** + * Set the Label + * + * @param label + */ + public void setLabel(byte[] label) { + this.setLabel(new BytesArray(label)); + } + + /** + * Set the Label + * + * @param label + */ + public void setLabel(BytesArray label) { + this.label = label; + this.setChecksum(); + } + + /** + * Get data + * + * @return data + */ + public String getData() { + return new String(this.data.getBytes()); + } + + /** + * Set data + * + * @param data + */ + public void setData(String data) { + this.setData(data.getBytes()); + } + + /** + * Set data + * + * @param data + */ + public void setData(byte[] data) { + this.setData(new BytesArray(data)); + } + + /** + * Set data + * + * @param data + */ + public void setData(BytesArray data) { + this.data = data; + this.setChecksum(); + } + + /** + * Get checksum + * + * @return checksum + */ + public byte getChecksum() { + + return this.checksum; + } + + /** + * Compute and set the checksum (NB: Label and Data shall have been set) + * + * @return true if the operation succeeds, else, return false + */ + public boolean setChecksum() { + Byte checksum = this.getConsistentChecksum(); + + if (checksum != null) { + this.checksum = checksum; + return true; + } else { + return false; + } + } + + /** + * Set the checksum at a given value + * + * @param checksum + */ + public void setChecksum(byte checksum) { + this.checksum = checksum; + } + + /** + * @return true if fields are consistent with checksum + */ + public boolean isValid() { + Byte consistentChecksum = this.getConsistentChecksum(); + + if ((consistentChecksum != null) + && (consistentChecksum.byteValue() == (this.checksum & (byte) 0xFF))) { + return true; + } else { + return false; + } + } + + /** + * Get bytes + * + * @return bytes + */ + public abstract byte[] getBytes(); + + /** + * Conver to JSON + * + * @return json object + */ + public abstract JSONObject toJSON(); + + /** + * Conver to JSON + * + * @param option + * @return json object + */ + public abstract JSONObject toJSON(int option); + + /** + * Return the consistent checksum of the DataSet + * + * @return the consistent checksum of the DataSet + */ + protected abstract Byte getConsistentChecksum(); + + private void init() { + this.label = null; + this.data = null; + this.checksum = -1; + } } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java index a172f10..d2d7a1c 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java @@ -10,76 +10,54 @@ import enedis.lab.protocol.tic.TICMode; import enedis.lab.protocol.tic.frame.TICFrame; -/** - * TIC frame historic - */ -public class TICFrameHistoric extends TICFrame -{ - /** Separator */ - public static final byte SEPARATOR = 0x20; // SP - - /** - * Default constructor - */ - public TICFrameHistoric() - { - super(); - } - - @Override - public TICFrameHistoricDataSet addDataSet(String label, String data) - { - return this.addDataSet(this.DataSetList.size(), label, data); - } - - @Override - public TICFrameHistoricDataSet addDataSet(int index, String label, String data) - { - TICFrameHistoricDataSet dataSet = (TICFrameHistoricDataSet) this.getDataSet(label); - - if (dataSet == null) - { - dataSet = new TICFrameHistoricDataSet(); - dataSet.setLabel(label); - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - if ((index >= 0) && (index < this.DataSetList.size())) - { - this.DataSetList.add(index, dataSet); - } - - else - { - this.DataSetList.add(dataSet); - } - } - - else - { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - this.DataSetList.remove(dataSet); - - if (index >= this.DataSetList.size()) - { - this.DataSetList.add(dataSet); - } - - else - { - this.DataSetList.add(index, dataSet); - } - } - - return dataSet; - } - - @Override - public TICMode getMode() - { - return TICMode.HISTORIC; - } - +/** TIC frame historic */ +public class TICFrameHistoric extends TICFrame { + /** Separator */ + public static final byte SEPARATOR = 0x20; // SP + + /** Default constructor */ + public TICFrameHistoric() { + super(); + } + + @Override + public TICFrameHistoricDataSet addDataSet(String label, String data) { + return this.addDataSet(this.DataSetList.size(), label, data); + } + + @Override + public TICFrameHistoricDataSet addDataSet(int index, String label, String data) { + TICFrameHistoricDataSet dataSet = (TICFrameHistoricDataSet) this.getDataSet(label); + + if (dataSet == null) { + dataSet = new TICFrameHistoricDataSet(); + dataSet.setLabel(label); + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + if ((index >= 0) && (index < this.DataSetList.size())) { + this.DataSetList.add(index, dataSet); + } else { + this.DataSetList.add(dataSet); + } + } else { + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + this.DataSetList.remove(dataSet); + + if (index >= this.DataSetList.size()) { + this.DataSetList.add(dataSet); + } else { + this.DataSetList.add(index, dataSet); + } + } + + return dataSet; + } + + @Override + public TICMode getMode() { + return TICMode.HISTORIC; + } } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java index 2061d5b..3e727fc 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,129 +7,104 @@ package enedis.lab.protocol.tic.frame.historic; -import org.json.JSONObject; - import enedis.lab.protocol.tic.frame.TICFrame; import enedis.lab.protocol.tic.frame.TICFrameDataSet; import enedis.lab.types.BytesArray; +import org.json.JSONObject; -/** - * TIC frame historic data set - */ -public class TICFrameHistoricDataSet extends TICFrameDataSet -{ - /** Separator */ - public static final byte SEPARATOR = 0x20; // SP - - /** - * Constructor - */ - public TICFrameHistoricDataSet() - { - super(); - } - - @Override - public Byte getConsistentChecksum() - { - Byte crc = 0; - byte[] labelByte; - byte[] dataByte; - if ((this.label != null) && (this.data != null)) - { - labelByte = this.label.getBytes(); - for (int i = 0; i < labelByte.length; i++) - { - crc = this.computeUpdate(crc, labelByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - dataByte = this.data.getBytes(); - - for (int i = 0; i < dataByte.length; i++) - { - crc = this.computeUpdate(crc, dataByte[i]); - } - - return this.computeEnd(crc); - } - else - { - return null; - } - - } - - /** - * compute Update - * - * @param crc - * @param octet - * @return Byte crc after compute update - */ - public Byte computeUpdate(Byte crc, byte octet) - { - return (byte) (crc + (octet & 0xff)); - } - - /** - * compute End - * - * @param crc - * @return Byte crc after compute end - */ - public Byte computeEnd(Byte crc) - { - return (byte) ((crc & 0x3F) + 0x20); - } - - @Override - public byte[] getBytes() - { - BytesArray dataSet = new BytesArray(); - - if ((this.label != null) && (this.data != null)) - { - dataSet.add(BEGINNING_PATTERN); - dataSet.addAll(this.label.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.addAll(this.data.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.add(this.checksum); - - dataSet.add(END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public JSONObject toJSON() - { - return this.toJSON(TICFrame.TRIMMED); - } - - @Override - public JSONObject toJSON(int option) - { - JSONObject jsonObject = new JSONObject(); - - if ((option & TICFrame.NOCHECKSUM) > 0) - { - jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); - } - - else - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - - return jsonObject; - } - +/** TIC frame historic data set */ +public class TICFrameHistoricDataSet extends TICFrameDataSet { + /** Separator */ + public static final byte SEPARATOR = 0x20; // SP + + /** Constructor */ + public TICFrameHistoricDataSet() { + super(); + } + + @Override + public Byte getConsistentChecksum() { + Byte crc = 0; + byte[] labelByte; + byte[] dataByte; + if ((this.label != null) && (this.data != null)) { + labelByte = this.label.getBytes(); + for (int i = 0; i < labelByte.length; i++) { + crc = this.computeUpdate(crc, labelByte[i]); + } + + crc = this.computeUpdate(crc, SEPARATOR); + dataByte = this.data.getBytes(); + + for (int i = 0; i < dataByte.length; i++) { + crc = this.computeUpdate(crc, dataByte[i]); + } + + return this.computeEnd(crc); + } else { + return null; + } + } + + /** + * compute Update + * + * @param crc + * @param octet + * @return Byte crc after compute update + */ + public Byte computeUpdate(Byte crc, byte octet) { + return (byte) (crc + (octet & 0xff)); + } + + /** + * compute End + * + * @param crc + * @return Byte crc after compute end + */ + public Byte computeEnd(Byte crc) { + return (byte) ((crc & 0x3F) + 0x20); + } + + @Override + public byte[] getBytes() { + BytesArray dataSet = new BytesArray(); + + if ((this.label != null) && (this.data != null)) { + dataSet.add(BEGINNING_PATTERN); + dataSet.addAll(this.label.getBytes()); + + dataSet.add(SEPARATOR); + dataSet.addAll(this.data.getBytes()); + + dataSet.add(SEPARATOR); + dataSet.add(this.checksum); + + dataSet.add(END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public JSONObject toJSON() { + return this.toJSON(TICFrame.TRIMMED); + } + + @Override + public JSONObject toJSON(int option) { + JSONObject jsonObject = new JSONObject(); + + if ((option & TICFrame.NOCHECKSUM) > 0) { + jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); + } else { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_CHECKSUM, this.checksum); + jsonObject.put(new String(this.label.getBytes()), content); + } + + return jsonObject; + } } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java index d50abe1..8efb236 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -9,65 +9,51 @@ import enedis.lab.protocol.tic.frame.TICError; -/** - * TICException - * - */ -public class TICException extends Exception -{ - /** - * Unique identifier used for serialization - */ - private static final long serialVersionUID = -2780151361870269473L; - private TICError error; - - /** - * Constructor TICException - */ - public TICException() - { - super(); - this.reset(); - } - - /** - * Constructor TICException - * - * @param message - */ - public TICException(String message) - { - super(message); - this.error = TICError.TIC_READER_DEFAULT_ERROR; - } - - /** - * Constructor TICException - * - * @param message - * @param readerError - */ - public TICException(String message, TICError readerError) - { - super(message); - this.error = readerError; - } - - /** - * Reset TICError - */ - public void reset() - { - this.error = TICError.TIC_READER_DEFAULT_ERROR; - } - - /** - * Get error - * - * @return the readerError - */ - public TICError getError() - { - return this.error; - } +/** TICException */ +public class TICException extends Exception { + /** Unique identifier used for serialization */ + private static final long serialVersionUID = -2780151361870269473L; + + private TICError error; + + /** Constructor TICException */ + public TICException() { + super(); + this.reset(); + } + + /** + * Constructor TICException + * + * @param message + */ + public TICException(String message) { + super(message); + this.error = TICError.TIC_READER_DEFAULT_ERROR; + } + + /** + * Constructor TICException + * + * @param message + * @param readerError + */ + public TICException(String message, TICError readerError) { + super(message); + this.error = readerError; + } + + /** Reset TICError */ + public void reset() { + this.error = TICError.TIC_READER_DEFAULT_ERROR; + } + + /** + * Get error + * + * @return the readerError + */ + public TICError getError() { + return this.error; + } } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java index 1a2437a..8dc9928 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java @@ -7,186 +7,145 @@ package enedis.lab.protocol.tic.frame.standard; -import java.time.LocalDateTime; - import enedis.lab.protocol.tic.TICMode; import enedis.lab.protocol.tic.frame.TICFrame; +import java.time.LocalDateTime; -/** - * TIC frame standard - */ -public class TICFrameStandard extends TICFrame -{ - /** Begin info group */ - public static final byte INFOGROUP_BEGIN = 0x03; // LF - /** End info group */ - public static final byte INFOGROUP_END = 0x03; // CR - /** Sep info group */ - public static final byte INFOGROUP_SEP = 0x03; // HT - - /** Min size info group */ - public static final int INFOGROUP_MIN_SIZE = 7; // LF + Label + HT + Data + HT + - /** MIN_SIZE */ - public static final int MIN_SIZE = INFOGROUP_MIN_SIZE + 2; // ETX + INFGROUP_MIN_SIZE + STX - - /** - * Default constructor - */ - public TICFrameStandard() - { - super(); - } - - @Override - public TICFrameStandardDataSet addDataSet(String label, String data) - { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - - if (dataSet == null) - { - dataSet = new TICFrameStandardDataSet(); - dataSet.setLabel(label); - dataSet.setData(data); - dataSet.getConsistentChecksum(); - } - - else - { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - } - - this.DataSetList.add(dataSet); - - return dataSet; - } - - @Override - public TICFrameStandardDataSet addDataSet(int index, String label, String data) - { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - - if (dataSet == null) - { - dataSet = new TICFrameStandardDataSet(); - dataSet.setLabel(label); - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - if ((index >= 0) && (index < this.DataSetList.size())) - { - this.DataSetList.add(index, dataSet); - } - - else - { - this.DataSetList.add(dataSet); - } - } - - else - { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - this.DataSetList.remove(dataSet); - - if (index >= this.DataSetList.size()) - { - this.DataSetList.add(dataSet); - } - - else - { - this.DataSetList.add(index, dataSet); - } - } - - return dataSet; - } - - @Override - public TICMode getMode() - { - return TICMode.STANDARD; - } - - @Override - public String getData(String label) - { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - - if (dataSet != null) - { - if (dataSet.getLabel().equals("DATE")) - { - return dataSet.getDateTime(); - } - else - { - return dataSet.getData(); - } - } - - else - { - return null; - } - } - - /** - * Add data set - * - * @param label - * @param data - * @param dateTime - * @return the added data set - */ - public TICFrameStandardDataSet addDataSet(String label, String data, String dateTime) - { - TICFrameStandardDataSet dataSet = this.addDataSet(label, data); - - dataSet.setDateTime(dateTime); - - return dataSet; - } - - /** - * Get string datetime - * - * @param label - * @return string datetime - */ - public String getDateTime(String label) - { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - if (dataSet != null) - { - return dataSet.getDateTime(); - } - else - { - return null; - } - } - - /** - * Get date time as localDateTime - * - * @param label - * @return LocalDateTime - */ - public LocalDateTime getDateTimeAsLocalDateTime(String label) - { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - if (dataSet != null) - { - return dataSet.getDateTimeAsLocalDateTime(); - } - else - { - return null; - } - } - +/** TIC frame standard */ +public class TICFrameStandard extends TICFrame { + /** Begin info group */ + public static final byte INFOGROUP_BEGIN = 0x03; // LF + + /** End info group */ + public static final byte INFOGROUP_END = 0x03; // CR + + /** Sep info group */ + public static final byte INFOGROUP_SEP = 0x03; // HT + + /** Min size info group */ + public static final int INFOGROUP_MIN_SIZE = 7; // LF + Label + HT + Data + HT + + + /** MIN_SIZE */ + public static final int MIN_SIZE = INFOGROUP_MIN_SIZE + 2; // ETX + INFGROUP_MIN_SIZE + STX + + /** Default constructor */ + public TICFrameStandard() { + super(); + } + + @Override + public TICFrameStandardDataSet addDataSet(String label, String data) { + TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); + + if (dataSet == null) { + dataSet = new TICFrameStandardDataSet(); + dataSet.setLabel(label); + dataSet.setData(data); + dataSet.getConsistentChecksum(); + } else { + dataSet.setData(data); + dataSet.getConsistentChecksum(); + } + + this.DataSetList.add(dataSet); + + return dataSet; + } + + @Override + public TICFrameStandardDataSet addDataSet(int index, String label, String data) { + TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); + + if (dataSet == null) { + dataSet = new TICFrameStandardDataSet(); + dataSet.setLabel(label); + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + if ((index >= 0) && (index < this.DataSetList.size())) { + this.DataSetList.add(index, dataSet); + } else { + this.DataSetList.add(dataSet); + } + } else { + dataSet.setData(data); + dataSet.getConsistentChecksum(); + + this.DataSetList.remove(dataSet); + + if (index >= this.DataSetList.size()) { + this.DataSetList.add(dataSet); + } else { + this.DataSetList.add(index, dataSet); + } + } + + return dataSet; + } + + @Override + public TICMode getMode() { + return TICMode.STANDARD; + } + + @Override + public String getData(String label) { + TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); + + if (dataSet != null) { + if (dataSet.getLabel().equals("DATE")) { + return dataSet.getDateTime(); + } else { + return dataSet.getData(); + } + } else { + return null; + } + } + + /** + * Add data set + * + * @param label + * @param data + * @param dateTime + * @return the added data set + */ + public TICFrameStandardDataSet addDataSet(String label, String data, String dateTime) { + TICFrameStandardDataSet dataSet = this.addDataSet(label, data); + + dataSet.setDateTime(dateTime); + + return dataSet; + } + + /** + * Get string datetime + * + * @param label + * @return string datetime + */ + public String getDateTime(String label) { + TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); + if (dataSet != null) { + return dataSet.getDateTime(); + } else { + return null; + } + } + + /** + * Get date time as localDateTime + * + * @param label + * @return LocalDateTime + */ + public LocalDateTime getDateTimeAsLocalDateTime(String label) { + TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); + if (dataSet != null) { + return dataSet.getDateTimeAsLocalDateTime(); + } else { + return null; + } + } } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java index a41c06c..628a05f 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,291 +7,241 @@ package enedis.lab.protocol.tic.frame.standard; +import enedis.lab.protocol.tic.frame.TICFrame; +import enedis.lab.protocol.tic.frame.TICFrameDataSet; +import enedis.lab.types.BytesArray; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; - import org.json.JSONObject; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.types.BytesArray; - -/** - * TIC frame standard data set - */ -public class TICFrameStandardDataSet extends TICFrameDataSet -{ - /** Separator */ - public static final byte SEPARATOR = 0x09; // HT - - protected BytesArray dateTime = null; - - /** - * Constructor - */ - public TICFrameStandardDataSet() - { - super(); - this.init(); - } - - @Override - public Byte getConsistentChecksum() - { - Byte crc = 0; - byte[] labelByte; - byte[] dataByte; - byte[] dateByte; - - if ((this.label != null) && (this.data != null)) - { - labelByte = this.label.getBytes(); - for (int i = 0; i < labelByte.length; i++) - { - crc = this.computeUpdate(crc, labelByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - - if (this.dateTime != null) - { - dateByte = this.dateTime.getBytes(); - for (int i = 0; i < dateByte.length; i++) - { - crc = this.computeUpdate(crc, dateByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - } - - dataByte = this.data.getBytes(); - - for (int i = 0; i < dataByte.length; i++) - { - crc = this.computeUpdate(crc, dataByte[i]); - } - crc = this.computeUpdate(crc, SEPARATOR); - - return this.computeEnd(crc); - } - else - { - return null; - } - - } - - /** - * Compute update - * - * @param crc - * @param octet - * @return Byte crc compute after Update - */ - public Byte computeUpdate(Byte crc, byte octet) - { - return (byte) (crc + (octet & 0xff)); - } - - /** - * Compute end - * - * @param crc - * @return Byte crc compute after End - */ - public Byte computeEnd(Byte crc) - { - return (byte) ((crc & 0x3F) + 0x20); - } - - @Override - public byte[] getBytes() - { - BytesArray dataSet = new BytesArray(); - - if ((this.label != null) && (this.data != null)) - { - dataSet.add(BEGINNING_PATTERN); - dataSet.addAll(this.label.getBytes()); - - if (null != this.dateTime) - { - dataSet.add(SEPARATOR); - dataSet.addAll(this.dateTime.getBytes()); - } - - dataSet.add(SEPARATOR); - dataSet.addAll(this.data.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.add(this.checksum); - - dataSet.add(END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public JSONObject toJSON() - { - return this.toJSON(TICFrame.TRIMMED); - } - - @Override - public JSONObject toJSON(int option) - { - JSONObject jsonObject = new JSONObject(); - - if ((option & TICFrame.NOCHECKSUM) > 0) - { - if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) - { - jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); - } - - else - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); - jsonObject.put(new String(this.label.getBytes()), content); - } - } - - else - { - if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - - else - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - } - - return jsonObject; - } - - /** - * Setup - * - * @param label - * @param data - * @param dateTime - */ - public void setup(String label, String data, String dateTime) - { - this.setup(label.getBytes(), data.getBytes(), dateTime.getBytes()); - } - - /** - * Setup - * - * @param label - * @param data - * @param dateTime - */ - public void setup(byte[] label, byte[] data, byte[] dateTime) - { - super.setup(label, data); - this.dateTime = new BytesArray(dateTime); - } - - /** - * Get datetime - * - * @return datetime - */ - public String getDateTime() - { - return new String(this.dateTime.getBytes()); - } - - /** - * Check datetime - * - * @return result - */ - public Boolean checkDateTime() - { - if (this.dateTime == null) - { - return false; - } - return true; - } - - /** - * Get datetime as local datetime - * - * @return datetime as local datetime - */ - // DATE H190730100158 yyMMddHHmmss - public LocalDateTime getDateTimeAsLocalDateTime() - { - String strDateTime = this.getDateTime(); - LocalDateTime localDateTime = null; - - if (strDateTime != null && strDateTime.length() == 13) - { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMddHHmmss"); - try - { - localDateTime = LocalDateTime.parse(strDateTime.substring(1), formatter); - } - catch (DateTimeParseException e) - { - localDateTime = null; - } - } - - return localDateTime; - } - - /** - * Set date time - * - * @param dateTime - */ - public void setDateTime(String dateTime) - { - this.setDateTime(dateTime.getBytes()); - } - - /** - * Set datetime - * - * @param dateTime - */ - public void setDateTime(byte[] dateTime) - { - this.setDateTime(new BytesArray(dateTime)); - } - - /** - * Set date time - * - * @param dateTime - */ - public void setDateTime(BytesArray dateTime) - { - this.dateTime = dateTime; - this.setChecksum(); - } - - private void init() - { - this.dateTime = null; - } +/** TIC frame standard data set */ +public class TICFrameStandardDataSet extends TICFrameDataSet { + /** Separator */ + public static final byte SEPARATOR = 0x09; // HT + + protected BytesArray dateTime = null; + + /** Constructor */ + public TICFrameStandardDataSet() { + super(); + this.init(); + } + + @Override + public Byte getConsistentChecksum() { + Byte crc = 0; + byte[] labelByte; + byte[] dataByte; + byte[] dateByte; + + if ((this.label != null) && (this.data != null)) { + labelByte = this.label.getBytes(); + for (int i = 0; i < labelByte.length; i++) { + crc = this.computeUpdate(crc, labelByte[i]); + } + + crc = this.computeUpdate(crc, SEPARATOR); + + if (this.dateTime != null) { + dateByte = this.dateTime.getBytes(); + for (int i = 0; i < dateByte.length; i++) { + crc = this.computeUpdate(crc, dateByte[i]); + } + + crc = this.computeUpdate(crc, SEPARATOR); + } + + dataByte = this.data.getBytes(); + + for (int i = 0; i < dataByte.length; i++) { + crc = this.computeUpdate(crc, dataByte[i]); + } + crc = this.computeUpdate(crc, SEPARATOR); + + return this.computeEnd(crc); + } else { + return null; + } + } + + /** + * Compute update + * + * @param crc + * @param octet + * @return Byte crc compute after Update + */ + public Byte computeUpdate(Byte crc, byte octet) { + return (byte) (crc + (octet & 0xff)); + } + + /** + * Compute end + * + * @param crc + * @return Byte crc compute after End + */ + public Byte computeEnd(Byte crc) { + return (byte) ((crc & 0x3F) + 0x20); + } + + @Override + public byte[] getBytes() { + BytesArray dataSet = new BytesArray(); + + if ((this.label != null) && (this.data != null)) { + dataSet.add(BEGINNING_PATTERN); + dataSet.addAll(this.label.getBytes()); + + if (null != this.dateTime) { + dataSet.add(SEPARATOR); + dataSet.addAll(this.dateTime.getBytes()); + } + + dataSet.add(SEPARATOR); + dataSet.addAll(this.data.getBytes()); + + dataSet.add(SEPARATOR); + dataSet.add(this.checksum); + + dataSet.add(END_PATTERN); + } + + return dataSet.getBytes(); + } + + @Override + public JSONObject toJSON() { + return this.toJSON(TICFrame.TRIMMED); + } + + @Override + public JSONObject toJSON(int option) { + JSONObject jsonObject = new JSONObject(); + + if ((option & TICFrame.NOCHECKSUM) > 0) { + if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) { + jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); + } else { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); + jsonObject.put(new String(this.label.getBytes()), content); + } + } else { + if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_CHECKSUM, this.checksum); + jsonObject.put(new String(this.label.getBytes()), content); + } else { + JSONObject content = new JSONObject(); + content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); + content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); + content.put(TICFrame.KEY_CHECKSUM, this.checksum); + jsonObject.put(new String(this.label.getBytes()), content); + } + } + + return jsonObject; + } + + /** + * Setup + * + * @param label + * @param data + * @param dateTime + */ + public void setup(String label, String data, String dateTime) { + this.setup(label.getBytes(), data.getBytes(), dateTime.getBytes()); + } + + /** + * Setup + * + * @param label + * @param data + * @param dateTime + */ + public void setup(byte[] label, byte[] data, byte[] dateTime) { + super.setup(label, data); + this.dateTime = new BytesArray(dateTime); + } + + /** + * Get datetime + * + * @return datetime + */ + public String getDateTime() { + return new String(this.dateTime.getBytes()); + } + + /** + * Check datetime + * + * @return result + */ + public Boolean checkDateTime() { + if (this.dateTime == null) { + return false; + } + return true; + } + + /** + * Get datetime as local datetime + * + * @return datetime as local datetime + */ + // DATE H190730100158 yyMMddHHmmss + public LocalDateTime getDateTimeAsLocalDateTime() { + String strDateTime = this.getDateTime(); + LocalDateTime localDateTime = null; + + if (strDateTime != null && strDateTime.length() == 13) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMddHHmmss"); + try { + localDateTime = LocalDateTime.parse(strDateTime.substring(1), formatter); + } catch (DateTimeParseException e) { + localDateTime = null; + } + } + + return localDateTime; + } + + /** + * Set date time + * + * @param dateTime + */ + public void setDateTime(String dateTime) { + this.setDateTime(dateTime.getBytes()); + } + + /** + * Set datetime + * + * @param dateTime + */ + public void setDateTime(byte[] dateTime) { + this.setDateTime(new BytesArray(dateTime)); + } + + /** + * Set date time + * + * @param dateTime + */ + public void setDateTime(BytesArray dateTime) { + this.dateTime = dateTime; + this.setChecksum(); + } + + private void init() { + this.dateTime = null; + } } From 134b5eef140bc23568d4fc53e6b96aada42a812e Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Fri, 3 Oct 2025 18:57:35 +0200 Subject: [PATCH 24/59] chore: format enedis.lab.protocol package (suite) --- .../java/enedis/lab/protocol/tic/TICMode.java | 108 +++++---- .../codec/CodecTICFrameHistoricDataSet.java | 91 +++++++- .../tic/codec/CodecTICFrameStandard.java | 67 +++++- .../codec/CodecTICFrameStandardDataSet.java | 92 +++++++- .../lab/protocol/tic/codec/TICCodec.java | 201 +++++++++++----- .../tic/datastreams/TICInputStream.java | 119 +++++++++- .../datastreams/TICStreamConfiguration.java | 101 ++++++-- .../lab/protocol/tic/frame/TICError.java | 64 +++-- .../lab/protocol/tic/frame/TICFrame.java | 218 ++++++++++-------- .../protocol/tic/frame/TICFrameDataSet.java | 137 +++++++---- .../tic/frame/historic/TICFrameHistoric.java | 57 ++++- .../historic/TICFrameHistoricDataSet.java | 71 +++++- .../tic/frame/standard/TICException.java | 53 ++++- .../tic/frame/standard/TICFrameStandard.java | 108 +++++++-- .../standard/TICFrameStandardDataSet.java | 134 ++++++++--- 15 files changed, 1233 insertions(+), 388 deletions(-) diff --git a/src/main/java/enedis/lab/protocol/tic/TICMode.java b/src/main/java/enedis/lab/protocol/tic/TICMode.java index 5bbc299..b87b645 100644 --- a/src/main/java/enedis/lab/protocol/tic/TICMode.java +++ b/src/main/java/enedis/lab/protocol/tic/TICMode.java @@ -10,7 +10,20 @@ import java.util.Arrays; /** - * TIC mode used available + * Enumeration of available TIC modes and related utilities. + * + *

    This enum defines the supported TIC protocol modes (STANDARD, HISTORIC, AUTO, UNKNOWN) and provides + * utility methods for mode detection from frame or group buffers. It also defines protocol-specific + * separator and buffer start patterns. + * + *

    Key features: + *

      + *
    • Defines all supported TIC modes
    • + *
    • Provides methods to detect mode from frame or group buffers
    • + *
    • Defines protocol-specific separators and buffer start patterns
    • + *
    + * + * @author Enedis Smarties team */ import enedis.lab.protocol.tic.frame.TICError; @@ -20,55 +33,57 @@ * TIC Mode * */ -public enum TICMode -{ - /** Unknown mode */ + +public enum TICMode { + /** Unknown mode. */ UNKNOWN, - /** Standard mode */ + /** Standard mode. */ STANDARD, - /** Historic mode */ + /** Historic mode. */ HISTORIC, - /** Auto mode */ + /** Auto-detect mode. */ AUTO; - /** Historic separator */ - public static final char HISTORIC_SEPARATOR = ' '; - /** Standard separator */ - public static final char STANDARD_SEPARATOR = '\t'; - /** Historic buffer start */ - public static final byte[] HISTORIC_BUFFER_START = { 2, 10, 65, 68, 67, 79 }; - /** Standard buffer start */ - public static final byte[] STANDARD_BUFFER_START = { 2, 10, 65, 68, 83, 67 }; + /** + * Separator character (space, 0x20) for historic TIC frames. + */ + public static final char HISTORIC_SEPARATOR = ' '; + /** + * Separator character (tab, 0x09) for standard TIC frames. + */ + public static final char STANDARD_SEPARATOR = '\t'; + /** + * Buffer start pattern for historic TIC frames. + */ + public static final byte[] HISTORIC_BUFFER_START = { 2, 10, 65, 68, 67, 79 }; + /** + * Buffer start pattern for standard TIC frames. + */ + public static final byte[] STANDARD_BUFFER_START = { 2, 10, 65, 68, 83, 67 }; /** - * Find mode from Frame Buffer + * Detects the TIC mode from the given frame buffer. * - * @param frameBuffer - * @return TICMode - * @throws TICException + * @param frameBuffer the byte array containing the frame start + * @return the detected {@link TICMode}, or null if not recognized + * @throws TICException if the buffer is too short or invalid */ - public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) throws TICException - { + public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) throws TICException { byte[] frameBufferStart = new byte[HISTORIC_BUFFER_START.length]; - if (frameBuffer.length < frameBufferStart.length) - { - throw new TICException("Tic frame read 0x" + bytesToHex(frameBuffer) + " too short to determine TIC Mode !", + if (frameBuffer.length < frameBufferStart.length) { + throw new TICException( + "Tic frame read 0x" + bytesToHex(frameBuffer) + " too short to determine TIC Mode !", TICError.TIC_READER_FRAME_DECODE_FAILED); } System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); - if (Arrays.equals(frameBufferStart, HISTORIC_BUFFER_START)) - { + if (Arrays.equals(frameBufferStart, HISTORIC_BUFFER_START)) { return HISTORIC; - } - else - { - if (STANDARD_BUFFER_START.length != HISTORIC_BUFFER_START.length) - { + } else { + if (STANDARD_BUFFER_START.length != HISTORIC_BUFFER_START.length) { frameBufferStart = new byte[STANDARD_BUFFER_START.length]; System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); } - if (Arrays.equals(frameBufferStart, STANDARD_BUFFER_START)) - { + if (Arrays.equals(frameBufferStart, STANDARD_BUFFER_START)) { return STANDARD; } return null; @@ -76,21 +91,16 @@ public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) throws TICExce } /** - * Find Mode frome Group Buffer + * Detects the TIC mode from the given group buffer. * - * @param groupBuffer - * @return TICMode + * @param groupBuffer the byte array containing the group data + * @return the detected {@link TICMode}, or null if not recognized */ - public static TICMode findModeFromGroupBuffer(byte[] groupBuffer) - { - for (int i = 0; i < groupBuffer.length; i++) - { - if (groupBuffer[i] == HISTORIC_SEPARATOR) - { + public static TICMode findModeFromGroupBuffer(byte[] groupBuffer) { + for (int i = 0; i < groupBuffer.length; i++) { + if (groupBuffer[i] == HISTORIC_SEPARATOR) { return HISTORIC; - } - else if (groupBuffer[i] == STANDARD_SEPARATOR) - { + } else if (groupBuffer[i] == STANDARD_SEPARATOR) { return STANDARD; } } @@ -98,11 +108,11 @@ else if (groupBuffer[i] == STANDARD_SEPARATOR) } /** - * Convert byte array to hexadecimal string (replacement for - * DatatypeConverter.printHexBinary) - * + * Converts a byte array to a hexadecimal string (replacement for + * DatatypeConverter.printHexBinary). + * * @param bytes the byte array to convert - * @return hexadecimal string representation + * @return the hexadecimal string representation */ private static String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java index 069efb3..d87e4eb 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java @@ -14,9 +14,48 @@ import enedis.lab.types.BytesArray; import java.util.List; -/** Codec TIC Frame Historic Data Set */ +/** + * Codec for encoding and decoding TIC historic frame data sets. + * + *

    + * This class implements the {@link Codec} interface to provide serialization + * and deserialization + * of {@link TICFrameHistoricDataSet} objects to and from their byte array + * representation, according + * to the historic TIC protocol format. It ensures the correct structure, + * delimiters, and checksum + * validation for each data set. + * + *

    + * Main features: + *

      + *
    • Encodes a {@link TICFrameHistoricDataSet} into a byte array with proper + * delimiters, separators, and checksum.
    • + *
    • Decodes a byte array into a {@link TICFrameHistoricDataSet}, validating + * structure and checksum.
    • + *
    • Handles TIC historic data set format: + * LF LABEL SP DATA SP CHECKSUM CR.
    • + *
    • Throws {@link CodecException} on invalid format or checksum.
    • + *
    + * + * @author Enedis Smarties team + * @see TICFrameHistoricDataSet + * @see CodecTICFrameHistoric + * @see Codec + * @see CodecException + */ public class CodecTICFrameHistoricDataSet implements Codec { + /** + * Encode a TICFrameHistoricDataSet into its byte array representation. + * + *

    + * Format: LF LABEL SP DATA SP CHECKSUM CR + * + * @param ticFrameHistoricDataSet the data set to encode + * @return the encoded byte array + * @throws CodecException if the data set is invalid or checksum is incorrect + */ @Override public byte[] encode(TICFrameHistoricDataSet ticFrameHistoricDataSet) throws CodecException { BytesArray dataSet = new BytesArray(); @@ -41,6 +80,16 @@ public byte[] encode(TICFrameHistoricDataSet ticFrameHistoricDataSet) throws Cod return dataSet.getBytes(); } + /** + * Decode a byte array into a TICFrameHistoricDataSet. + * + *

    + * Validates delimiters, structure, and checksum. + * + * @param bytes the byte array to decode + * @return the decoded TICFrameHistoricDataSet + * @throws CodecException if the format or checksum is invalid + */ @Override public TICFrameHistoricDataSet decode(byte[] bytes) throws CodecException { BytesArray bytesArray = new BytesArray(bytes); @@ -76,17 +125,35 @@ public TICFrameHistoricDataSet decode(byte[] bytes) throws CodecException { return dataSet; } + /** + * Checks if the byte array starts and ends with the expected TIC delimiters. + * + * @param bytes the byte array to check + * @return true if delimiters are present, false otherwise + */ private boolean isStartStopDelimiterPresent(BytesArray bytes) { return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); } + /** + * Removes the start and stop delimiters from the byte array (LF and CR). + * + * @param bytes the byte array to modify + */ private void removeStartStopDelimiter(BytesArray bytes) { bytes.remove(0); bytes.remove(bytes.size() - 1); } + /** + * Initializes a TICFrameHistoricDataSet from label, data, and checksum parts. + * + * @param parts the list of BytesArray: label, data, checksum + * @param dataSet the data set to initialize + * @return the initialized TICFrameHistoricDataSet + */ private TICFrameHistoricDataSet createDataSetLabelDataChecksum( List parts, TICFrameHistoricDataSet dataSet) { @@ -95,14 +162,34 @@ private TICFrameHistoricDataSet createDataSetLabelDataChecksum( return dataSet; } + /** + * Checks if the checksum part is exactly one byte. + * + * @param part the BytesArray to check + * @return true if the part size is 1, false otherwise + */ private boolean isChecksumInOneByte(BytesArray part) { return part.size() == 1; } + /** + * Checks if the split frame has the expected structure: label, data, checksum. + * + * @param parts the list of BytesArray + * @return true if the structure matches, false otherwise + */ private boolean isStructureLabelDataChecksum(List parts) { return parts.size() == 3; } + /** + * Splits the frame into its parts (label, data, checksum) using the TIC + * separator. + * + * @param bytesArray the byte array to split + * @return a list of BytesArray: label, data, checksum + * @throws CodecException if the frame is too short or has an invalid format + */ private List splitFrame(BytesArray bytesArray) throws CodecException { List parts; @@ -116,7 +203,7 @@ private List splitFrame(BytesArray bytesArray) throws CodecException throw new CodecException("Invalid format of TICFrameHistoricDataSet"); } BytesArray checksum = parts.get(parts.size() - 1); - checksum.addAll(new byte[] {TICFrameHistoricDataSet.SEPARATOR}); + checksum.addAll(new byte[] { TICFrameHistoricDataSet.SEPARATOR }); } else { parts = bytesArray.split(TICFrameHistoricDataSet.SEPARATOR); } diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java index c07fd08..e3f1ebb 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java @@ -16,8 +16,48 @@ import enedis.lab.types.BytesArray; import java.util.List; -/** Codec TIC Frame Standard */ +/** + * Codec for encoding and decoding TIC standard frame data sets. + * + *

    + * This class implements the {@link Codec} interface to provide serialization + * and deserialization + * of {@link TICFrameStandard} objects to and from their byte array + * representation, according + * to the standard TIC protocol format. It ensures the correct structure, + * delimiters, and checksum + * validation for each data set in a frame. + * + *

    + * Main features: + *

      + *
    • Encodes a {@link TICFrameStandard} into a byte array with proper + * delimiters, separators, and checksum.
    • + *
    • Decodes a byte array into a {@link TICFrameStandard}, validating + * structure and checksum of each data set.
    • + *
    • Handles TIC standard data set format, including support for multiple data + * sets per frame.
    • + *
    • Throws {@link CodecException} on invalid format or checksum.
    • + *
    + * + * @author Enedis Smarties team + * @see TICFrameStandard + * @see TICFrameStandardDataSet + * @see CodecTICFrameStandardDataSet + * @see Codec + * @see CodecException + */ public class CodecTICFrameStandard implements Codec { + /** + * Decode a byte array into a TICFrameStandard. + * + *

    + * Validates delimiters, structure, and checksum of each data set. + * + * @param bytes the byte array to decode + * @return the decoded TICFrameStandard + * @throws CodecException if the format or checksum is invalid + */ @Override public TICFrameStandard decode(byte[] bytes) throws CodecException { String errorMessage = ""; @@ -39,14 +79,14 @@ public TICFrameStandard decode(byte[] bytes) throws CodecException { // Isolate each memory area supposed to correspond to an Information Group // (DataSet) - List datasetList = - bytesArray.slice( - TICFrameDataSet.BEGINNING_PATTERN, - TICFrameDataSet.END_PATTERN, - BytesArray.CONTIGUOUS); + List datasetList = bytesArray.slice( + TICFrameDataSet.BEGINNING_PATTERN, + TICFrameDataSet.END_PATTERN, + BytesArray.CONTIGUOUS); // Analyze each memory area to extract the controls of the Group of information - // If the format of a zone is invalid, then the browse is interrupted and the whole frame is + // If the format of a zone is invalid, then the browse is interrupted and the + // whole frame is // invalid // (null) if (datasetList.isEmpty() == false) { @@ -58,8 +98,7 @@ public TICFrameStandard decode(byte[] bytes) throws CodecException { dataSet = codecTICFrameStandardDataSet.decode(rawDataSetByte); ticFrame.addDataSet(dataSet); } catch (CodecException exception) { - errorMessage += - exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; + errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; } } } @@ -72,6 +111,16 @@ public TICFrameStandard decode(byte[] bytes) throws CodecException { } } + /** + * Encode a TICFrameStandard into its byte array representation. + * + *

    + * Format: LF [DataSet1] ... [DataSetN] CR + * + * @param ticFrameStandard the frame to encode + * @return the encoded byte array, or null if the frame is empty + * @throws CodecException if a data set is invalid or checksum is incorrect + */ @Override public byte[] encode(TICFrameStandard ticFrameStandard) throws CodecException { CodecTICFrameStandardDataSet codec = new CodecTICFrameStandardDataSet(); diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java index 05f0b30..941c220 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java @@ -14,8 +14,42 @@ import enedis.lab.types.BytesArray; import java.util.List; -/** Codec TIC Frame Standard Data Set */ +/** + * Codec for encoding and decoding TIC standard frame data sets. + * + *

    + * This class implements the {@link Codec} interface to provide serialization + * and deserialization + * of {@link TICFrameStandardDataSet} objects to and from their byte array + * representation, according + * to the standard TIC protocol format. + * + *

    + * Main features: + *

      + *
    • Encodes a {@link TICFrameStandardDataSet} into a byte array with proper + * delimiters, separators, and checksum.
    • + *
    • Decodes a byte array into a {@link TICFrameStandardDataSet}, validating + * structure and checksum.
    • + *
    • Handles both "LABEL/DATA/CHECKSUM" and "LABEL/DATETIME/DATA/CHECKSUM" + * formats.
    • + *
    • Throws {@link CodecException} on invalid format or checksum.
    • + *
    + * + * @author Enedis Smarties team + * @see TICFrameStandardDataSet + * @see CodecTICFrameStandard + * @see Codec + * @see CodecException + */ public class CodecTICFrameStandardDataSet implements Codec { + /** + * Encode a TICFrameStandardDataSet into its byte array representation. + * + * @param ticFrameStandardDataSet the data set to encode + * @return the encoded byte array + * @throws CodecException if the data set is invalid or checksum is incorrect + */ @Override public byte[] encode(TICFrameStandardDataSet ticFrameStandardDataSet) throws CodecException { BytesArray dataSet = new BytesArray(); @@ -45,6 +79,17 @@ public byte[] encode(TICFrameStandardDataSet ticFrameStandardDataSet) throws Cod return dataSet.getBytes(); } + /** + * Decode a byte array into a TICFrameStandardDataSet. + * + *

    + * Supports both classic and datetime-extended formats. Validates delimiters, + * structure, and checksum. + * + * @param bytes the byte array to decode + * @return the decoded TICFrameStandardDataSet + * @throws CodecException if the format or checksum is invalid + */ @Override public TICFrameStandardDataSet decode(byte[] bytes) throws CodecException { BytesArray bytesArray = new BytesArray(bytes); @@ -88,28 +133,65 @@ else if (this.isStructureLabelDateTimeDataChecksum(parts)) { return dataSet; } + /** + * Checks if the split frame has the expected structure: label, datetime, data, + * checksum. + * + * @param parts the list of BytesArray + * @return true if the structure matches, false otherwise + */ private boolean isStructureLabelDateTimeDataChecksum(List parts) { return parts.size() == 4; } + /** + * Checks if the split frame has the expected structure: label, data, checksum. + * + * @param parts the list of BytesArray + * @return true if the structure matches, false otherwise + */ private boolean isStructureLabelDataChecksum(List parts) { return parts.size() == 3; } + /** + * Checks if the checksum part is exactly one byte. + * + * @param part the BytesArray to check + * @return true if the part size is 1, false otherwise + */ private boolean isChecksumInOneByte(BytesArray part) { return part.size() == 1; } + /** + * Removes the start and stop delimiters from the byte array (LF and CR). + * + * @param bytes the byte array to modify + */ private void removeStartStopDelimiter(BytesArray bytes) { bytes.remove(0); bytes.remove(bytes.size() - 1); } + /** + * Checks if the byte array starts and ends with the expected TIC delimiters. + * + * @param bytes the byte array to check + * @return true if delimiters are present, false otherwise + */ private boolean isStartStopDelimiterPresent(BytesArray bytes) { return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); } + /** + * Initializes a TICFrameStandardDataSet from label, data, and checksum parts. + * + * @param parts the list of BytesArray: label, data, checksum + * @param dataSet the data set to initialize + * @return the initialized TICFrameStandardDataSet + */ private TICFrameStandardDataSet createDataSetLabelDataChecksum( List parts, TICFrameStandardDataSet dataSet) { dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); @@ -117,6 +199,14 @@ private TICFrameStandardDataSet createDataSetLabelDataChecksum( return dataSet; } + /** + * Initializes a TICFrameStandardDataSet from label, datetime, data, and + * checksum parts. + * + * @param parts the list of BytesArray: label, datetime, data, checksum + * @param dataSet the data set to initialize + * @return the initialized TICFrameStandardDataSet + */ private TICFrameStandardDataSet createDataSetLabelDatetimeDataChecksum( List parts, TICFrameStandardDataSet dataSet) { dataSet.setup(parts.get(0).getBytes(), parts.get(2).getBytes(), parts.get(1).getBytes()); diff --git a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java index 543e108..cc2d4e8 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java @@ -16,7 +16,39 @@ import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; import enedis.lab.types.BytesArray; -/** Codec TIC */ +/** + * Main codec for encoding and decoding TIC frames (standard and historic). + * + *

    + * This class implements the {@link Codec} interface to provide serialization + * and deserialization + * of {@link TICFrame} objects to and from their byte array representation, + * supporting both standard + * and historic TIC protocol formats. It delegates the actual encoding/decoding + * to the appropriate + * sub-codec depending on the frame mode. + * + *

    + * Main features: + *

      + *
    • Encodes and decode both {@link TICFrameStandard} and + * {@link TICFrameHistoric} frames.
    • + *
    • Automatically detects the TIC mode (standard/historic/auto) when + * decoding.
    • + *
    • Maintains an internal buffer and mode state for incremental + * operations.
    • + *
    • Throws {@link CodecException} on invalid format or checksum.
    • + *
    + * + * @author Enedis Smarties team + * @see TICFrame + * @see TICFrameStandard + * @see TICFrameHistoric + * @see CodecTICFrameStandard + * @see CodecTICFrameHistoric + * @see Codec + * @see CodecException + */ public class TICCodec implements Codec { private static final TICMode DEFAULT_MODE = TICMode.STANDARD; @@ -24,16 +56,41 @@ public class TICCodec implements Codec { private TICMode mode = TICMode.UNKNOWN; private TICMode currentMode = TICMode.UNKNOWN; - /** Default constructor */ + /** + * Constructs a new TICCodec with default mode (AUTO) and empty buffer. + * + *

    + * Initializes the codec to auto-detect TIC mode and prepares the internal + * buffer for decoding operations. + */ public TICCodec() { + /** + * Current TIC mode (STANDARD, HISTORIC, or AUTO for auto-detection). + */ this.mode = TICMode.UNKNOWN; this.currentMode = TICMode.UNKNOWN; this.Buffer = new BytesArray(); } + /** + * Decodes a byte array into a TICFrame (standard or historic). + * + *

    + * Automatically detects the TIC mode if set to AUTO, and delegates decoding to + * the appropriate codec. + * + * @param newData the byte array containing the TIC frame data + * @return the decoded {@link TICFrame} (either {@link TICFrameStandard} or + * {@link TICFrameHistoric}) + * @throws CodecException if the data is invalid, the mode cannot be determined, + * or decoding fails + */ @Override public TICFrame decode(byte[] newData) throws CodecException { CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); + /** + * Codec for standard TIC frames (historic). + */ CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); TICFrameStandard ticFrameStandard = null; TICFrameHistoric ticFrameHistoric = null; @@ -42,44 +99,46 @@ public TICFrame decode(byte[] newData) throws CodecException { TICMode currentTICMode = null; try { + /** + * Internal buffer for accumulating incoming bytes during decoding. + */ switch (this.currentMode) { - case STANDARD: - { + case STANDARD: { + ticFrameStandard = codecStandard.decode(newData); + ticFrame = ticFrameStandard; + break; + } + case HISTORIC: { + ticFrameHistoric = codecHistoric.decode(newData); + ticFrame = ticFrameHistoric; + break; + } + + case AUTO: { + try { + currentTICMode = TICMode.findModeFromFrameBuffer(newData); + } catch (TICException exception) { + /** + * Codec for historic TIC frames (historic). + */ + throw new CodecException("can't determinated TIC Mode"); + } + + if (currentTICMode == TICMode.STANDARD) { ticFrameStandard = codecStandard.decode(newData); ticFrame = ticFrameStandard; break; - } - case HISTORIC: - { + } else if (currentTICMode == TICMode.HISTORIC) { ticFrameHistoric = codecHistoric.decode(newData); ticFrame = ticFrameHistoric; - break; + } else { + throw new CodecException("can't decode TIC, unable to find TIC Modem"); } + } - case AUTO: - { - try { - currentTICMode = TICMode.findModeFromFrameBuffer(newData); - } catch (TICException exception) { - throw new CodecException("can't determinated TIC Mode"); - } - - if (currentTICMode == TICMode.STANDARD) { - ticFrameStandard = codecStandard.decode(newData); - ticFrame = ticFrameStandard; - break; - } else if (currentTICMode == TICMode.HISTORIC) { - ticFrameHistoric = codecHistoric.decode(newData); - ticFrame = ticFrameHistoric; - } else { - throw new CodecException("can't decode TIC, unable to find TIC Modem"); - } - } - - default: - { - /**/ - } + default: { + /**/ + } } } catch (CodecException exception) { throw new CodecException(exception.getMessage(), exception.getData()); @@ -87,6 +146,17 @@ public TICFrame decode(byte[] newData) throws CodecException { return ticFrame; } + /** + * Encodes a TICFrame (standard or historic) into a byte array. + * + *

    + * Delegates encoding to the appropriate codec based on the frame's mode. + * + * @param ticFrame the TIC frame to encode (must be {@link TICFrameStandard} or + * {@link TICFrameHistoric}) + * @return the encoded byte array representing the TIC frame + * @throws CodecException if encoding fails or the frame type is unsupported + */ @Override public byte[] encode(TICFrame ticFrame) throws CodecException { CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); @@ -96,21 +166,18 @@ public byte[] encode(TICFrame ticFrame) throws CodecException { try { switch (ticFrame.getMode()) { - case STANDARD: - { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } - case HISTORIC: - { - bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); - break; - } - default: - { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } + case STANDARD: { + bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); + break; + } + case HISTORIC: { + bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); + break; + } + default: { + bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); + break; + } } } catch (CodecException exception) { throw new CodecException("Can't encode TICFrame" + exception.getMessage()); @@ -118,51 +185,61 @@ public byte[] encode(TICFrame ticFrame) throws CodecException { return bytesBuffer; } - /** Reset buffer */ + /** + * Resets the internal buffer, clearing any accumulated data. + * + *

    + * Should be called before starting a new decoding operation or when reusing the + * codec. + */ public void reset() { this.Buffer.clear(); } /** - * Append data to buffer + * Appends a byte array to the internal buffer. * - * @param data + * @param data the byte array to append */ public void append(byte[] data) { this.Buffer.addAll(data); } /** - * Append data to buffer + * Appends a single byte to the internal buffer. * - * @param data + * @param data the byte to append */ public void append(byte data) { this.Buffer.add(data); } /** - * Append data to buffer + * Appends the contents of a {@link BytesArray} to the internal buffer. * - * @param data + * @param data the {@link BytesArray} to append */ public void append(BytesArray data) { this.Buffer.addAll(data.getBytes()); } /** - * Get mode + * Returns the configured TIC mode (STANDARD, HISTORIC, or AUTO). * - * @return mode + * @return the current configured {@link TICMode} */ public TICMode getMode() { return this.mode; } /** - * Set mode + * Sets the TIC mode (STANDARD, HISTORIC, or AUTO). + * + *

    + * If set to AUTO, the codec will attempt to auto-detect the mode during + * decoding. * - * @param mode + * @param mode the TIC mode to set */ public void setMode(TICMode mode) { if (this.mode != mode) { @@ -177,18 +254,18 @@ public void setMode(TICMode mode) { } /** - * Get current mode + * Returns the currently active TIC mode (used for decoding). * - * @return current mode + * @return the current active {@link TICMode} */ public TICMode getCurrentMode() { return this.currentMode; } /** - * Set current mode + * Sets the currently active TIC mode (used for decoding). * - * @param currentMode + * @param currentMode the TIC mode to set as active */ public void setCurrentMode(TICMode currentMode) { if (currentMode != this.currentMode) { diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java index 15a1a9e..c2cc85b 100644 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java +++ b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java @@ -20,24 +20,63 @@ import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; -/** TIC input stream */ +/** + * Data input stream for TIC (Teleinformation Client) frames. + * + *

    + * This class extends {@link DataInputStream} to provide decoding and event + * handling for TIC frames + * received from a configured channel. It uses a {@link TICCodec} to decode raw + * byte arrays into + * {@link TICFrame} objects and exposes them as {@link DataDictionary} + * instances. The stream supports + * error handling, event notification, and mode management for both standard and + * historic TIC protocols. + * + *

    + * Key features: + *

      + *
    • Decodes incoming TIC frames using {@link TICCodec}
    • + *
    • Notifies subscribers on new data or errors
    • + *
    • Supports both standard and historic TIC modes
    • + *
    • Provides access to the current TIC mode
    • + *
    + * + * @author Enedis Smarties team + * @see TICCodec + * @see TICFrame + * @see DataInputStream + * @see DataDictionary + */ public class TICInputStream extends DataInputStream { - /** Key timestamp */ + /** + * Key for the timestamp field in the decoded data dictionary. + */ public static final String KEY_TIMESTAMP = "timestamp"; - /** Key channel */ + /** + * Key for the channel name field in the decoded data dictionary. + */ public static final String KEY_CHANNEL = "channel"; - /** Key data */ + /** + * Key for the TIC frame data field in the decoded data dictionary. + */ public static final String KEY_DATA = "data"; + /** + * Codec used to decode TIC frames from byte arrays. + */ protected TICCodec codec; /** - * Constructor using configuration + * Constructs a new TICInputStream with the specified configuration. + * + *

    + * Initializes the codec and sets the TIC mode according to the configuration. * - * @param configuration - * @throws DataStreamException + * @param configuration the TIC stream configuration + * @throws DataStreamException if the configuration is invalid */ public TICInputStream(TICStreamConfiguration configuration) throws DataStreamException { super(configuration); @@ -45,16 +84,43 @@ public TICInputStream(TICStreamConfiguration configuration) throws DataStreamExc this.codec.setCurrentMode(configuration.getTicMode()); } + /** + * Reads a TIC frame from the input stream and returns it as a + * {@link DataDictionary}. + * + *

    + * This method should be implemented to provide actual reading logic. Currently + * returns null. + * + * @return the decoded TIC frame as a {@link DataDictionary}, or null if not + * implemented + * @throws DataStreamException if a read error occurs + */ @Override public DataDictionary read() throws DataStreamException { return null; } + /** + * Returns the type of this data stream (TIC). + * + * @return {@link DataStreamType#TIC} + */ @Override public DataStreamType getType() { return DataStreamType.TIC; } + /** + * Handles incoming data read from the channel. + * + *

    + * Decodes the TIC frame and notifies subscribers. If decoding fails, notifies + * error subscribers. + * + * @param channelName the name of the channel + * @param data the raw byte array received + */ @Override public void onDataRead(String channelName, byte[] data) { if (!this.notifier.getSubscribers().isEmpty()) { @@ -109,16 +175,37 @@ public void onDataRead(String channelName, byte[] data) { } } + /** + * Not used. TICInputStream does not handle data written events. + * + * @param channelName the name of the channel + * @param data the data written + */ @Override public void onDataWritten(String channelName, byte[] data) { // Not used } + /** + * Handles error events detected on the channel (without error data). + * + * @param channelName the name of the channel + * @param errorCode the error code + * @param errorMessage the error message + */ @Override public void onErrorDetected(String channelName, int errorCode, String errorMessage) { this.notifyOnErrorDetected(errorCode, errorMessage, null); } + /** + * Handles error events detected on the channel (with error data). + * + * @param channelName the name of the channel + * @param errorCode the error code + * @param errorMessage the error message + * @param errorData additional error data + */ @Override public void onErrorDetected( String channelName, int errorCode, String errorMessage, DataDictionary errorData) { @@ -126,16 +213,28 @@ public void onErrorDetected( } /** - * Get Mode + * Returns the current TIC mode of the underlying channel. * - * @return TIC Mode + * @return the current {@link TICMode} */ public TICMode getMode() { ChannelTICSerialPort channelTIC = (ChannelTICSerialPort) this.channel; - return channelTIC.getMode(); } + /** + * Decodes a raw TIC frame byte array into a {@link DataDictionary}. + * + *

    + * Uses the internal {@link TICCodec} to decode the frame, adds timestamp and + * channel info. + * + * @param data the raw TIC frame bytes + * @return a {@link DataDictionary} containing the decoded frame, timestamp, and + * channel name + * @throws DataDictionaryException if the data dictionary cannot be created + * @throws CodecException if decoding fails + */ protected DataDictionary decodeTICFrame(byte[] data) throws DataDictionaryException, CodecException { TICFrame ticFrame; diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java index b201d36..eb27b6e 100644 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java +++ b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java @@ -21,23 +21,67 @@ import java.util.Map; /** - * TICStreamConfiguration class + * Configuration class for TIC data streams. * - *

    Generated + *

    + * This class extends {@link DataStreamConfiguration} to provide configuration + * management + * for TIC data streams, including TIC mode selection, type, direction, and + * channel name. + * It supports construction from maps, data dictionaries, or direct parameters, + * and ensures + * that all required configuration keys are present and valid for TIC + * input/output streams. + * + *

    + * Key features: + *

      + *
    • Supports both standard and historic TIC modes
    • + *
    • Validates configuration parameters for TIC data streams
    • + *
    • Provides accessors for TIC mode and other stream properties
    • + *
    + * + * @author Enedis Smarties team + * @see DataStreamConfiguration + * @see TICMode */ public class TICStreamConfiguration extends DataStreamConfiguration { + /** + * Key for the TIC mode parameter in the configuration. + */ protected static final String KEY_TIC_MODE = "ticMode"; + /** + * Accepted value for the stream type (TIC). + */ private static final DataStreamType TYPE_ACCEPTED_VALUE = DataStreamType.TIC; + + /** + * Accepted values for the stream direction (INPUT or OUTPUT). + */ private static final DataStreamDirection[] DIRECTION_ACCEPTED_VALUES = { - DataStreamDirection.OUTPUT, DataStreamDirection.INPUT + DataStreamDirection.OUTPUT, DataStreamDirection.INPUT }; + + /** + * Default value for the TIC mode (AUTO). + */ private static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; + /** + * List of key descriptors for configuration validation. + */ private List> keys = new ArrayList>(); + /** + * Key descriptor for the TIC mode parameter. + */ protected KeyDescriptorEnum kTicMode; + /** + * Protected default constructor. Initializes key descriptors and sets default + * values. + */ protected TICStreamConfiguration() { super(); this.loadKeyDescriptors(); @@ -48,10 +92,10 @@ protected TICStreamConfiguration() { } /** - * Constructor using map + * Constructs a TICStreamConfiguration from a map of parameters. * - * @param map - * @throws DataDictionaryException + * @param map the map containing configuration parameters + * @throws DataDictionaryException if the configuration is invalid */ public TICStreamConfiguration(Map map) throws DataDictionaryException { this(); @@ -59,10 +103,10 @@ public TICStreamConfiguration(Map map) throws DataDictionaryExce } /** - * Constructor using datadictionary + * Constructs a TICStreamConfiguration from a {@link DataDictionary}. * - * @param other - * @throws DataDictionaryException + * @param other the data dictionary containing configuration parameters + * @throws DataDictionaryException if the configuration is invalid */ public TICStreamConfiguration(DataDictionary other) throws DataDictionaryException { this(); @@ -70,7 +114,8 @@ public TICStreamConfiguration(DataDictionary other) throws DataDictionaryExcepti } /** - * Constructor setting configuration name/file and parameters to default values + * Constructs a TICStreamConfiguration with the specified name and file, using + * default parameters. * * @param name the configuration name * @param file the configuration file @@ -81,13 +126,13 @@ public TICStreamConfiguration(String name, File file) { } /** - * Constructor setting parameters to specific values + * Constructs a TICStreamConfiguration with the specified parameters. * - * @param name - * @param direction - * @param channelName - * @param ticMode - * @throws DataDictionaryException + * @param name the configuration name + * @param direction the data stream direction (INPUT or OUTPUT) + * @param channelName the channel name + * @param ticMode the TIC mode (STANDARD, HISTORIC, or AUTO) + * @throws DataDictionaryException if the configuration is invalid */ public TICStreamConfiguration( String name, DataStreamDirection direction, String channelName, TICMode ticMode) @@ -102,6 +147,11 @@ public TICStreamConfiguration( this.checkAndUpdate(); } + /** + * Updates optional configuration parameters to their default values if not set. + * + * @throws DataDictionaryException if an error occurs during update + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_TYPE)) { @@ -114,28 +164,37 @@ protected void updateOptionalParameters() throws DataDictionaryException { } /** - * Get tic mode + * Returns the configured TIC mode for this stream. * - * @return the tic mode + * @return the TIC mode (STANDARD, HISTORIC, or AUTO) */ public TICMode getTicMode() { return (TICMode) this.data.get(KEY_TIC_MODE); } /** - * Set tic mode + * Sets the TIC mode for this stream. * - * @param ticMode - * @throws DataDictionaryException + * @param ticMode the TIC mode to set (STANDARD, HISTORIC, or AUTO) + * @throws DataDictionaryException if the value is invalid */ public void setTicMode(TICMode ticMode) throws DataDictionaryException { this.setTicMode((Object) ticMode); } + /** + * Sets the TIC mode for this stream using a generic object. + * + * @param ticMode the TIC mode as an object + * @throws DataDictionaryException if the value is invalid + */ protected void setTicMode(Object ticMode) throws DataDictionaryException { this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); } + /** + * Loads key descriptors for configuration validation and type conversion. + */ private void loadKeyDescriptors() { try { this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java index a2ed66a..6f69d64 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java @@ -9,54 +9,82 @@ import jssc.SerialPortException; -/** TICClient errors definition */ +/** + * Enumeration of error codes for TIC frame and serial port operations. + * + *

    + * This enum defines error codes for serial port issues and TIC frame processing + * errors. It provides + * a mapping from {@link SerialPortException} types to TIC error codes for + * unified error handling. + * + *

    + * Key features: + *

      + *
    • Defines error codes for serial port and TIC frame errors
    • + *
    • Provides a method to map {@link SerialPortException} to TICError
    • + *
    • Each error code has an associated integer value
    • + *
    + * + * @author Enedis Smarties team + * @see SerialPortException + */ public enum TICError { - /** No error */ + /** No error. */ NO_ERROR(0), - /** Serial port not found */ + /** Serial port not found. */ SERIAL_PORT_NOT_FOUND(17), - /** Serial port not found */ + /** Incorrect serial port. */ SERIAL_PORT_INCORRECT_SERIAL_PORT(18), - /** Serial port not found */ + /** Null not permitted for serial port. */ SERIAL_PORT_NULL_NOT_PERMITTED(19), - /** Serial port not found */ + /** Serial port already opened. */ SERIAL_PORT_ALREADY_OPENED(20), - /** Serial port not found */ + /** Serial port is busy. */ SERIAL_PORT_SERIAL_PORT_BUSY(21), - /** Serial port not found */ + /** Serial port configuration failure. */ SERIAL_PORT_CONFIGURATION_FAILURE(22), - /** TIC reader default error */ + /** Default error for TIC reader. */ TIC_READER_DEFAULT_ERROR(23), - /** TIC reader label name not found */ + /** TIC reader label name not found. */ TIC_READER_LABEL_NAME_NOT_FOUND(24), - /** TIC reader frame decode failed */ + /** TIC reader frame decode failed. */ TIC_READER_FRAME_DECODE_FAILED(25), - /** TIC reader read frame timeout */ + /** TIC reader read frame timeout. */ TIC_READER_READ_FRAME_TIMEOUT(26), - /** TIC reader read frame with checksum invalid */ + /** TIC reader read frame with checksum invalid. */ TIC_READER_READ_FRAME_CHECKSUM_INVALID(27), ; + /** + * Integer value associated with the error code. + */ private int value; + /** + * Constructs a TICError with the specified integer value. + * + * @param value the integer value for the error code + */ private TICError(int value) { this.value = value; } /** - * Get error value + * Returns the integer value associated with this error code. * - * @return the value + * @return the error code value */ public int getValue() { return this.value; } /** - * Get serial port error + * Maps a {@link SerialPortException} to the corresponding {@link TICError} + * code. * - * @param serialPortException - * @return TICError + * @param serialPortException the serial port exception to map + * @return the corresponding {@link TICError}, or null if not recognized */ public static TICError getSerialPortError(SerialPortException serialPortException) { if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) { diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java index 307a934..8929791 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java @@ -17,57 +17,80 @@ import java.util.Objects; import org.json.JSONObject; -/** TICFrame */ +/** + * Abstract base class for TIC frames. + * + *

    + * This class provides the structure and common operations for TIC frames, + * including data set + * management, byte serialization, and data dictionary conversion. Subclasses + * implement protocol-specific + * details for standard and historic TIC formats. + * + *

    + * Key features: + *

      + *
    • Defines constants for frame delimiters and options
    • + *
    • Manages a list of labeled data sets
    • + *
    • Provides methods for adding, removing, and retrieving data sets
    • + *
    • Supports conversion to byte arrays and data dictionaries
    • + *
    • Abstract methods for protocol-specific data set and mode handling
    • + *
    + * + * @author Enedis Smarties team + * @see TICFrameDataSet + * @see TICMode + */ public abstract class TICFrame { - /** Beginning pattern */ + /** + * Frame start delimiter (STX, 0x02). + */ public static final byte BEGINNING_PATTERN = 0x02; // STX - /** End pattern */ + /** + * Frame end delimiter (ETX, 0x03). + */ public static final byte END_PATTERN = 0x03; // ETX - /** End of trame pattern */ + /** + * End of transmission delimiter (EOT, 0x04). + */ public static final byte EOT = 0x04; // EOT - // Options - /** No specific options */ + // Frame options and keys + /** No specific options. */ public static final int EXPLICIT = 0x0000; - - /** No checksum options */ + /** Hide checksums in data dictionary. */ public static final int NOCHECKSUM = 0x0001; - - /** No date time options */ + /** Hide date/time fields in data dictionary. */ public static final int NODATETIME = 0x0002; - - /** No tic mode options */ + /** Hide TIC mode in data dictionary. */ public static final int NOTICMODE = 0x0004; - - /** No invalid options */ + /** Filter invalid data sets in data dictionary. */ public static final int NOINVALID = 0x0008; - - /** Trimmed options */ + /** Trimmed options: hide checksum, date/time, and invalid data sets. */ public static final int TRIMMED = NOCHECKSUM | NODATETIME | NOINVALID; - - /** Key tic frame */ + /** Key for TIC frame in data dictionary. */ public static final String KEY_TICFRAME = "ticFrame"; - - /** Key tic mode */ + /** Key for TIC mode in data dictionary. */ public static final String KEY_TICMODE = "ticMode"; - - /** Key label */ + /** Key for label in data dictionary. */ public static final String KEY_LABEL = "label"; - - /** Key data */ + /** Key for data in data dictionary. */ public static final String KEY_DATA = "data"; - - /** Key checksum */ + /** Key for checksum in data dictionary. */ public static final String KEY_CHECKSUM = "checksum"; - - /** Key date time */ + /** Key for date/time in data dictionary. */ public static final String KEY_DATETIME = "dateTime"; + /** + * List of data sets contained in this TIC frame. + */ protected List DataSetList; - /** Default constructor */ + /** + * Constructs an empty TIC frame with an empty data set list. + */ public TICFrame() { this.DataSetList = new ArrayList(); } @@ -79,36 +102,39 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (this.getClass() != obj.getClass()) return false; + if (this == obj) + return true; + if (obj == null) + return false; + if (this.getClass() != obj.getClass()) + return false; TICFrame other = (TICFrame) obj; return Objects.equals(this.DataSetList, other.DataSetList); } /** - * Return the list of DataSet + * Returns the list of data sets contained in this frame. * - * @return the list of DataSet + * @return the list of data sets */ public List getDataSetList() { return this.DataSetList; } /** - * Set the list of DataSet + * Sets the list of data sets for this frame. * - * @param dataSetList + * @param dataSetList the list of data sets to set */ public void setDataSetList(List dataSetList) { this.DataSetList = dataSetList; } /** - * Return the index of the given DataSet + * Returns the index of the data set with the given label. * - * @param label Label of the DataSet - * @return the index of the given DataSet or '-1' if it does not exist + * @param label the label of the data set + * @return the index of the data set, or -1 if not found */ public int indexOf(String label) { int index = -1; @@ -124,10 +150,10 @@ public int indexOf(String label) { } /** - * Return the given DataSet + * Returns the data set with the given label, or null if not found. * - * @param label Label of the DataSet - * @return the given DataSet or 'null' if it does not exist + * @param label the label of the data set + * @return the data set, or null if not found */ public TICFrameDataSet getDataSet(String label) { TICFrameDataSet dataSet = null; @@ -143,10 +169,11 @@ public TICFrameDataSet getDataSet(String label) { } /** - * Return the data field of the given DataSet + * Returns the data value for the data set with the given label, or null if not + * found. * - * @param label Label of the DataSet - * @return the data field of the given DataSet + * @param label the label of the data set + * @return the data value, or null if not found */ public String getData(String label) { TICFrameDataSet dataSet = this.getDataSet(label); @@ -159,10 +186,11 @@ public String getData(String label) { } /** - * Add the dataSet at the end of the list. If the dataSet already existed, its data and checksum - * are just set + * Adds the given data set to the end of the list. If a data set with the same + * label exists, + * its data and checksum are updated. * - * @param dataSet + * @param dataSet the data set to add or update */ public void addDataSet(TICFrameDataSet dataSet) { TICFrameDataSet oldDataSet = this.getDataSet(dataSet.getLabel()); @@ -176,11 +204,12 @@ public void addDataSet(TICFrameDataSet dataSet) { } /** - * Add the dataSet at the given index of the list. If the dataSet already existed, its data and - * checksum are set and it is moved at the given index + * Adds the given data set at the specified index. If a data set with the same + * label exists, + * its data and checksum are updated and it is moved to the new index. * - * @param index - * @param dataSet + * @param index the position to insert the data set + * @param dataSet the data set to add or update */ public void addDataSet(int index, TICFrameDataSet dataSet) { int dataSetIndex = this.indexOf(dataSet.getLabel()); @@ -199,30 +228,31 @@ public void addDataSet(int index, TICFrameDataSet dataSet) { } /** - * Add data set + * Adds a new data set with the given label and data to the frame. * - * @param label - * @param data - * @return tic frame data set + * @param label the label for the data set + * @param data the data value + * @return the created or updated data set */ public abstract TICFrameDataSet addDataSet(String label, String data); /** - * Add data set + * Adds a new data set with the given label and data at the specified index in + * the frame. * - * @param index - * @param label - * @param data - * @return tic frame data set + * @param index the position to insert the data set + * @param label the label for the data set + * @param data the data value + * @return the created or updated data set */ public abstract TICFrameDataSet addDataSet(int index, String label, String data); /** - * Move the DataSet at the given index. + * Moves the data set with the given label to the specified index. * - * @param label - * @param index - * @return true if the operation has been done, else return false + * @param label the label of the data set to move + * @param index the new index for the data set + * @return true if the operation succeeded, false otherwise */ public boolean moveDataSet(String label, int index) { int previous_index = this.indexOf(label); @@ -253,11 +283,11 @@ public boolean moveDataSet(String label, int index) { } /** - * Set data set + * Sets the data value for the data set with the given label. * - * @param label - * @param data - * @return true if succeed + * @param label the label of the data set + * @param data the new data value + * @return true if the data set was found and updated, false otherwise */ public boolean setDataSet(String label, String data) { TICFrameDataSet dataSet = this.getDataSet(label); @@ -273,12 +303,12 @@ public boolean setDataSet(String label, String data) { } /** - * Set data set + * Sets the data value and checksum for the data set with the given label. * - * @param label - * @param data - * @param checksum - * @return true if succeed + * @param label the label of the data set + * @param data the new data value + * @param checksum the new checksum value + * @return true if the data set was found and updated, false otherwise */ public boolean setDataSet(String label, String data, byte checksum) { TICFrameDataSet dataSet = this.getDataSet(label); @@ -294,10 +324,10 @@ public boolean setDataSet(String label, String data, byte checksum) { } /** - * Remove data set + * Removes the data set with the given label from the frame. * - * @param label - * @return tic frame data set + * @param label the label of the data set to remove + * @return the removed data set, or null if not found */ public TICFrameDataSet removeDataSet(String label) { TICFrameDataSet dataSet = null; @@ -312,10 +342,10 @@ public TICFrameDataSet removeDataSet(String label) { } /** - * Remove data set + * Removes the data set at the specified index from the frame. * - * @param index - * @return tic frame data set + * @param index the index of the data set to remove + * @return the removed data set, or null if not found */ public TICFrameDataSet removeDataSet(int index) { TICFrameDataSet dataSet = null; @@ -328,9 +358,9 @@ public TICFrameDataSet removeDataSet(int index) { } /** - * Get bytes + * Serializes this frame to a byte array according to the TIC protocol. * - * @return bytes + * @return the byte array representation of the frame */ public byte[] getBytes() { BytesArray bytesFrame = new BytesArray(); @@ -347,22 +377,26 @@ public byte[] getBytes() { } /** - * Get data dictionary + * Returns a data dictionary representation of this frame with default options. * - * @return data dictionary - * @throws DataDictionaryException + * @return the data dictionary + * @throws DataDictionaryException if conversion fails */ public DataDictionary getDataDictionary() throws DataDictionaryException { return this.getDataDictionary(0); } /** - * Return a DataDictionary object corresponding to the TIC frame + * Returns a data dictionary representation of this frame with the specified + * options. * - * @param options options to DataDictionary render (use like bits fields) : - EXPLICIT : frame is - * detailed - NOCHECKSUM : DataSet checksum are hidden - NODATETIME : DataSet dateTime fields - * are hidden - NOTICMODE : Tic mode is hidden; - TRIMMED : NOCHECKSUM | NODATETIME - - * FILTER_INVALID : Invalid DataSet are filtered + * @param options options to DataDictionary render (use like bits fields) : - + * EXPLICIT : frame is + * detailed - NOCHECKSUM : DataSet checksum are hidden - + * NODATETIME : DataSet dateTime fields + * are hidden - NOTICMODE : Tic mode is hidden; - TRIMMED : + * NOCHECKSUM | NODATETIME - + * FILTER_INVALID : Invalid DataSet are filtered * @return data dictionary * @throws DataDictionaryException */ @@ -398,9 +432,9 @@ public DataDictionary getDataDictionary(int options) throws DataDictionaryExcept } /** - * Get mode + * Returns the TIC mode for this frame (protocol-specific). * - * @return mode + * @return the TIC mode */ public abstract TICMode getMode(); } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java index 942f4e4..49a9efc 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java @@ -11,19 +11,55 @@ import java.util.Objects; import org.json.JSONObject; -/** TIC frame data set */ +/** + * Abstract base class for data sets in TIC frames. + * + *

    + * This class provides the structure and common operations for TIC frame data + * sets, including + * label/data management, checksum calculation, and serialization. Subclasses + * implement protocol-specific + * details for standard and historic TIC formats. + * + *

    + * Key features: + *

      + *
    • Defines constants for data set delimiters
    • + *
    • Manages label, data, and checksum fields
    • + *
    • Provides methods for setting and validating fields
    • + *
    • Abstract methods for protocol-specific serialization and checksum
    • + *
    + * + * @author Enedis Smarties team + * @see BytesArray + */ public abstract class TICFrameDataSet { - /** Beginning pattern */ + /** + * Data set start delimiter (LF, 0x0A). + */ public static final byte BEGINNING_PATTERN = 0x0A; // LF - /** End pattern */ + /** + * Data set end delimiter (CR, 0x0D). + */ public static final byte END_PATTERN = 0x0D; // CR + /** + * Label field for the data set. + */ protected BytesArray label = null; + /** + * Data field for the data set. + */ protected BytesArray data = null; + /** + * Checksum value for the data set. + */ protected byte checksum; - /** Default constructor */ + /** + * Constructs an empty TIC frame data set. + */ public TICFrameDataSet() { this.init(); } @@ -35,9 +71,12 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (this.getClass() != obj.getClass()) return false; + if (this == obj) + return true; + if (obj == null) + return false; + if (this.getClass() != obj.getClass()) + return false; TICFrameDataSet other = (TICFrameDataSet) obj; return this.checksum == other.checksum && Objects.equals(this.data, other.data) @@ -45,20 +84,20 @@ public boolean equals(Object obj) { } /** - * Setup from string label and data + * Sets up the data set with the given label and data strings. * - * @param label - * @param data + * @param label the label string + * @param data the data string */ public void setup(String label, String data) { this.setup(label.getBytes(), data.getBytes()); } /** - * Setup from byte[] label and data + * Sets up the data set with the given label and data byte arrays. * - * @param label - * @param data + * @param label the label bytes + * @param data the data bytes */ public void setup(byte[] label, byte[] data) { this.label = new BytesArray(label); @@ -66,36 +105,37 @@ public void setup(byte[] label, byte[] data) { } /** - * Get label + * Returns the label string for this data set. * - * @return label + * @return the label string */ public String getLabel() { return new String(this.label.getBytes()); } /** - * Set the Label + * Sets the label field from a string value. * - * @param label + * @param label the label string */ public void setLabel(String label) { this.setLabel(label.getBytes()); } /** - * Set the Label + * Sets the label field from a byte array value. * - * @param label + * @param label the label bytes */ public void setLabel(byte[] label) { this.setLabel(new BytesArray(label)); } /** - * Set the Label + * Sets the label field from a {@link BytesArray} value and updates the + * checksum. * - * @param label + * @param label the label as a {@link BytesArray} */ public void setLabel(BytesArray label) { this.label = label; @@ -103,36 +143,36 @@ public void setLabel(BytesArray label) { } /** - * Get data + * Returns the data string for this data set. * - * @return data + * @return the data string */ public String getData() { return new String(this.data.getBytes()); } /** - * Set data + * Sets the data field from a string value. * - * @param data + * @param data the data string */ public void setData(String data) { this.setData(data.getBytes()); } /** - * Set data + * Sets the data field from a byte array value. * - * @param data + * @param data the data bytes */ public void setData(byte[] data) { this.setData(new BytesArray(data)); } /** - * Set data + * Sets the data field from a {@link BytesArray} value and updates the checksum. * - * @param data + * @param data the data as a {@link BytesArray} */ public void setData(BytesArray data) { this.data = data; @@ -140,19 +180,19 @@ public void setData(BytesArray data) { } /** - * Get checksum + * Returns the checksum value for this data set. * - * @return checksum + * @return the checksum value */ public byte getChecksum() { - return this.checksum; } /** - * Compute and set the checksum (NB: Label and Data shall have been set) + * Computes and sets the checksum for this data set (label and data must be + * set). * - * @return true if the operation succeeds, else, return false + * @return true if the operation succeeds, false otherwise */ public boolean setChecksum() { Byte checksum = this.getConsistentChecksum(); @@ -166,16 +206,18 @@ public boolean setChecksum() { } /** - * Set the checksum at a given value + * Sets the checksum to the given value. * - * @param checksum + * @param checksum the checksum value to set */ public void setChecksum(byte checksum) { this.checksum = checksum; } /** - * @return true if fields are consistent with checksum + * Checks if the label, data, and checksum fields are consistent. + * + * @return true if the fields are consistent with the checksum, false otherwise */ public boolean isValid() { Byte consistentChecksum = this.getConsistentChecksum(); @@ -189,34 +231,37 @@ public boolean isValid() { } /** - * Get bytes + * Serializes this data set to a byte array according to the TIC protocol. * - * @return bytes + * @return the byte array representation of the data set */ public abstract byte[] getBytes(); /** - * Conver to JSON + * Converts this data set to a JSON object using the default options. * - * @return json object + * @return the JSON representation of the data set */ public abstract JSONObject toJSON(); /** - * Conver to JSON + * Converts this data set to a JSON object with the specified options. * - * @param option - * @return json object + * @param option bitmask of options (e.g., hide checksum, date/time) + * @return the JSON representation of the data set */ public abstract JSONObject toJSON(int option); /** - * Return the consistent checksum of the DataSet + * Computes the consistent checksum for this data set according to the protocol. * - * @return the consistent checksum of the DataSet + * @return the consistent checksum value, or null if not computable */ protected abstract Byte getConsistentChecksum(); + /** + * Initializes the data set, clearing label, data, and checksum fields. + */ private void init() { this.label = null; this.data = null; diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java index d2d7a1c..7053357 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java @@ -10,21 +10,67 @@ import enedis.lab.protocol.tic.TICMode; import enedis.lab.protocol.tic.frame.TICFrame; -/** TIC frame historic */ +/** + * TIC frame representation for historic TIC protocol. + * + *

    + * This class extends {@link TICFrame} to provide support for historic TIC + * frames, including + * separator definition and data set management. It allows adding labeled data + * sets to the frame + * and always reports its mode as {@link TICMode#HISTORIC}. + * + *

    + * Key features: + *

      + *
    • Defines the separator for historic TIC frames
    • + *
    • Supports adding labeled data sets at specific positions
    • + *
    • Always returns HISTORIC as the TIC mode
    • + *
    + * + * @author Enedis Smarties team + * @see TICFrame + * @see TICFrameHistoricDataSet + * @see TICMode + */ public class TICFrameHistoric extends TICFrame { - /** Separator */ + /** + * Separator character (space, 0x20) used in historic TIC frames. + */ public static final byte SEPARATOR = 0x20; // SP - /** Default constructor */ + /** + * Constructs an empty historic TIC frame. + */ public TICFrameHistoric() { super(); } + /** + * Adds a new data set with the given label and data to the end of the frame. + * + * @param label the label for the data set + * @param data the data value + * @return the created or updated {@link TICFrameHistoricDataSet} + */ @Override public TICFrameHistoricDataSet addDataSet(String label, String data) { return this.addDataSet(this.DataSetList.size(), label, data); } + /** + * Adds a new data set with the given label and data at the specified index in + * the frame. + * + *

    + * If a data set with the same label exists, it is updated and moved to the new + * index. + * + * @param index the position to insert the data set + * @param label the label for the data set + * @param data the data value + * @return the created or updated {@link TICFrameHistoricDataSet} + */ @Override public TICFrameHistoricDataSet addDataSet(int index, String label, String data) { TICFrameHistoricDataSet dataSet = (TICFrameHistoricDataSet) this.getDataSet(label); @@ -56,6 +102,11 @@ public TICFrameHistoricDataSet addDataSet(int index, String label, String data) return dataSet; } + /** + * Returns the TIC mode for this frame (always HISTORIC). + * + * @return {@link TICMode#HISTORIC} + */ @Override public TICMode getMode() { return TICMode.HISTORIC; diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java index 3e727fc..3b9e7b4 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java @@ -12,16 +12,50 @@ import enedis.lab.types.BytesArray; import org.json.JSONObject; -/** TIC frame historic data set */ +/** + * Data set representation for historic TIC frames. + * + *

    + * This class extends {@link TICFrameDataSet} to provide checksum calculation, + * byte serialization, + * and JSON conversion for historic TIC data sets. It defines the separator and + * implements the + * protocol-specific checksum logic. + * + *

    + * Key features: + *

      + *
    • Defines the separator for historic TIC data sets
    • + *
    • Implements checksum calculation for label and data
    • + *
    • Serializes the data set to bytes and JSON
    • + *
    + * + * @author Enedis Smarties team + * @see TICFrameDataSet + * @see TICFrame + */ public class TICFrameHistoricDataSet extends TICFrameDataSet { - /** Separator */ + /** + * Separator character (space, 0x20) used in historic TIC data sets. + */ public static final byte SEPARATOR = 0x20; // SP - /** Constructor */ + /** + * Constructs an empty historic TIC data set. + */ public TICFrameHistoricDataSet() { super(); } + /** + * Computes the consistent checksum for this data set according to the historic + * TIC protocol. + * + *

    + * The checksum is calculated over the label, separator, and data fields. + * + * @return the computed checksum, or null if label or data is missing + */ @Override public Byte getConsistentChecksum() { Byte crc = 0; @@ -47,26 +81,32 @@ public Byte getConsistentChecksum() { } /** - * compute Update + * Updates the checksum value with the given byte. * - * @param crc - * @param octet - * @return Byte crc after compute update + * @param crc the current checksum value + * @param octet the byte to add + * @return the updated checksum value */ public Byte computeUpdate(Byte crc, byte octet) { return (byte) (crc + (octet & 0xff)); } /** - * compute End + * Finalizes the checksum value according to the historic TIC protocol. * - * @param crc - * @return Byte crc after compute end + * @param crc the current checksum value + * @return the finalized checksum value */ public Byte computeEnd(Byte crc) { return (byte) ((crc & 0x3F) + 0x20); } + /** + * Serializes this data set to a byte array according to the historic TIC + * protocol. + * + * @return the byte array representation of the data set + */ @Override public byte[] getBytes() { BytesArray dataSet = new BytesArray(); @@ -87,11 +127,22 @@ public byte[] getBytes() { return dataSet.getBytes(); } + /** + * Converts this data set to a JSON object using the default options. + * + * @return the JSON representation of the data set + */ @Override public JSONObject toJSON() { return this.toJSON(TICFrame.TRIMMED); } + /** + * Converts this data set to a JSON object with the specified options. + * + * @param option bitmask of options (e.g., {@link TICFrame#NOCHECKSUM}) + * @return the JSON representation of the data set + */ @Override public JSONObject toJSON(int option) { JSONObject jsonObject = new JSONObject(); diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java index 8efb236..a4c4b15 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java @@ -9,23 +9,51 @@ import enedis.lab.protocol.tic.frame.TICError; -/** TICException */ +/** + * Exception class for errors encountered during TIC frame processing. + * + *

    + * This exception is thrown when a TIC frame cannot be parsed, validated, or + * processed correctly. + * It encapsulates a {@link TICError} code to provide additional context about + * the error type. + * + *

    + * Key features: + *

      + *
    • Associates a {@link TICError} with each exception instance
    • + *
    • Supports standard exception message and error code
    • + *
    • Provides a reset method to restore default error state
    • + *
    + * + * @author Enedis Smarties team + * @see TICError + * @see Exception + */ public class TICException extends Exception { - /** Unique identifier used for serialization */ + /** + * Unique identifier used for serialization. + */ private static final long serialVersionUID = -2780151361870269473L; + /** + * The TIC error code associated with this exception. + */ private TICError error; - /** Constructor TICException */ + /** + * Constructs a new TICException with default error code. + */ public TICException() { super(); this.reset(); } /** - * Constructor TICException + * Constructs a new TICException with the specified error message and default + * error code. * - * @param message + * @param message the detail message */ public TICException(String message) { super(message); @@ -33,25 +61,28 @@ public TICException(String message) { } /** - * Constructor TICException + * Constructs a new TICException with the specified error message and error + * code. * - * @param message - * @param readerError + * @param message the detail message + * @param readerError the TIC error code */ public TICException(String message, TICError readerError) { super(message); this.error = readerError; } - /** Reset TICError */ + /** + * Resets the error code to the default value. + */ public void reset() { this.error = TICError.TIC_READER_DEFAULT_ERROR; } /** - * Get error + * Returns the TIC error code associated with this exception. * - * @return the readerError + * @return the TIC error code */ public TICError getError() { return this.error; diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java index 8dc9928..bd7ad21 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java @@ -11,28 +11,74 @@ import enedis.lab.protocol.tic.frame.TICFrame; import java.time.LocalDateTime; -/** TIC frame standard */ +/** + * TIC frame representation for standard TIC protocol. + * + *

    + * This class extends {@link TICFrame} to provide support for standard TIC + * frames, including + * info group delimiters, data set management, and date/time handling. It allows + * adding labeled data sets + * and retrieving data or date/time values by label. Always reports its mode as + * {@link TICMode#STANDARD}. + * + *

    + * Key features: + *

      + *
    • Defines info group delimiters for standard TIC frames
    • + *
    • Supports adding and retrieving labeled data sets
    • + *
    • Handles date/time fields as strings or {@link LocalDateTime}
    • + *
    • Always returns STANDARD as the TIC mode
    • + *
    + * + * @author Enedis Smarties team + * @see TICFrame + * @see TICFrameStandardDataSet + * @see TICMode + */ public class TICFrameStandard extends TICFrame { - /** Begin info group */ + /** + * Info group begin delimiter (LF, 0x03) for standard TIC frames. + */ public static final byte INFOGROUP_BEGIN = 0x03; // LF - /** End info group */ + /** + * Info group end delimiter (CR, 0x03) for standard TIC frames. + */ public static final byte INFOGROUP_END = 0x03; // CR - /** Sep info group */ + /** + * Info group separator (HT, 0x03) for standard TIC frames. + */ public static final byte INFOGROUP_SEP = 0x03; // HT - /** Min size info group */ + /** + * Minimum size of an info group in bytes (LF + Label + HT + Data + HT + ...). + */ public static final int INFOGROUP_MIN_SIZE = 7; // LF + Label + HT + Data + HT + - /** MIN_SIZE */ + /** + * Minimum size of a standard TIC frame (info group + ETX + STX). + */ public static final int MIN_SIZE = INFOGROUP_MIN_SIZE + 2; // ETX + INFGROUP_MIN_SIZE + STX - /** Default constructor */ + /** + * Constructs an empty standard TIC frame. + */ public TICFrameStandard() { super(); } + /** + * Adds a new data set with the given label and data to the frame. + * + *

    + * If a data set with the same label exists, it is updated and added again. + * + * @param label the label for the data set + * @param data the data value + * @return the created or updated {@link TICFrameStandardDataSet} + */ @Override public TICFrameStandardDataSet addDataSet(String label, String data) { TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); @@ -52,6 +98,19 @@ public TICFrameStandardDataSet addDataSet(String label, String data) { return dataSet; } + /** + * Adds a new data set with the given label and data at the specified index in + * the frame. + * + *

    + * If a data set with the same label exists, it is updated and moved to the new + * index. + * + * @param index the position to insert the data set + * @param label the label for the data set + * @param data the data value + * @return the created or updated {@link TICFrameStandardDataSet} + */ @Override public TICFrameStandardDataSet addDataSet(int index, String label, String data) { TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); @@ -83,11 +142,23 @@ public TICFrameStandardDataSet addDataSet(int index, String label, String data) return dataSet; } + /** + * Returns the TIC mode for this frame (always STANDARD). + * + * @return {@link TICMode#STANDARD} + */ @Override public TICMode getMode() { return TICMode.STANDARD; } + /** + * Returns the data value for the given label, or the date/time if the label is + * "DATE". + * + * @param label the label to search for + * @return the data value or date/time string, or null if not found + */ @Override public String getData(String label) { TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); @@ -104,12 +175,12 @@ public String getData(String label) { } /** - * Add data set + * Adds a new data set with the given label, data, and date/time value. * - * @param label - * @param data - * @param dateTime - * @return the added data set + * @param label the label for the data set + * @param data the data value + * @param dateTime the date/time string + * @return the created or updated {@link TICFrameStandardDataSet} */ public TICFrameStandardDataSet addDataSet(String label, String data, String dateTime) { TICFrameStandardDataSet dataSet = this.addDataSet(label, data); @@ -120,10 +191,10 @@ public TICFrameStandardDataSet addDataSet(String label, String data, String date } /** - * Get string datetime + * Returns the date/time string for the given label, or null if not found. * - * @param label - * @return string datetime + * @param label the label to search for + * @return the date/time string, or null if not found */ public String getDateTime(String label) { TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); @@ -135,10 +206,11 @@ public String getDateTime(String label) { } /** - * Get date time as localDateTime + * Returns the date/time as a {@link LocalDateTime} for the given label, or null + * if not found. * - * @param label - * @return LocalDateTime + * @param label the label to search for + * @return the date/time as {@link LocalDateTime}, or null if not found */ public LocalDateTime getDateTimeAsLocalDateTime(String label) { TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java index 628a05f..ab4afa2 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java @@ -15,19 +15,59 @@ import java.time.format.DateTimeParseException; import org.json.JSONObject; -/** TIC frame standard data set */ +/** + * Data set representation for standard TIC frames. + * + *

    + * This class extends {@link TICFrameDataSet} to provide checksum calculation, + * byte serialization, + * date/time handling, and JSON conversion for standard TIC data sets. It + * defines the separator and + * implements the protocol-specific logic for standard frames. + * + *

    + * Key features: + *

      + *
    • Defines the separator for standard TIC data sets
    • + *
    • Implements checksum calculation for label, data, and optional + * date/time
    • + *
    • Handles date/time fields as strings or {@link LocalDateTime}
    • + *
    • Serializes the data set to bytes and JSON
    • + *
    + * + * @author Enedis Smarties team + * @see TICFrameDataSet + * @see TICFrame + */ public class TICFrameStandardDataSet extends TICFrameDataSet { - /** Separator */ + /** + * Separator character (horizontal tab, 0x09) used in standard TIC data sets. + */ public static final byte SEPARATOR = 0x09; // HT + /** + * Date/time field for the data set (optional, may be null). + */ protected BytesArray dateTime = null; - /** Constructor */ + /** + * Constructs an empty standard TIC data set. + */ public TICFrameStandardDataSet() { super(); this.init(); } + /** + * Computes the consistent checksum for this data set according to the standard + * TIC protocol. + * + *

    + * The checksum is calculated over the label, optional date/time, and data + * fields. + * + * @return the computed checksum, or null if label or data is missing + */ @Override public Byte getConsistentChecksum() { Byte crc = 0; @@ -66,26 +106,32 @@ public Byte getConsistentChecksum() { } /** - * Compute update + * Updates the checksum value with the given byte. * - * @param crc - * @param octet - * @return Byte crc compute after Update + * @param crc the current checksum value + * @param octet the byte to add + * @return the updated checksum value */ public Byte computeUpdate(Byte crc, byte octet) { return (byte) (crc + (octet & 0xff)); } /** - * Compute end + * Finalizes the checksum value according to the standard TIC protocol. * - * @param crc - * @return Byte crc compute after End + * @param crc the current checksum value + * @return the finalized checksum value */ public Byte computeEnd(Byte crc) { return (byte) ((crc & 0x3F) + 0x20); } + /** + * Serializes this data set to a byte array according to the standard TIC + * protocol. + * + * @return the byte array representation of the data set + */ @Override public byte[] getBytes() { BytesArray dataSet = new BytesArray(); @@ -111,11 +157,23 @@ public byte[] getBytes() { return dataSet.getBytes(); } + /** + * Converts this data set to a JSON object using the default options. + * + * @return the JSON representation of the data set + */ @Override public JSONObject toJSON() { return this.toJSON(TICFrame.TRIMMED); } + /** + * Converts this data set to a JSON object with the specified options. + * + * @param option bitmask of options (e.g., {@link TICFrame#NOCHECKSUM}, + * {@link TICFrame#NODATETIME}) + * @return the JSON representation of the data set + */ @Override public JSONObject toJSON(int option) { JSONObject jsonObject = new JSONObject(); @@ -148,22 +206,22 @@ public JSONObject toJSON(int option) { } /** - * Setup + * Sets up the data set with the given label, data, and date/time strings. * - * @param label - * @param data - * @param dateTime + * @param label the label string + * @param data the data string + * @param dateTime the date/time string */ public void setup(String label, String data, String dateTime) { this.setup(label.getBytes(), data.getBytes(), dateTime.getBytes()); } /** - * Setup + * Sets up the data set with the given label, data, and date/time byte arrays. * - * @param label - * @param data - * @param dateTime + * @param label the label bytes + * @param data the data bytes + * @param dateTime the date/time bytes */ public void setup(byte[] label, byte[] data, byte[] dateTime) { super.setup(label, data); @@ -171,32 +229,33 @@ public void setup(byte[] label, byte[] data, byte[] dateTime) { } /** - * Get datetime + * Returns the date/time string for this data set, or null if not set. * - * @return datetime + * @return the date/time string, or null if not set */ public String getDateTime() { - return new String(this.dateTime.getBytes()); + return this.dateTime == null ? null : new String(this.dateTime.getBytes()); } /** - * Check datetime + * Checks if the date/time field is set. * - * @return result + * @return true if date/time is set, false otherwise */ public Boolean checkDateTime() { - if (this.dateTime == null) { - return false; - } - return true; + return this.dateTime != null; } /** - * Get datetime as local datetime + * Returns the date/time as a {@link LocalDateTime} for this data set, or null + * if not set or invalid. + * + *

    + * The expected format is "HyyMMddHHmmss" (13 characters, with the first + * character ignored). * - * @return datetime as local datetime + * @return the date/time as {@link LocalDateTime}, or null if not set or invalid */ - // DATE H190730100158 yyMMddHHmmss public LocalDateTime getDateTimeAsLocalDateTime() { String strDateTime = this.getDateTime(); LocalDateTime localDateTime = null; @@ -214,33 +273,36 @@ public LocalDateTime getDateTimeAsLocalDateTime() { } /** - * Set date time + * Sets the date/time field from a string value. * - * @param dateTime + * @param dateTime the date/time string */ public void setDateTime(String dateTime) { this.setDateTime(dateTime.getBytes()); } /** - * Set datetime + * Sets the date/time field from a byte array value. * - * @param dateTime + * @param dateTime the date/time bytes */ public void setDateTime(byte[] dateTime) { this.setDateTime(new BytesArray(dateTime)); } /** - * Set date time + * Sets the date/time field from a {@link BytesArray} value. * - * @param dateTime + * @param dateTime the date/time as a {@link BytesArray} */ public void setDateTime(BytesArray dateTime) { this.dateTime = dateTime; this.setChecksum(); } + /** + * Initializes the data set, clearing the date/time field. + */ private void init() { this.dateTime = null; } From dd98ca91f5ba3d8a507c8c9a3d4726f0590cfdee Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Fri, 3 Oct 2025 18:59:10 +0200 Subject: [PATCH 25/59] chore: format with spotless --- .../codec/CodecTICFrameHistoricDataSet.java | 40 ++--- .../tic/codec/CodecTICFrameStandard.java | 45 +++-- .../codec/CodecTICFrameStandardDataSet.java | 39 ++--- .../lab/protocol/tic/codec/TICCodec.java | 156 ++++++++---------- .../tic/datastreams/TICInputStream.java | 83 ++++------ .../datastreams/TICStreamConfiguration.java | 67 +++----- .../lab/protocol/tic/frame/TICError.java | 25 ++- .../lab/protocol/tic/frame/TICFrame.java | 105 ++++++------ .../protocol/tic/frame/TICFrameDataSet.java | 70 +++----- .../tic/frame/historic/TICFrameHistoric.java | 38 ++--- .../historic/TICFrameHistoricDataSet.java | 38 ++--- .../tic/frame/standard/TICException.java | 41 ++--- .../tic/frame/standard/TICFrameStandard.java | 71 +++----- .../standard/TICFrameStandardDataSet.java | 70 +++----- 14 files changed, 349 insertions(+), 539 deletions(-) diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java index d87e4eb..ff8c40f 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java @@ -17,25 +17,20 @@ /** * Codec for encoding and decoding TIC historic frame data sets. * - *

    - * This class implements the {@link Codec} interface to provide serialization - * and deserialization - * of {@link TICFrameHistoricDataSet} objects to and from their byte array - * representation, according - * to the historic TIC protocol format. It ensures the correct structure, - * delimiters, and checksum + *

    This class implements the {@link Codec} interface to provide serialization and deserialization + * of {@link TICFrameHistoricDataSet} objects to and from their byte array representation, according + * to the historic TIC protocol format. It ensures the correct structure, delimiters, and checksum * validation for each data set. * - *

    - * Main features: + *

    Main features: + * *

      - *
    • Encodes a {@link TICFrameHistoricDataSet} into a byte array with proper - * delimiters, separators, and checksum.
    • - *
    • Decodes a byte array into a {@link TICFrameHistoricDataSet}, validating - * structure and checksum.
    • - *
    • Handles TIC historic data set format: - * LF LABEL SP DATA SP CHECKSUM CR.
    • - *
    • Throws {@link CodecException} on invalid format or checksum.
    • + *
    • Encodes a {@link TICFrameHistoricDataSet} into a byte array with proper delimiters, + * separators, and checksum. + *
    • Decodes a byte array into a {@link TICFrameHistoricDataSet}, validating structure and + * checksum. + *
    • Handles TIC historic data set format: LF LABEL SP DATA SP CHECKSUM CR. + *
    • Throws {@link CodecException} on invalid format or checksum. *
    * * @author Enedis Smarties team @@ -49,8 +44,7 @@ public class CodecTICFrameHistoricDataSet implements Codec - * Format: LF LABEL SP DATA SP CHECKSUM CR + *

    Format: LF LABEL SP DATA SP CHECKSUM CR * * @param ticFrameHistoricDataSet the data set to encode * @return the encoded byte array @@ -83,8 +77,7 @@ public byte[] encode(TICFrameHistoricDataSet ticFrameHistoricDataSet) throws Cod /** * Decode a byte array into a TICFrameHistoricDataSet. * - *

    - * Validates delimiters, structure, and checksum. + *

    Validates delimiters, structure, and checksum. * * @param bytes the byte array to decode * @return the decoded TICFrameHistoricDataSet @@ -150,7 +143,7 @@ private void removeStartStopDelimiter(BytesArray bytes) { /** * Initializes a TICFrameHistoricDataSet from label, data, and checksum parts. * - * @param parts the list of BytesArray: label, data, checksum + * @param parts the list of BytesArray: label, data, checksum * @param dataSet the data set to initialize * @return the initialized TICFrameHistoricDataSet */ @@ -183,8 +176,7 @@ private boolean isStructureLabelDataChecksum(List parts) { } /** - * Splits the frame into its parts (label, data, checksum) using the TIC - * separator. + * Splits the frame into its parts (label, data, checksum) using the TIC separator. * * @param bytesArray the byte array to split * @return a list of BytesArray: label, data, checksum @@ -203,7 +195,7 @@ private List splitFrame(BytesArray bytesArray) throws CodecException throw new CodecException("Invalid format of TICFrameHistoricDataSet"); } BytesArray checksum = parts.get(parts.size() - 1); - checksum.addAll(new byte[] { TICFrameHistoricDataSet.SEPARATOR }); + checksum.addAll(new byte[] {TICFrameHistoricDataSet.SEPARATOR}); } else { parts = bytesArray.split(TICFrameHistoricDataSet.SEPARATOR); } diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java index e3f1ebb..6a4f33c 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java @@ -19,25 +19,20 @@ /** * Codec for encoding and decoding TIC standard frame data sets. * - *

    - * This class implements the {@link Codec} interface to provide serialization - * and deserialization - * of {@link TICFrameStandard} objects to and from their byte array - * representation, according - * to the standard TIC protocol format. It ensures the correct structure, - * delimiters, and checksum + *

    This class implements the {@link Codec} interface to provide serialization and deserialization + * of {@link TICFrameStandard} objects to and from their byte array representation, according to the + * standard TIC protocol format. It ensures the correct structure, delimiters, and checksum * validation for each data set in a frame. * - *

    - * Main features: + *

    Main features: + * *

      - *
    • Encodes a {@link TICFrameStandard} into a byte array with proper - * delimiters, separators, and checksum.
    • - *
    • Decodes a byte array into a {@link TICFrameStandard}, validating - * structure and checksum of each data set.
    • - *
    • Handles TIC standard data set format, including support for multiple data - * sets per frame.
    • - *
    • Throws {@link CodecException} on invalid format or checksum.
    • + *
    • Encodes a {@link TICFrameStandard} into a byte array with proper delimiters, separators, + * and checksum. + *
    • Decodes a byte array into a {@link TICFrameStandard}, validating structure and checksum of + * each data set. + *
    • Handles TIC standard data set format, including support for multiple data sets per frame. + *
    • Throws {@link CodecException} on invalid format or checksum. *
    * * @author Enedis Smarties team @@ -51,8 +46,7 @@ public class CodecTICFrameStandard implements Codec { /** * Decode a byte array into a TICFrameStandard. * - *

    - * Validates delimiters, structure, and checksum of each data set. + *

    Validates delimiters, structure, and checksum of each data set. * * @param bytes the byte array to decode * @return the decoded TICFrameStandard @@ -79,10 +73,11 @@ public TICFrameStandard decode(byte[] bytes) throws CodecException { // Isolate each memory area supposed to correspond to an Information Group // (DataSet) - List datasetList = bytesArray.slice( - TICFrameDataSet.BEGINNING_PATTERN, - TICFrameDataSet.END_PATTERN, - BytesArray.CONTIGUOUS); + List datasetList = + bytesArray.slice( + TICFrameDataSet.BEGINNING_PATTERN, + TICFrameDataSet.END_PATTERN, + BytesArray.CONTIGUOUS); // Analyze each memory area to extract the controls of the Group of information // If the format of a zone is invalid, then the browse is interrupted and the @@ -98,7 +93,8 @@ public TICFrameStandard decode(byte[] bytes) throws CodecException { dataSet = codecTICFrameStandardDataSet.decode(rawDataSetByte); ticFrame.addDataSet(dataSet); } catch (CodecException exception) { - errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; + errorMessage += + exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; } } } @@ -114,8 +110,7 @@ public TICFrameStandard decode(byte[] bytes) throws CodecException { /** * Encode a TICFrameStandard into its byte array representation. * - *

    - * Format: LF [DataSet1] ... [DataSetN] CR + *

    Format: LF [DataSet1] ... [DataSetN] CR * * @param ticFrameStandard the frame to encode * @return the encoded byte array, or null if the frame is empty diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java index 941c220..03fd402 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java @@ -17,23 +17,19 @@ /** * Codec for encoding and decoding TIC standard frame data sets. * - *

    - * This class implements the {@link Codec} interface to provide serialization - * and deserialization - * of {@link TICFrameStandardDataSet} objects to and from their byte array - * representation, according + *

    This class implements the {@link Codec} interface to provide serialization and deserialization + * of {@link TICFrameStandardDataSet} objects to and from their byte array representation, according * to the standard TIC protocol format. * - *

    - * Main features: + *

    Main features: + * *

      - *
    • Encodes a {@link TICFrameStandardDataSet} into a byte array with proper - * delimiters, separators, and checksum.
    • - *
    • Decodes a byte array into a {@link TICFrameStandardDataSet}, validating - * structure and checksum.
    • - *
    • Handles both "LABEL/DATA/CHECKSUM" and "LABEL/DATETIME/DATA/CHECKSUM" - * formats.
    • - *
    • Throws {@link CodecException} on invalid format or checksum.
    • + *
    • Encodes a {@link TICFrameStandardDataSet} into a byte array with proper delimiters, + * separators, and checksum. + *
    • Decodes a byte array into a {@link TICFrameStandardDataSet}, validating structure and + * checksum. + *
    • Handles both "LABEL/DATA/CHECKSUM" and "LABEL/DATETIME/DATA/CHECKSUM" formats. + *
    • Throws {@link CodecException} on invalid format or checksum. *
    * * @author Enedis Smarties team @@ -82,9 +78,8 @@ public byte[] encode(TICFrameStandardDataSet ticFrameStandardDataSet) throws Cod /** * Decode a byte array into a TICFrameStandardDataSet. * - *

    - * Supports both classic and datetime-extended formats. Validates delimiters, - * structure, and checksum. + *

    Supports both classic and datetime-extended formats. Validates delimiters, structure, and + * checksum. * * @param bytes the byte array to decode * @return the decoded TICFrameStandardDataSet @@ -134,8 +129,7 @@ else if (this.isStructureLabelDateTimeDataChecksum(parts)) { } /** - * Checks if the split frame has the expected structure: label, datetime, data, - * checksum. + * Checks if the split frame has the expected structure: label, datetime, data, checksum. * * @param parts the list of BytesArray * @return true if the structure matches, false otherwise @@ -188,7 +182,7 @@ private boolean isStartStopDelimiterPresent(BytesArray bytes) { /** * Initializes a TICFrameStandardDataSet from label, data, and checksum parts. * - * @param parts the list of BytesArray: label, data, checksum + * @param parts the list of BytesArray: label, data, checksum * @param dataSet the data set to initialize * @return the initialized TICFrameStandardDataSet */ @@ -200,10 +194,9 @@ private TICFrameStandardDataSet createDataSetLabelDataChecksum( } /** - * Initializes a TICFrameStandardDataSet from label, datetime, data, and - * checksum parts. + * Initializes a TICFrameStandardDataSet from label, datetime, data, and checksum parts. * - * @param parts the list of BytesArray: label, datetime, data, checksum + * @param parts the list of BytesArray: label, datetime, data, checksum * @param dataSet the data set to initialize * @return the initialized TICFrameStandardDataSet */ diff --git a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java index cc2d4e8..fba799b 100644 --- a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java +++ b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java @@ -19,25 +19,18 @@ /** * Main codec for encoding and decoding TIC frames (standard and historic). * - *

    - * This class implements the {@link Codec} interface to provide serialization - * and deserialization - * of {@link TICFrame} objects to and from their byte array representation, - * supporting both standard - * and historic TIC protocol formats. It delegates the actual encoding/decoding - * to the appropriate + *

    This class implements the {@link Codec} interface to provide serialization and deserialization + * of {@link TICFrame} objects to and from their byte array representation, supporting both standard + * and historic TIC protocol formats. It delegates the actual encoding/decoding to the appropriate * sub-codec depending on the frame mode. * - *

    - * Main features: + *

    Main features: + * *

      - *
    • Encodes and decode both {@link TICFrameStandard} and - * {@link TICFrameHistoric} frames.
    • - *
    • Automatically detects the TIC mode (standard/historic/auto) when - * decoding.
    • - *
    • Maintains an internal buffer and mode state for incremental - * operations.
    • - *
    • Throws {@link CodecException} on invalid format or checksum.
    • + *
    • Encodes and decode both {@link TICFrameStandard} and {@link TICFrameHistoric} frames. + *
    • Automatically detects the TIC mode (standard/historic/auto) when decoding. + *
    • Maintains an internal buffer and mode state for incremental operations. + *
    • Throws {@link CodecException} on invalid format or checksum. *
    * * @author Enedis Smarties team @@ -59,14 +52,11 @@ public class TICCodec implements Codec { /** * Constructs a new TICCodec with default mode (AUTO) and empty buffer. * - *

    - * Initializes the codec to auto-detect TIC mode and prepares the internal - * buffer for decoding operations. + *

    Initializes the codec to auto-detect TIC mode and prepares the internal buffer for decoding + * operations. */ public TICCodec() { - /** - * Current TIC mode (STANDARD, HISTORIC, or AUTO for auto-detection). - */ + /** Current TIC mode (STANDARD, HISTORIC, or AUTO for auto-detection). */ this.mode = TICMode.UNKNOWN; this.currentMode = TICMode.UNKNOWN; this.Buffer = new BytesArray(); @@ -75,22 +65,18 @@ public TICCodec() { /** * Decodes a byte array into a TICFrame (standard or historic). * - *

    - * Automatically detects the TIC mode if set to AUTO, and delegates decoding to - * the appropriate codec. + *

    Automatically detects the TIC mode if set to AUTO, and delegates decoding to the appropriate + * codec. * * @param newData the byte array containing the TIC frame data - * @return the decoded {@link TICFrame} (either {@link TICFrameStandard} or - * {@link TICFrameHistoric}) - * @throws CodecException if the data is invalid, the mode cannot be determined, - * or decoding fails + * @return the decoded {@link TICFrame} (either {@link TICFrameStandard} or {@link + * TICFrameHistoric}) + * @throws CodecException if the data is invalid, the mode cannot be determined, or decoding fails */ @Override public TICFrame decode(byte[] newData) throws CodecException { CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); - /** - * Codec for standard TIC frames (historic). - */ + /** Codec for standard TIC frames (historic). */ CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); TICFrameStandard ticFrameStandard = null; TICFrameHistoric ticFrameHistoric = null; @@ -99,46 +85,46 @@ public TICFrame decode(byte[] newData) throws CodecException { TICMode currentTICMode = null; try { - /** - * Internal buffer for accumulating incoming bytes during decoding. - */ + /** Internal buffer for accumulating incoming bytes during decoding. */ switch (this.currentMode) { - case STANDARD: { - ticFrameStandard = codecStandard.decode(newData); - ticFrame = ticFrameStandard; - break; - } - case HISTORIC: { - ticFrameHistoric = codecHistoric.decode(newData); - ticFrame = ticFrameHistoric; - break; - } - - case AUTO: { - try { - currentTICMode = TICMode.findModeFromFrameBuffer(newData); - } catch (TICException exception) { - /** - * Codec for historic TIC frames (historic). - */ - throw new CodecException("can't determinated TIC Mode"); - } - - if (currentTICMode == TICMode.STANDARD) { + case STANDARD: + { ticFrameStandard = codecStandard.decode(newData); ticFrame = ticFrameStandard; break; - } else if (currentTICMode == TICMode.HISTORIC) { + } + case HISTORIC: + { ticFrameHistoric = codecHistoric.decode(newData); ticFrame = ticFrameHistoric; - } else { - throw new CodecException("can't decode TIC, unable to find TIC Modem"); + break; + } + + case AUTO: + { + try { + currentTICMode = TICMode.findModeFromFrameBuffer(newData); + } catch (TICException exception) { + /** Codec for historic TIC frames (historic). */ + throw new CodecException("can't determinated TIC Mode"); + } + + if (currentTICMode == TICMode.STANDARD) { + ticFrameStandard = codecStandard.decode(newData); + ticFrame = ticFrameStandard; + break; + } else if (currentTICMode == TICMode.HISTORIC) { + ticFrameHistoric = codecHistoric.decode(newData); + ticFrame = ticFrameHistoric; + } else { + throw new CodecException("can't decode TIC, unable to find TIC Modem"); + } } - } - default: { - /**/ - } + default: + { + /**/ + } } } catch (CodecException exception) { throw new CodecException(exception.getMessage(), exception.getData()); @@ -149,11 +135,10 @@ public TICFrame decode(byte[] newData) throws CodecException { /** * Encodes a TICFrame (standard or historic) into a byte array. * - *

    - * Delegates encoding to the appropriate codec based on the frame's mode. + *

    Delegates encoding to the appropriate codec based on the frame's mode. * - * @param ticFrame the TIC frame to encode (must be {@link TICFrameStandard} or - * {@link TICFrameHistoric}) + * @param ticFrame the TIC frame to encode (must be {@link TICFrameStandard} or {@link + * TICFrameHistoric}) * @return the encoded byte array representing the TIC frame * @throws CodecException if encoding fails or the frame type is unsupported */ @@ -166,18 +151,21 @@ public byte[] encode(TICFrame ticFrame) throws CodecException { try { switch (ticFrame.getMode()) { - case STANDARD: { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } - case HISTORIC: { - bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); - break; - } - default: { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } + case STANDARD: + { + bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); + break; + } + case HISTORIC: + { + bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); + break; + } + default: + { + bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); + break; + } } } catch (CodecException exception) { throw new CodecException("Can't encode TICFrame" + exception.getMessage()); @@ -188,9 +176,7 @@ public byte[] encode(TICFrame ticFrame) throws CodecException { /** * Resets the internal buffer, clearing any accumulated data. * - *

    - * Should be called before starting a new decoding operation or when reusing the - * codec. + *

    Should be called before starting a new decoding operation or when reusing the codec. */ public void reset() { this.Buffer.clear(); @@ -235,9 +221,7 @@ public TICMode getMode() { /** * Sets the TIC mode (STANDARD, HISTORIC, or AUTO). * - *

    - * If set to AUTO, the codec will attempt to auto-detect the mode during - * decoding. + *

    If set to AUTO, the codec will attempt to auto-detect the mode during decoding. * * @param mode the TIC mode to set */ diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java index c2cc85b..3c0f294 100644 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java +++ b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java @@ -23,23 +23,19 @@ /** * Data input stream for TIC (Teleinformation Client) frames. * - *

    - * This class extends {@link DataInputStream} to provide decoding and event - * handling for TIC frames - * received from a configured channel. It uses a {@link TICCodec} to decode raw - * byte arrays into - * {@link TICFrame} objects and exposes them as {@link DataDictionary} - * instances. The stream supports - * error handling, event notification, and mode management for both standard and - * historic TIC protocols. + *

    This class extends {@link DataInputStream} to provide decoding and event handling for TIC + * frames received from a configured channel. It uses a {@link TICCodec} to decode raw byte arrays + * into {@link TICFrame} objects and exposes them as {@link DataDictionary} instances. The stream + * supports error handling, event notification, and mode management for both standard and historic + * TIC protocols. + * + *

    Key features: * - *

    - * Key features: *

      - *
    • Decodes incoming TIC frames using {@link TICCodec}
    • - *
    • Notifies subscribers on new data or errors
    • - *
    • Supports both standard and historic TIC modes
    • - *
    • Provides access to the current TIC mode
    • + *
    • Decodes incoming TIC frames using {@link TICCodec} + *
    • Notifies subscribers on new data or errors + *
    • Supports both standard and historic TIC modes + *
    • Provides access to the current TIC mode *
    * * @author Enedis Smarties team @@ -49,31 +45,22 @@ * @see DataDictionary */ public class TICInputStream extends DataInputStream { - /** - * Key for the timestamp field in the decoded data dictionary. - */ + /** Key for the timestamp field in the decoded data dictionary. */ public static final String KEY_TIMESTAMP = "timestamp"; - /** - * Key for the channel name field in the decoded data dictionary. - */ + /** Key for the channel name field in the decoded data dictionary. */ public static final String KEY_CHANNEL = "channel"; - /** - * Key for the TIC frame data field in the decoded data dictionary. - */ + /** Key for the TIC frame data field in the decoded data dictionary. */ public static final String KEY_DATA = "data"; - /** - * Codec used to decode TIC frames from byte arrays. - */ + /** Codec used to decode TIC frames from byte arrays. */ protected TICCodec codec; /** * Constructs a new TICInputStream with the specified configuration. * - *

    - * Initializes the codec and sets the TIC mode according to the configuration. + *

    Initializes the codec and sets the TIC mode according to the configuration. * * @param configuration the TIC stream configuration * @throws DataStreamException if the configuration is invalid @@ -85,15 +72,11 @@ public TICInputStream(TICStreamConfiguration configuration) throws DataStreamExc } /** - * Reads a TIC frame from the input stream and returns it as a - * {@link DataDictionary}. + * Reads a TIC frame from the input stream and returns it as a {@link DataDictionary}. * - *

    - * This method should be implemented to provide actual reading logic. Currently - * returns null. + *

    This method should be implemented to provide actual reading logic. Currently returns null. * - * @return the decoded TIC frame as a {@link DataDictionary}, or null if not - * implemented + * @return the decoded TIC frame as a {@link DataDictionary}, or null if not implemented * @throws DataStreamException if a read error occurs */ @Override @@ -114,12 +97,11 @@ public DataStreamType getType() { /** * Handles incoming data read from the channel. * - *

    - * Decodes the TIC frame and notifies subscribers. If decoding fails, notifies - * error subscribers. + *

    Decodes the TIC frame and notifies subscribers. If decoding fails, notifies error + * subscribers. * * @param channelName the name of the channel - * @param data the raw byte array received + * @param data the raw byte array received */ @Override public void onDataRead(String channelName, byte[] data) { @@ -179,7 +161,7 @@ public void onDataRead(String channelName, byte[] data) { * Not used. TICInputStream does not handle data written events. * * @param channelName the name of the channel - * @param data the data written + * @param data the data written */ @Override public void onDataWritten(String channelName, byte[] data) { @@ -189,8 +171,8 @@ public void onDataWritten(String channelName, byte[] data) { /** * Handles error events detected on the channel (without error data). * - * @param channelName the name of the channel - * @param errorCode the error code + * @param channelName the name of the channel + * @param errorCode the error code * @param errorMessage the error message */ @Override @@ -201,10 +183,10 @@ public void onErrorDetected(String channelName, int errorCode, String errorMessa /** * Handles error events detected on the channel (with error data). * - * @param channelName the name of the channel - * @param errorCode the error code + * @param channelName the name of the channel + * @param errorCode the error code * @param errorMessage the error message - * @param errorData additional error data + * @param errorData additional error data */ @Override public void onErrorDetected( @@ -225,15 +207,12 @@ public TICMode getMode() { /** * Decodes a raw TIC frame byte array into a {@link DataDictionary}. * - *

    - * Uses the internal {@link TICCodec} to decode the frame, adds timestamp and - * channel info. + *

    Uses the internal {@link TICCodec} to decode the frame, adds timestamp and channel info. * * @param data the raw TIC frame bytes - * @return a {@link DataDictionary} containing the decoded frame, timestamp, and - * channel name + * @return a {@link DataDictionary} containing the decoded frame, timestamp, and channel name * @throws DataDictionaryException if the data dictionary cannot be created - * @throws CodecException if decoding fails + * @throws CodecException if decoding fails */ protected DataDictionary decodeTICFrame(byte[] data) throws DataDictionaryException, CodecException { diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java index eb27b6e..775dd1c 100644 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java +++ b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java @@ -23,22 +23,17 @@ /** * Configuration class for TIC data streams. * - *

    - * This class extends {@link DataStreamConfiguration} to provide configuration - * management - * for TIC data streams, including TIC mode selection, type, direction, and - * channel name. - * It supports construction from maps, data dictionaries, or direct parameters, - * and ensures - * that all required configuration keys are present and valid for TIC - * input/output streams. + *

    This class extends {@link DataStreamConfiguration} to provide configuration management for TIC + * data streams, including TIC mode selection, type, direction, and channel name. It supports + * construction from maps, data dictionaries, or direct parameters, and ensures that all required + * configuration keys are present and valid for TIC input/output streams. + * + *

    Key features: * - *

    - * Key features: *

      - *
    • Supports both standard and historic TIC modes
    • - *
    • Validates configuration parameters for TIC data streams
    • - *
    • Provides accessors for TIC mode and other stream properties
    • + *
    • Supports both standard and historic TIC modes + *
    • Validates configuration parameters for TIC data streams + *
    • Provides accessors for TIC mode and other stream properties *
    * * @author Enedis Smarties team @@ -46,42 +41,27 @@ * @see TICMode */ public class TICStreamConfiguration extends DataStreamConfiguration { - /** - * Key for the TIC mode parameter in the configuration. - */ + /** Key for the TIC mode parameter in the configuration. */ protected static final String KEY_TIC_MODE = "ticMode"; - /** - * Accepted value for the stream type (TIC). - */ + /** Accepted value for the stream type (TIC). */ private static final DataStreamType TYPE_ACCEPTED_VALUE = DataStreamType.TIC; - /** - * Accepted values for the stream direction (INPUT or OUTPUT). - */ + /** Accepted values for the stream direction (INPUT or OUTPUT). */ private static final DataStreamDirection[] DIRECTION_ACCEPTED_VALUES = { - DataStreamDirection.OUTPUT, DataStreamDirection.INPUT + DataStreamDirection.OUTPUT, DataStreamDirection.INPUT }; - /** - * Default value for the TIC mode (AUTO). - */ + /** Default value for the TIC mode (AUTO). */ private static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; - /** - * List of key descriptors for configuration validation. - */ + /** List of key descriptors for configuration validation. */ private List> keys = new ArrayList>(); - /** - * Key descriptor for the TIC mode parameter. - */ + /** Key descriptor for the TIC mode parameter. */ protected KeyDescriptorEnum kTicMode; - /** - * Protected default constructor. Initializes key descriptors and sets default - * values. - */ + /** Protected default constructor. Initializes key descriptors and sets default values. */ protected TICStreamConfiguration() { super(); this.loadKeyDescriptors(); @@ -114,8 +94,7 @@ public TICStreamConfiguration(DataDictionary other) throws DataDictionaryExcepti } /** - * Constructs a TICStreamConfiguration with the specified name and file, using - * default parameters. + * Constructs a TICStreamConfiguration with the specified name and file, using default parameters. * * @param name the configuration name * @param file the configuration file @@ -128,10 +107,10 @@ public TICStreamConfiguration(String name, File file) { /** * Constructs a TICStreamConfiguration with the specified parameters. * - * @param name the configuration name - * @param direction the data stream direction (INPUT or OUTPUT) + * @param name the configuration name + * @param direction the data stream direction (INPUT or OUTPUT) * @param channelName the channel name - * @param ticMode the TIC mode (STANDARD, HISTORIC, or AUTO) + * @param ticMode the TIC mode (STANDARD, HISTORIC, or AUTO) * @throws DataDictionaryException if the configuration is invalid */ public TICStreamConfiguration( @@ -192,9 +171,7 @@ protected void setTicMode(Object ticMode) throws DataDictionaryException { this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); } - /** - * Loads key descriptors for configuration validation and type conversion. - */ + /** Loads key descriptors for configuration validation and type conversion. */ private void loadKeyDescriptors() { try { this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java index 6f69d64..9054950 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java @@ -12,18 +12,16 @@ /** * Enumeration of error codes for TIC frame and serial port operations. * - *

    - * This enum defines error codes for serial port issues and TIC frame processing - * errors. It provides - * a mapping from {@link SerialPortException} types to TIC error codes for - * unified error handling. + *

    This enum defines error codes for serial port issues and TIC frame processing errors. It + * provides a mapping from {@link SerialPortException} types to TIC error codes for unified error + * handling. + * + *

    Key features: * - *

    - * Key features: *

      - *
    • Defines error codes for serial port and TIC frame errors
    • - *
    • Provides a method to map {@link SerialPortException} to TICError
    • - *
    • Each error code has an associated integer value
    • + *
    • Defines error codes for serial port and TIC frame errors + *
    • Provides a method to map {@link SerialPortException} to TICError + *
    • Each error code has an associated integer value *
    * * @author Enedis Smarties team @@ -56,9 +54,7 @@ public enum TICError { TIC_READER_READ_FRAME_CHECKSUM_INVALID(27), ; - /** - * Integer value associated with the error code. - */ + /** Integer value associated with the error code. */ private int value; /** @@ -80,8 +76,7 @@ public int getValue() { } /** - * Maps a {@link SerialPortException} to the corresponding {@link TICError} - * code. + * Maps a {@link SerialPortException} to the corresponding {@link TICError} code. * * @param serialPortException the serial port exception to map * @return the corresponding {@link TICError}, or null if not recognized diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java index 8929791..042ddcc 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java @@ -20,21 +20,18 @@ /** * Abstract base class for TIC frames. * - *

    - * This class provides the structure and common operations for TIC frames, - * including data set - * management, byte serialization, and data dictionary conversion. Subclasses - * implement protocol-specific - * details for standard and historic TIC formats. + *

    This class provides the structure and common operations for TIC frames, including data set + * management, byte serialization, and data dictionary conversion. Subclasses implement + * protocol-specific details for standard and historic TIC formats. + * + *

    Key features: * - *

    - * Key features: *

      - *
    • Defines constants for frame delimiters and options
    • - *
    • Manages a list of labeled data sets
    • - *
    • Provides methods for adding, removing, and retrieving data sets
    • - *
    • Supports conversion to byte arrays and data dictionaries
    • - *
    • Abstract methods for protocol-specific data set and mode handling
    • + *
    • Defines constants for frame delimiters and options + *
    • Manages a list of labeled data sets + *
    • Provides methods for adding, removing, and retrieving data sets + *
    • Supports conversion to byte arrays and data dictionaries + *
    • Abstract methods for protocol-specific data set and mode handling *
    * * @author Enedis Smarties team @@ -42,55 +39,56 @@ * @see TICMode */ public abstract class TICFrame { - /** - * Frame start delimiter (STX, 0x02). - */ + /** Frame start delimiter (STX, 0x02). */ public static final byte BEGINNING_PATTERN = 0x02; // STX - /** - * Frame end delimiter (ETX, 0x03). - */ + /** Frame end delimiter (ETX, 0x03). */ public static final byte END_PATTERN = 0x03; // ETX - /** - * End of transmission delimiter (EOT, 0x04). - */ + /** End of transmission delimiter (EOT, 0x04). */ public static final byte EOT = 0x04; // EOT // Frame options and keys /** No specific options. */ public static final int EXPLICIT = 0x0000; + /** Hide checksums in data dictionary. */ public static final int NOCHECKSUM = 0x0001; + /** Hide date/time fields in data dictionary. */ public static final int NODATETIME = 0x0002; + /** Hide TIC mode in data dictionary. */ public static final int NOTICMODE = 0x0004; + /** Filter invalid data sets in data dictionary. */ public static final int NOINVALID = 0x0008; + /** Trimmed options: hide checksum, date/time, and invalid data sets. */ public static final int TRIMMED = NOCHECKSUM | NODATETIME | NOINVALID; + /** Key for TIC frame in data dictionary. */ public static final String KEY_TICFRAME = "ticFrame"; + /** Key for TIC mode in data dictionary. */ public static final String KEY_TICMODE = "ticMode"; + /** Key for label in data dictionary. */ public static final String KEY_LABEL = "label"; + /** Key for data in data dictionary. */ public static final String KEY_DATA = "data"; + /** Key for checksum in data dictionary. */ public static final String KEY_CHECKSUM = "checksum"; + /** Key for date/time in data dictionary. */ public static final String KEY_DATETIME = "dateTime"; - /** - * List of data sets contained in this TIC frame. - */ + /** List of data sets contained in this TIC frame. */ protected List DataSetList; - /** - * Constructs an empty TIC frame with an empty data set list. - */ + /** Constructs an empty TIC frame with an empty data set list. */ public TICFrame() { this.DataSetList = new ArrayList(); } @@ -102,12 +100,9 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (this.getClass() != obj.getClass()) - return false; + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; TICFrame other = (TICFrame) obj; return Objects.equals(this.DataSetList, other.DataSetList); } @@ -169,8 +164,7 @@ public TICFrameDataSet getDataSet(String label) { } /** - * Returns the data value for the data set with the given label, or null if not - * found. + * Returns the data value for the data set with the given label, or null if not found. * * @param label the label of the data set * @return the data value, or null if not found @@ -186,9 +180,8 @@ public String getData(String label) { } /** - * Adds the given data set to the end of the list. If a data set with the same - * label exists, - * its data and checksum are updated. + * Adds the given data set to the end of the list. If a data set with the same label exists, its + * data and checksum are updated. * * @param dataSet the data set to add or update */ @@ -204,11 +197,10 @@ public void addDataSet(TICFrameDataSet dataSet) { } /** - * Adds the given data set at the specified index. If a data set with the same - * label exists, - * its data and checksum are updated and it is moved to the new index. + * Adds the given data set at the specified index. If a data set with the same label exists, its + * data and checksum are updated and it is moved to the new index. * - * @param index the position to insert the data set + * @param index the position to insert the data set * @param dataSet the data set to add or update */ public void addDataSet(int index, TICFrameDataSet dataSet) { @@ -231,18 +223,17 @@ public void addDataSet(int index, TICFrameDataSet dataSet) { * Adds a new data set with the given label and data to the frame. * * @param label the label for the data set - * @param data the data value + * @param data the data value * @return the created or updated data set */ public abstract TICFrameDataSet addDataSet(String label, String data); /** - * Adds a new data set with the given label and data at the specified index in - * the frame. + * Adds a new data set with the given label and data at the specified index in the frame. * * @param index the position to insert the data set * @param label the label for the data set - * @param data the data value + * @param data the data value * @return the created or updated data set */ public abstract TICFrameDataSet addDataSet(int index, String label, String data); @@ -286,7 +277,7 @@ public boolean moveDataSet(String label, int index) { * Sets the data value for the data set with the given label. * * @param label the label of the data set - * @param data the new data value + * @param data the new data value * @return true if the data set was found and updated, false otherwise */ public boolean setDataSet(String label, String data) { @@ -305,8 +296,8 @@ public boolean setDataSet(String label, String data) { /** * Sets the data value and checksum for the data set with the given label. * - * @param label the label of the data set - * @param data the new data value + * @param label the label of the data set + * @param data the new data value * @param checksum the new checksum value * @return true if the data set was found and updated, false otherwise */ @@ -387,16 +378,12 @@ public DataDictionary getDataDictionary() throws DataDictionaryException { } /** - * Returns a data dictionary representation of this frame with the specified - * options. + * Returns a data dictionary representation of this frame with the specified options. * - * @param options options to DataDictionary render (use like bits fields) : - - * EXPLICIT : frame is - * detailed - NOCHECKSUM : DataSet checksum are hidden - - * NODATETIME : DataSet dateTime fields - * are hidden - NOTICMODE : Tic mode is hidden; - TRIMMED : - * NOCHECKSUM | NODATETIME - - * FILTER_INVALID : Invalid DataSet are filtered + * @param options options to DataDictionary render (use like bits fields) : - EXPLICIT : frame is + * detailed - NOCHECKSUM : DataSet checksum are hidden - NODATETIME : DataSet dateTime fields + * are hidden - NOTICMODE : Tic mode is hidden; - TRIMMED : NOCHECKSUM | NODATETIME - + * FILTER_INVALID : Invalid DataSet are filtered * @return data dictionary * @throws DataDictionaryException */ diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java index 49a9efc..1664b87 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java @@ -14,52 +14,39 @@ /** * Abstract base class for data sets in TIC frames. * - *

    - * This class provides the structure and common operations for TIC frame data - * sets, including - * label/data management, checksum calculation, and serialization. Subclasses - * implement protocol-specific - * details for standard and historic TIC formats. + *

    This class provides the structure and common operations for TIC frame data sets, including + * label/data management, checksum calculation, and serialization. Subclasses implement + * protocol-specific details for standard and historic TIC formats. + * + *

    Key features: * - *

    - * Key features: *

      - *
    • Defines constants for data set delimiters
    • - *
    • Manages label, data, and checksum fields
    • - *
    • Provides methods for setting and validating fields
    • - *
    • Abstract methods for protocol-specific serialization and checksum
    • + *
    • Defines constants for data set delimiters + *
    • Manages label, data, and checksum fields + *
    • Provides methods for setting and validating fields + *
    • Abstract methods for protocol-specific serialization and checksum *
    * * @author Enedis Smarties team * @see BytesArray */ public abstract class TICFrameDataSet { - /** - * Data set start delimiter (LF, 0x0A). - */ + /** Data set start delimiter (LF, 0x0A). */ public static final byte BEGINNING_PATTERN = 0x0A; // LF - /** - * Data set end delimiter (CR, 0x0D). - */ + /** Data set end delimiter (CR, 0x0D). */ public static final byte END_PATTERN = 0x0D; // CR - /** - * Label field for the data set. - */ + /** Label field for the data set. */ protected BytesArray label = null; - /** - * Data field for the data set. - */ + + /** Data field for the data set. */ protected BytesArray data = null; - /** - * Checksum value for the data set. - */ + + /** Checksum value for the data set. */ protected byte checksum; - /** - * Constructs an empty TIC frame data set. - */ + /** Constructs an empty TIC frame data set. */ public TICFrameDataSet() { this.init(); } @@ -71,12 +58,9 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (this.getClass() != obj.getClass()) - return false; + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; TICFrameDataSet other = (TICFrameDataSet) obj; return this.checksum == other.checksum && Objects.equals(this.data, other.data) @@ -87,7 +71,7 @@ public boolean equals(Object obj) { * Sets up the data set with the given label and data strings. * * @param label the label string - * @param data the data string + * @param data the data string */ public void setup(String label, String data) { this.setup(label.getBytes(), data.getBytes()); @@ -97,7 +81,7 @@ public void setup(String label, String data) { * Sets up the data set with the given label and data byte arrays. * * @param label the label bytes - * @param data the data bytes + * @param data the data bytes */ public void setup(byte[] label, byte[] data) { this.label = new BytesArray(label); @@ -132,8 +116,7 @@ public void setLabel(byte[] label) { } /** - * Sets the label field from a {@link BytesArray} value and updates the - * checksum. + * Sets the label field from a {@link BytesArray} value and updates the checksum. * * @param label the label as a {@link BytesArray} */ @@ -189,8 +172,7 @@ public byte getChecksum() { } /** - * Computes and sets the checksum for this data set (label and data must be - * set). + * Computes and sets the checksum for this data set (label and data must be set). * * @return true if the operation succeeds, false otherwise */ @@ -259,9 +241,7 @@ public boolean isValid() { */ protected abstract Byte getConsistentChecksum(); - /** - * Initializes the data set, clearing label, data, and checksum fields. - */ + /** Initializes the data set, clearing label, data, and checksum fields. */ private void init() { this.label = null; this.data = null; diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java index 7053357..20995f8 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java @@ -13,19 +13,16 @@ /** * TIC frame representation for historic TIC protocol. * - *

    - * This class extends {@link TICFrame} to provide support for historic TIC - * frames, including - * separator definition and data set management. It allows adding labeled data - * sets to the frame - * and always reports its mode as {@link TICMode#HISTORIC}. + *

    This class extends {@link TICFrame} to provide support for historic TIC frames, including + * separator definition and data set management. It allows adding labeled data sets to the frame and + * always reports its mode as {@link TICMode#HISTORIC}. + * + *

    Key features: * - *

    - * Key features: *

      - *
    • Defines the separator for historic TIC frames
    • - *
    • Supports adding labeled data sets at specific positions
    • - *
    • Always returns HISTORIC as the TIC mode
    • + *
    • Defines the separator for historic TIC frames + *
    • Supports adding labeled data sets at specific positions + *
    • Always returns HISTORIC as the TIC mode *
    * * @author Enedis Smarties team @@ -34,14 +31,10 @@ * @see TICMode */ public class TICFrameHistoric extends TICFrame { - /** - * Separator character (space, 0x20) used in historic TIC frames. - */ + /** Separator character (space, 0x20) used in historic TIC frames. */ public static final byte SEPARATOR = 0x20; // SP - /** - * Constructs an empty historic TIC frame. - */ + /** Constructs an empty historic TIC frame. */ public TICFrameHistoric() { super(); } @@ -50,7 +43,7 @@ public TICFrameHistoric() { * Adds a new data set with the given label and data to the end of the frame. * * @param label the label for the data set - * @param data the data value + * @param data the data value * @return the created or updated {@link TICFrameHistoricDataSet} */ @Override @@ -59,16 +52,13 @@ public TICFrameHistoricDataSet addDataSet(String label, String data) { } /** - * Adds a new data set with the given label and data at the specified index in - * the frame. + * Adds a new data set with the given label and data at the specified index in the frame. * - *

    - * If a data set with the same label exists, it is updated and moved to the new - * index. + *

    If a data set with the same label exists, it is updated and moved to the new index. * * @param index the position to insert the data set * @param label the label for the data set - * @param data the data value + * @param data the data value * @return the created or updated {@link TICFrameHistoricDataSet} */ @Override diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java index 3b9e7b4..8bfa7bd 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java @@ -15,19 +15,16 @@ /** * Data set representation for historic TIC frames. * - *

    - * This class extends {@link TICFrameDataSet} to provide checksum calculation, - * byte serialization, - * and JSON conversion for historic TIC data sets. It defines the separator and - * implements the - * protocol-specific checksum logic. + *

    This class extends {@link TICFrameDataSet} to provide checksum calculation, byte + * serialization, and JSON conversion for historic TIC data sets. It defines the separator and + * implements the protocol-specific checksum logic. + * + *

    Key features: * - *

    - * Key features: *

      - *
    • Defines the separator for historic TIC data sets
    • - *
    • Implements checksum calculation for label and data
    • - *
    • Serializes the data set to bytes and JSON
    • + *
    • Defines the separator for historic TIC data sets + *
    • Implements checksum calculation for label and data + *
    • Serializes the data set to bytes and JSON *
    * * @author Enedis Smarties team @@ -35,24 +32,18 @@ * @see TICFrame */ public class TICFrameHistoricDataSet extends TICFrameDataSet { - /** - * Separator character (space, 0x20) used in historic TIC data sets. - */ + /** Separator character (space, 0x20) used in historic TIC data sets. */ public static final byte SEPARATOR = 0x20; // SP - /** - * Constructs an empty historic TIC data set. - */ + /** Constructs an empty historic TIC data set. */ public TICFrameHistoricDataSet() { super(); } /** - * Computes the consistent checksum for this data set according to the historic - * TIC protocol. + * Computes the consistent checksum for this data set according to the historic TIC protocol. * - *

    - * The checksum is calculated over the label, separator, and data fields. + *

    The checksum is calculated over the label, separator, and data fields. * * @return the computed checksum, or null if label or data is missing */ @@ -83,7 +74,7 @@ public Byte getConsistentChecksum() { /** * Updates the checksum value with the given byte. * - * @param crc the current checksum value + * @param crc the current checksum value * @param octet the byte to add * @return the updated checksum value */ @@ -102,8 +93,7 @@ public Byte computeEnd(Byte crc) { } /** - * Serializes this data set to a byte array according to the historic TIC - * protocol. + * Serializes this data set to a byte array according to the historic TIC protocol. * * @return the byte array representation of the data set */ diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java index a4c4b15..67f238b 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java @@ -12,18 +12,15 @@ /** * Exception class for errors encountered during TIC frame processing. * - *

    - * This exception is thrown when a TIC frame cannot be parsed, validated, or - * processed correctly. - * It encapsulates a {@link TICError} code to provide additional context about - * the error type. + *

    This exception is thrown when a TIC frame cannot be parsed, validated, or processed correctly. + * It encapsulates a {@link TICError} code to provide additional context about the error type. + * + *

    Key features: * - *

    - * Key features: *

      - *
    • Associates a {@link TICError} with each exception instance
    • - *
    • Supports standard exception message and error code
    • - *
    • Provides a reset method to restore default error state
    • + *
    • Associates a {@link TICError} with each exception instance + *
    • Supports standard exception message and error code + *
    • Provides a reset method to restore default error state *
    * * @author Enedis Smarties team @@ -31,27 +28,20 @@ * @see Exception */ public class TICException extends Exception { - /** - * Unique identifier used for serialization. - */ + /** Unique identifier used for serialization. */ private static final long serialVersionUID = -2780151361870269473L; - /** - * The TIC error code associated with this exception. - */ + /** The TIC error code associated with this exception. */ private TICError error; - /** - * Constructs a new TICException with default error code. - */ + /** Constructs a new TICException with default error code. */ public TICException() { super(); this.reset(); } /** - * Constructs a new TICException with the specified error message and default - * error code. + * Constructs a new TICException with the specified error message and default error code. * * @param message the detail message */ @@ -61,10 +51,9 @@ public TICException(String message) { } /** - * Constructs a new TICException with the specified error message and error - * code. + * Constructs a new TICException with the specified error message and error code. * - * @param message the detail message + * @param message the detail message * @param readerError the TIC error code */ public TICException(String message, TICError readerError) { @@ -72,9 +61,7 @@ public TICException(String message, TICError readerError) { this.error = readerError; } - /** - * Resets the error code to the default value. - */ + /** Resets the error code to the default value. */ public void reset() { this.error = TICError.TIC_READER_DEFAULT_ERROR; } diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java index bd7ad21..fee7b1b 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java @@ -14,21 +14,18 @@ /** * TIC frame representation for standard TIC protocol. * - *

    - * This class extends {@link TICFrame} to provide support for standard TIC - * frames, including - * info group delimiters, data set management, and date/time handling. It allows - * adding labeled data sets - * and retrieving data or date/time values by label. Always reports its mode as - * {@link TICMode#STANDARD}. + *

    This class extends {@link TICFrame} to provide support for standard TIC frames, including info + * group delimiters, data set management, and date/time handling. It allows adding labeled data sets + * and retrieving data or date/time values by label. Always reports its mode as {@link + * TICMode#STANDARD}. + * + *

    Key features: * - *

    - * Key features: *

      - *
    • Defines info group delimiters for standard TIC frames
    • - *
    • Supports adding and retrieving labeled data sets
    • - *
    • Handles date/time fields as strings or {@link LocalDateTime}
    • - *
    • Always returns STANDARD as the TIC mode
    • + *
    • Defines info group delimiters for standard TIC frames + *
    • Supports adding and retrieving labeled data sets + *
    • Handles date/time fields as strings or {@link LocalDateTime} + *
    • Always returns STANDARD as the TIC mode *
    * * @author Enedis Smarties team @@ -37,34 +34,22 @@ * @see TICMode */ public class TICFrameStandard extends TICFrame { - /** - * Info group begin delimiter (LF, 0x03) for standard TIC frames. - */ + /** Info group begin delimiter (LF, 0x03) for standard TIC frames. */ public static final byte INFOGROUP_BEGIN = 0x03; // LF - /** - * Info group end delimiter (CR, 0x03) for standard TIC frames. - */ + /** Info group end delimiter (CR, 0x03) for standard TIC frames. */ public static final byte INFOGROUP_END = 0x03; // CR - /** - * Info group separator (HT, 0x03) for standard TIC frames. - */ + /** Info group separator (HT, 0x03) for standard TIC frames. */ public static final byte INFOGROUP_SEP = 0x03; // HT - /** - * Minimum size of an info group in bytes (LF + Label + HT + Data + HT + ...). - */ + /** Minimum size of an info group in bytes (LF + Label + HT + Data + HT + ...). */ public static final int INFOGROUP_MIN_SIZE = 7; // LF + Label + HT + Data + HT + - /** - * Minimum size of a standard TIC frame (info group + ETX + STX). - */ + /** Minimum size of a standard TIC frame (info group + ETX + STX). */ public static final int MIN_SIZE = INFOGROUP_MIN_SIZE + 2; // ETX + INFGROUP_MIN_SIZE + STX - /** - * Constructs an empty standard TIC frame. - */ + /** Constructs an empty standard TIC frame. */ public TICFrameStandard() { super(); } @@ -72,11 +57,10 @@ public TICFrameStandard() { /** * Adds a new data set with the given label and data to the frame. * - *

    - * If a data set with the same label exists, it is updated and added again. + *

    If a data set with the same label exists, it is updated and added again. * * @param label the label for the data set - * @param data the data value + * @param data the data value * @return the created or updated {@link TICFrameStandardDataSet} */ @Override @@ -99,16 +83,13 @@ public TICFrameStandardDataSet addDataSet(String label, String data) { } /** - * Adds a new data set with the given label and data at the specified index in - * the frame. + * Adds a new data set with the given label and data at the specified index in the frame. * - *

    - * If a data set with the same label exists, it is updated and moved to the new - * index. + *

    If a data set with the same label exists, it is updated and moved to the new index. * * @param index the position to insert the data set * @param label the label for the data set - * @param data the data value + * @param data the data value * @return the created or updated {@link TICFrameStandardDataSet} */ @Override @@ -153,8 +134,7 @@ public TICMode getMode() { } /** - * Returns the data value for the given label, or the date/time if the label is - * "DATE". + * Returns the data value for the given label, or the date/time if the label is "DATE". * * @param label the label to search for * @return the data value or date/time string, or null if not found @@ -177,8 +157,8 @@ public String getData(String label) { /** * Adds a new data set with the given label, data, and date/time value. * - * @param label the label for the data set - * @param data the data value + * @param label the label for the data set + * @param data the data value * @param dateTime the date/time string * @return the created or updated {@link TICFrameStandardDataSet} */ @@ -206,8 +186,7 @@ public String getDateTime(String label) { } /** - * Returns the date/time as a {@link LocalDateTime} for the given label, or null - * if not found. + * Returns the date/time as a {@link LocalDateTime} for the given label, or null if not found. * * @param label the label to search for * @return the date/time as {@link LocalDateTime}, or null if not found diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java index ab4afa2..5db717d 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java @@ -18,21 +18,17 @@ /** * Data set representation for standard TIC frames. * - *

    - * This class extends {@link TICFrameDataSet} to provide checksum calculation, - * byte serialization, - * date/time handling, and JSON conversion for standard TIC data sets. It - * defines the separator and - * implements the protocol-specific logic for standard frames. + *

    This class extends {@link TICFrameDataSet} to provide checksum calculation, byte + * serialization, date/time handling, and JSON conversion for standard TIC data sets. It defines the + * separator and implements the protocol-specific logic for standard frames. + * + *

    Key features: * - *

    - * Key features: *

      - *
    • Defines the separator for standard TIC data sets
    • - *
    • Implements checksum calculation for label, data, and optional - * date/time
    • - *
    • Handles date/time fields as strings or {@link LocalDateTime}
    • - *
    • Serializes the data set to bytes and JSON
    • + *
    • Defines the separator for standard TIC data sets + *
    • Implements checksum calculation for label, data, and optional date/time + *
    • Handles date/time fields as strings or {@link LocalDateTime} + *
    • Serializes the data set to bytes and JSON *
    * * @author Enedis Smarties team @@ -40,31 +36,22 @@ * @see TICFrame */ public class TICFrameStandardDataSet extends TICFrameDataSet { - /** - * Separator character (horizontal tab, 0x09) used in standard TIC data sets. - */ + /** Separator character (horizontal tab, 0x09) used in standard TIC data sets. */ public static final byte SEPARATOR = 0x09; // HT - /** - * Date/time field for the data set (optional, may be null). - */ + /** Date/time field for the data set (optional, may be null). */ protected BytesArray dateTime = null; - /** - * Constructs an empty standard TIC data set. - */ + /** Constructs an empty standard TIC data set. */ public TICFrameStandardDataSet() { super(); this.init(); } /** - * Computes the consistent checksum for this data set according to the standard - * TIC protocol. + * Computes the consistent checksum for this data set according to the standard TIC protocol. * - *

    - * The checksum is calculated over the label, optional date/time, and data - * fields. + *

    The checksum is calculated over the label, optional date/time, and data fields. * * @return the computed checksum, or null if label or data is missing */ @@ -108,7 +95,7 @@ public Byte getConsistentChecksum() { /** * Updates the checksum value with the given byte. * - * @param crc the current checksum value + * @param crc the current checksum value * @param octet the byte to add * @return the updated checksum value */ @@ -127,8 +114,7 @@ public Byte computeEnd(Byte crc) { } /** - * Serializes this data set to a byte array according to the standard TIC - * protocol. + * Serializes this data set to a byte array according to the standard TIC protocol. * * @return the byte array representation of the data set */ @@ -170,8 +156,8 @@ public JSONObject toJSON() { /** * Converts this data set to a JSON object with the specified options. * - * @param option bitmask of options (e.g., {@link TICFrame#NOCHECKSUM}, - * {@link TICFrame#NODATETIME}) + * @param option bitmask of options (e.g., {@link TICFrame#NOCHECKSUM}, {@link + * TICFrame#NODATETIME}) * @return the JSON representation of the data set */ @Override @@ -208,8 +194,8 @@ public JSONObject toJSON(int option) { /** * Sets up the data set with the given label, data, and date/time strings. * - * @param label the label string - * @param data the data string + * @param label the label string + * @param data the data string * @param dateTime the date/time string */ public void setup(String label, String data, String dateTime) { @@ -219,8 +205,8 @@ public void setup(String label, String data, String dateTime) { /** * Sets up the data set with the given label, data, and date/time byte arrays. * - * @param label the label bytes - * @param data the data bytes + * @param label the label bytes + * @param data the data bytes * @param dateTime the date/time bytes */ public void setup(byte[] label, byte[] data, byte[] dateTime) { @@ -247,12 +233,10 @@ public Boolean checkDateTime() { } /** - * Returns the date/time as a {@link LocalDateTime} for this data set, or null - * if not set or invalid. + * Returns the date/time as a {@link LocalDateTime} for this data set, or null if not set or + * invalid. * - *

    - * The expected format is "HyyMMddHHmmss" (13 characters, with the first - * character ignored). + *

    The expected format is "HyyMMddHHmmss" (13 characters, with the first character ignored). * * @return the date/time as {@link LocalDateTime}, or null if not set or invalid */ @@ -300,9 +284,7 @@ public void setDateTime(BytesArray dateTime) { this.setChecksum(); } - /** - * Initializes the data set, clearing the date/time field. - */ + /** Initializes the data set, clearing the date/time field. */ private void init() { this.dateTime = null; } From 4329ec53569e5623954da5a037959be36180c116 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Fri, 3 Oct 2025 19:02:21 +0200 Subject: [PATCH 26/59] chore: format with spotless --- .../java/enedis/lab/protocol/tic/TICMode.java | 180 +- .../java/enedis/lab/types/BytesArray.java | 2227 ++++++++--------- .../java/enedis/lab/types/DataArrayList.java | 583 ++--- .../java/enedis/lab/types/DataDictionary.java | 250 +- .../lab/types/DataDictionaryException.java | 112 +- src/main/java/enedis/lab/types/DataList.java | 68 +- .../java/enedis/lab/types/ExceptionBase.java | 110 +- .../types/configuration/Configuration.java | 70 +- .../configuration/ConfigurationBase.java | 231 +- .../configuration/ConfigurationException.java | 184 +- .../datadictionary/DataDictionaryBase.java | 1146 ++++----- .../types/datadictionary/KeyDescriptor.java | 95 +- .../datadictionary/KeyDescriptorBase.java | 257 +- .../KeyDescriptorDataDictionary.java | 170 +- .../datadictionary/KeyDescriptorEnum.java | 128 +- .../datadictionary/KeyDescriptorList.java | 279 +-- .../KeyDescriptorListMinMaxSize.java | 164 +- .../KeyDescriptorLocalDateTime.java | 137 +- .../datadictionary/KeyDescriptorNumber.java | 126 +- .../KeyDescriptorNumberMinMax.java | 158 +- .../datadictionary/KeyDescriptorString.java | 115 +- .../java/enedis/lab/util/MinMaxChecker.java | 199 +- .../java/enedis/lab/util/SystemError.java | 90 +- src/main/java/enedis/lab/util/SystemLibC.java | 28 +- .../java/enedis/lab/util/message/Event.java | 217 +- .../java/enedis/lab/util/message/Message.java | 263 +- .../enedis/lab/util/message/MessageType.java | 19 +- .../java/enedis/lab/util/message/Request.java | 132 +- .../enedis/lab/util/message/Response.java | 347 ++- .../enedis/lab/util/message/ResponseBase.java | 225 +- .../message/exception/MessageException.java | 84 +- .../MessageInvalidContentException.java | 86 +- .../MessageInvalidFormatException.java | 84 +- .../MessageInvalidTypeException.java | 84 +- .../MessageKeyNameDoesntExistException.java | 84 +- .../MessageKeyTypeDoesntExistException.java | 86 +- .../UnsupportedMessageException.java | 84 +- .../factory/AbstractMessageFactory.java | 141 +- .../message/factory/BasicMessageFactory.java | 167 +- .../util/message/factory/EventFactory.java | 18 +- .../util/message/factory/MessageFactory.java | 188 +- .../util/message/factory/RequestFactory.java | 18 +- .../util/message/factory/ResponseFactory.java | 18 +- .../lab/util/task/FilteredNotifier.java | 160 +- .../lab/util/task/FilteredNotifierBase.java | 411 ++- .../java/enedis/lab/util/task/Notifier.java | 53 +- .../enedis/lab/util/task/NotifierBase.java | 82 +- .../java/enedis/lab/util/task/Subscriber.java | 9 +- src/main/java/enedis/lab/util/task/Task.java | 31 +- .../java/enedis/lab/util/task/TaskBase.java | 231 +- .../enedis/lab/util/task/TaskPeriodic.java | 138 +- .../task/TaskPeriodicWithSubscribers.java | 93 +- src/main/java/enedis/lab/util/time/Time.java | 164 +- .../tic/core/ReadNextFrameSubscriber.java | 70 +- src/main/java/enedis/tic/core/TICCore.java | 155 +- .../java/enedis/tic/core/TICCoreBase.java | 918 ++++--- .../java/enedis/tic/core/TICCoreError.java | 406 ++- .../enedis/tic/core/TICCoreErrorCode.java | 39 +- .../enedis/tic/core/TICCoreException.java | 15 +- .../java/enedis/tic/core/TICCoreFrame.java | 394 ++- .../java/enedis/tic/core/TICCoreStream.java | 21 +- .../enedis/tic/core/TICCoreStreamBase.java | 444 ++-- .../enedis/tic/core/TICCoreSubscriber.java | 33 +- .../java/enedis/tic/core/TICIdentifier.java | 390 ++- .../tic/service/TIC2WebSocketApplication.java | 646 +++-- .../TIC2WebSocketApplicationErrorCode.java | 58 +- .../tic/service/TIC2WebSocketCommandLine.java | 130 +- .../service/client/TIC2WebSocketClient.java | 112 +- .../client/TIC2WebSocketClientPool.java | 75 +- .../client/TIC2WebSocketClientPoolBase.java | 112 +- .../config/TIC2WebSocketConfiguration.java | 406 ++- .../tic/service/endpoint/EventSender.java | 24 +- .../TIC2WebSocketEndPointErrorCode.java | 82 +- .../TIC2WebSocketEndPointException.java | 120 +- .../tic/service/message/EventOnError.java | 220 +- .../tic/service/message/EventOnTICData.java | 222 +- .../message/RequestGetAvailableTICs.java | 96 +- .../service/message/RequestGetModemsInfo.java | 94 +- .../tic/service/message/RequestReadTIC.java | 214 +- .../service/message/RequestSubscribeTIC.java | 217 +- .../message/RequestUnsubscribeTIC.java | 217 +- .../message/ResponseGetAvailableTICs.java | 231 +- .../message/ResponseGetModemsInfo.java | 234 +- .../tic/service/message/ResponseReadTIC.java | 230 +- .../service/message/ResponseSubscribeTIC.java | 143 +- .../message/ResponseUnsubscribeTIC.java | 141 +- .../factory/TIC2WebSocketEventFactory.java | 24 +- .../factory/TIC2WebSocketMessageFactory.java | 23 +- .../factory/TIC2WebSocketRequestFactory.java | 28 +- .../factory/TIC2WebSocketResponseFactory.java | 30 +- .../TIC2WebSocketChannelInitializer.java | 76 +- .../service/netty/TIC2WebSocketHandler.java | 386 ++- .../service/netty/TIC2WebSocketServer.java | 207 +- .../TIC2WebSocketRequestHandler.java | 25 +- .../TIC2WebSocketRequestHandlerBase.java | 449 ++-- .../java/enedis/lab/io/PortFinderMock.java | 104 +- .../enedis/lab/io/tic/TICPortFinderMock.java | 58 +- .../java/enedis/lab/mock/FunctionCall.java | 25 +- .../java/enedis/tic/core/TICCoreBaseTest.java | 1002 ++++---- .../tic/core/TICCoreReadNextFrameTask.java | 74 +- .../enedis/tic/core/TICCoreStreamMock.java | 151 +- .../tic/core/TICCoreSubscriberMock.java | 28 +- .../tic/core/TICCoreSubscriberOnDataCall.java | 15 +- .../core/TICCoreSubscriberOnErrorCall.java | 15 +- .../enedis/tic/core/TICIdentifierTest.java | 330 ++- .../TIC2WebSocketEventFactoryTest.java | 122 +- .../TIC2WebSocketMessageFactoryTest.java | 28 +- .../TIC2WebSocketRequestFactoryTest.java | 515 ++-- .../TIC2WebSocketResponseFactoryTest.java | 320 +-- .../netty/TIC2WebSocketServerTest.java | 38 +- 110 files changed, 10228 insertions(+), 11578 deletions(-) diff --git a/src/main/java/enedis/lab/protocol/tic/TICMode.java b/src/main/java/enedis/lab/protocol/tic/TICMode.java index b87b645..4a17ed0 100644 --- a/src/main/java/enedis/lab/protocol/tic/TICMode.java +++ b/src/main/java/enedis/lab/protocol/tic/TICMode.java @@ -7,119 +7,107 @@ package enedis.lab.protocol.tic; +import enedis.lab.protocol.tic.frame.TICError; +import enedis.lab.protocol.tic.frame.standard.TICException; import java.util.Arrays; /** * Enumeration of available TIC modes and related utilities. * - *

    This enum defines the supported TIC protocol modes (STANDARD, HISTORIC, AUTO, UNKNOWN) and provides - * utility methods for mode detection from frame or group buffers. It also defines protocol-specific - * separator and buffer start patterns. + *

    This enum defines the supported TIC protocol modes (STANDARD, HISTORIC, AUTO, UNKNOWN) and + * provides utility methods for mode detection from frame or group buffers. It also defines + * protocol-specific separator and buffer start patterns. * *

    Key features: + * *

      - *
    • Defines all supported TIC modes
    • - *
    • Provides methods to detect mode from frame or group buffers
    • - *
    • Defines protocol-specific separators and buffer start patterns
    • + *
    • Defines all supported TIC modes + *
    • Provides methods to detect mode from frame or group buffers + *
    • Defines protocol-specific separators and buffer start patterns *
    * * @author Enedis Smarties team */ +public enum TICMode { + /** Unknown mode. */ + UNKNOWN, + /** Standard mode. */ + STANDARD, + /** Historic mode. */ + HISTORIC, + /** Auto-detect mode. */ + AUTO; -import enedis.lab.protocol.tic.frame.TICError; -import enedis.lab.protocol.tic.frame.standard.TICException; - -/** - * TIC Mode - * - */ + /** Separator character (space, 0x20) for historic TIC frames. */ + public static final char HISTORIC_SEPARATOR = ' '; -public enum TICMode { - /** Unknown mode. */ - UNKNOWN, - /** Standard mode. */ - STANDARD, - /** Historic mode. */ - HISTORIC, - /** Auto-detect mode. */ - AUTO; + /** Separator character (tab, 0x09) for standard TIC frames. */ + public static final char STANDARD_SEPARATOR = '\t'; - /** - * Separator character (space, 0x20) for historic TIC frames. - */ - public static final char HISTORIC_SEPARATOR = ' '; - /** - * Separator character (tab, 0x09) for standard TIC frames. - */ - public static final char STANDARD_SEPARATOR = '\t'; - /** - * Buffer start pattern for historic TIC frames. - */ - public static final byte[] HISTORIC_BUFFER_START = { 2, 10, 65, 68, 67, 79 }; - /** - * Buffer start pattern for standard TIC frames. - */ - public static final byte[] STANDARD_BUFFER_START = { 2, 10, 65, 68, 83, 67 }; + /** Buffer start pattern for historic TIC frames. */ + public static final byte[] HISTORIC_BUFFER_START = {2, 10, 65, 68, 67, 79}; - /** - * Detects the TIC mode from the given frame buffer. - * - * @param frameBuffer the byte array containing the frame start - * @return the detected {@link TICMode}, or null if not recognized - * @throws TICException if the buffer is too short or invalid - */ - public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) throws TICException { - byte[] frameBufferStart = new byte[HISTORIC_BUFFER_START.length]; - if (frameBuffer.length < frameBufferStart.length) { - throw new TICException( - "Tic frame read 0x" + bytesToHex(frameBuffer) + " too short to determine TIC Mode !", - TICError.TIC_READER_FRAME_DECODE_FAILED); - } - System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); - if (Arrays.equals(frameBufferStart, HISTORIC_BUFFER_START)) { - return HISTORIC; - } else { - if (STANDARD_BUFFER_START.length != HISTORIC_BUFFER_START.length) { - frameBufferStart = new byte[STANDARD_BUFFER_START.length]; - System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); - } - if (Arrays.equals(frameBufferStart, STANDARD_BUFFER_START)) { - return STANDARD; - } - return null; - } - } + /** Buffer start pattern for standard TIC frames. */ + public static final byte[] STANDARD_BUFFER_START = {2, 10, 65, 68, 83, 67}; - /** - * Detects the TIC mode from the given group buffer. - * - * @param groupBuffer the byte array containing the group data - * @return the detected {@link TICMode}, or null if not recognized - */ - public static TICMode findModeFromGroupBuffer(byte[] groupBuffer) { - for (int i = 0; i < groupBuffer.length; i++) { - if (groupBuffer[i] == HISTORIC_SEPARATOR) { - return HISTORIC; - } else if (groupBuffer[i] == STANDARD_SEPARATOR) { - return STANDARD; - } - } - return null; - } + /** + * Detects the TIC mode from the given frame buffer. + * + * @param frameBuffer the byte array containing the frame start + * @return the detected {@link TICMode}, or null if not recognized + * @throws TICException if the buffer is too short or invalid + */ + public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) throws TICException { + byte[] frameBufferStart = new byte[HISTORIC_BUFFER_START.length]; + if (frameBuffer.length < frameBufferStart.length) { + throw new TICException( + "Tic frame read 0x" + bytesToHex(frameBuffer) + " too short to determine TIC Mode !", + TICError.TIC_READER_FRAME_DECODE_FAILED); + } + System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); + if (Arrays.equals(frameBufferStart, HISTORIC_BUFFER_START)) { + return HISTORIC; + } else { + if (STANDARD_BUFFER_START.length != HISTORIC_BUFFER_START.length) { + frameBufferStart = new byte[STANDARD_BUFFER_START.length]; + System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); + } + if (Arrays.equals(frameBufferStart, STANDARD_BUFFER_START)) { + return STANDARD; + } + return null; + } + } - /** - * Converts a byte array to a hexadecimal string (replacement for - * DatatypeConverter.printHexBinary). - * - * @param bytes the byte array to convert - * @return the hexadecimal string representation - */ - private static String bytesToHex(byte[] bytes) { - StringBuilder result = new StringBuilder(); - for (byte b : bytes) { - result.append(String.format("%02X", b)); - } - return result.toString(); - } + /** + * Detects the TIC mode from the given group buffer. + * + * @param groupBuffer the byte array containing the group data + * @return the detected {@link TICMode}, or null if not recognized + */ + public static TICMode findModeFromGroupBuffer(byte[] groupBuffer) { + for (int i = 0; i < groupBuffer.length; i++) { + if (groupBuffer[i] == HISTORIC_SEPARATOR) { + return HISTORIC; + } else if (groupBuffer[i] == STANDARD_SEPARATOR) { + return STANDARD; + } + } + return null; + } + /** + * Converts a byte array to a hexadecimal string (replacement for + * DatatypeConverter.printHexBinary). + * + * @param bytes the byte array to convert + * @return the hexadecimal string representation + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02X", b)); + } + return result.toString(); + } } diff --git a/src/main/java/enedis/lab/types/BytesArray.java b/src/main/java/enedis/lab/types/BytesArray.java index 067d224..70203e7 100644 --- a/src/main/java/enedis/lab/types/BytesArray.java +++ b/src/main/java/enedis/lab/types/BytesArray.java @@ -13,1213 +13,1022 @@ import java.util.List; import java.util.ListIterator; -/** - * Bytes array - */ -public class BytesArray implements List -{ - /** Default option */ - public static final int DEFAULT_OPTIONS = 0x0000; - /** Greedy option */ - public static final int GREEDY = 0x0001; - /** Contiguous option */ - public static final int CONTIGUOUS = 0x0002; - /** Remove patterns option */ - public static final int REMOVE_PATTERNS = 0x0004; - - /** - * Convert int to byte[] - * - * @param value - * @param numberOfByte - * @return byte[] - * @throws NumberFormatException - */ - public static byte[] intToArrayOfByte(int value, int numberOfByte) throws NumberFormatException - { - if (countBytes(value) > numberOfByte) - { - throw new NumberFormatException("Value " + value + " can't be converted in a byte[" + numberOfByte + "]"); - } - - byte[] byteArray = new byte[numberOfByte]; - - for (int i = 0; i < byteArray.length; i++) - { - if (i < Integer.BYTES) - { - byteArray[byteArray.length - i - 1] = (byte) (value >> (i * 8)); - } - } - - return byteArray; - } - - /** - * Conver int to BytesArray - * - * @param value - * @param numberOfByte - * @return BytesArray - * @throws Exception - */ - public static BytesArray valueOf(int value, int numberOfByte) throws Exception - { - return new BytesArray(intToArrayOfByte(value, numberOfByte)); - } - - private static int countBytes(int value) - { - int tmp = Math.abs(value); - int count = 0; - - for (int i = 0; i < Integer.BYTES; i++) - { - tmp = tmp >> 8; - count++; - if (tmp == 0) - { - break; - } - } - return count; - } - - protected byte[] buffer; - - /** - * Default constructor - */ - public BytesArray() - { - this.buffer = new byte[0]; - } - - /** - * Constructor from array of byte - * - * @param bytesArray - */ - public BytesArray(byte[] bytesArray) - { - this.buffer = bytesArray.clone(); - } - - @Override - public int size() - { - return this.buffer.length; - } - - @Override - public boolean isEmpty() - { - return (0 == this.buffer.length) ? true : false; - } - - @Override - public boolean contains(Object element) - { - int index = this.indexOf(element); - return (index >= 0) ? true : false; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public Iterator iterator() - { - throw new UnsupportedOperationException(); - } - - @Override - public Byte[] toArray() - { - Byte[] bytesArray = new Byte[this.buffer.length]; - - for (int i = 0; i < this.buffer.length; i++) - { - bytesArray[i] = this.buffer[i]; - } - - return bytesArray; - } - - @SuppressWarnings("unchecked") - @Override - public Byte[] toArray(Object[] a) - { - throw new UnsupportedOperationException(); - } - - @Override - public boolean add(Byte element) - { - byte byte_element = element; - byte[] bytesArray = new byte[this.buffer.length + 1]; - - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); - bytesArray[bytesArray.length - 1] = byte_element; - this.buffer = bytesArray; - - return true; - } - - @Override - public void add(int index, Byte element) - { - if ((index < 0) || (index >= this.buffer.length)) - { - this.add(element); - } - - else - { - byte[] bytesArray = new byte[this.buffer.length + 1]; - - // Si l'index est le premier - if (0 == index) - { - System.arraycopy(this.buffer, 0, bytesArray, 1, this.buffer.length); - } - - // Sinon, si l'index est le dernier - else if ((this.buffer.length - 1) == index) - { - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length - 1); - - bytesArray[bytesArray.length - 1] = this.buffer[this.buffer.length - 1]; - } - - // Sinon, si l'index est au milieu du tableau - else - { - System.arraycopy(this.buffer, 0, bytesArray, 0, index - 1); - - System.arraycopy(this.buffer, index, // src , src_pos - bytesArray, index + 1, // dest, dest_pos - this.buffer.length - index - 1); // size - } - - bytesArray[index] = element; - this.buffer = bytesArray; - } - } - - @Override - public boolean remove(Object element) - { - boolean success = true; - - if ((null != element) && (element instanceof Number)) - { - // Obtenir l'index de la première occurence de l'élément - int index = this.indexOf(element); - - // Si un tel élément a été trouvé, - if (index >= 0) - { - this.removeIndex(index); - } - - // Sinon, - else - { - success = false; - } - } - - else - { - success = false; - } - - return success; - } - - @Override - public Byte remove(int index) - { - Byte element = null; - - if ((index >= 0) && (index < this.buffer.length)) - { - element = new Byte(this.buffer[index]); - this.removeIndex(index); - } - - else - { - // Aucune action - } - - return element; - } - - /** - * Remove the buffer from an index to other - * - * @param fromIndex - * @param toIndex - * @return new length of buffer - */ - public int remove(int fromIndex, int toIndex) - { - int length = 0; - - if ((fromIndex >= 0) && (toIndex < this.buffer.length) && (toIndex > fromIndex)) - { - this.removeSubList(fromIndex, toIndex); - length = toIndex - fromIndex + 1; - } - - return length; - } - - @Override - public boolean containsAll(Collection c) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean addAll(Collection collection) - { - boolean hasChanged; - - if ((null != collection) && (collection.size() > 0)) - { - Object[] collectionArray = collection.toArray(); - byte[] bytesArray = new byte[this.buffer.length + collection.size()]; - - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); - - int j = this.buffer.length; - for (int i = 0; i < collectionArray.length; i++) - { - bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); - j++; - } - - this.buffer = bytesArray; - - hasChanged = true; - } - - else - { - hasChanged = false; - } - - return hasChanged; - } - - @Override - public boolean addAll(int index, Collection collection) - { - boolean hasChanged; - - if ((null != collection) && (collection.size() > 0) && (index >= 0) && (index <= this.buffer.length)) - { - Object[] collectionArray = collection.toArray(); - byte[] bytesArray = new byte[this.buffer.length + collection.size()]; - - // Cas ou on ajoute en début: - if (0 == index) - { - for (int i = 0; i < collectionArray.length; i++) - { - bytesArray[i] = ((Byte) collectionArray[i]).byteValue(); - } - - System.arraycopy(this.buffer, 0, bytesArray, collectionArray.length, this.buffer.length); - } - - // Cas où on ajoute à la fin: - else if (this.buffer.length == index) - { - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); - - int j = index; - for (int i = 0; i < collectionArray.length; i++) - { - bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); - j++; - } - } - - // Cas où on ajoute au milieu: - else - { - System.arraycopy(this.buffer, 0, bytesArray, 0, index); - - int j = index; - for (int i = 0; i < collectionArray.length; i++) - { - bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); - j++; - } - - System.arraycopy(this.buffer, index, bytesArray, index + collectionArray.length, this.buffer.length - index); - } - - this.buffer = bytesArray; - - hasChanged = true; - } - - else - { - hasChanged = false; - } - - return hasChanged; - } - - @Override - public boolean removeAll(Collection c) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean retainAll(Collection c) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public void clear() - { - this.buffer = new byte[0]; - } - - @Override - public Byte get(int index) - { - Byte element = null; - - if ((index >= 0) && (index < this.buffer.length)) - { - element = new Byte(this.buffer[index]); - } - - else - { - - } - - return element; - } - - @Override - public Byte set(int index, Byte element) - { - Byte previous_element = null; - - if ((index >= 0) && (index < this.buffer.length)) - { - previous_element = new Byte(this.buffer[index]); - this.buffer[index] = element; - } - - else - { - previous_element = new Byte(this.buffer[this.buffer.length - 1]); - this.buffer[this.buffer.length - 1] = element; - } - - return new Byte(previous_element); - } - - @Override - public int indexOf(Object element) - { - return this.indexOf(element, 0); - } - - /** - * Get the buffer element of the given index - * - * @param element - * @param offset - * @return the element - */ - public int indexOf(Object element, int offset) - { - int index = -1; - - if ((null != element) && (element instanceof Number) && offset >= 0 && offset < this.buffer.length) - { - Byte byte_element = ((Number) element).byteValue(); - - for (int i = offset; i < this.buffer.length; i++) - { - if (this.buffer[i] == byte_element) - { - index = i; - break; - } - } - } - - else - { - // Aucune action - } - - return index; - } - - @Override - public int lastIndexOf(Object element) - { - int index = -1; - - if ((null != element) && (element instanceof Number)) - { - Byte byte_element = ((Number) element).byteValue(); - - for (int i = this.buffer.length - 1; i >= 0; i--) - { - if (this.buffer[i] == byte_element) - { - index = i; - break; - } - } - } - - else - { - // Aucune action - } - - return index; - } - - @Override - public ListIterator listIterator() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public ListIterator listIterator(int index) - { - // TODO Auto-generated method stub - return null; - } - - @Override - public BytesArray subList(int fromIndex, int toIndex) - { - BytesArray subBytesArray = new BytesArray(new byte[0]); - - if (fromIndex >= 0) - { - if ((toIndex > fromIndex) && (toIndex < this.buffer.length)) - { - int size = (toIndex - fromIndex) + 1; - byte[] buffer = new byte[size]; - - System.arraycopy(this.buffer, fromIndex, buffer, 0, size); - - subBytesArray = new BytesArray(buffer); - } - - else if ((toIndex < 0) || (toIndex >= this.buffer.length)) - { - int size = this.buffer.length - fromIndex; - byte[] buffer = new byte[size]; - - System.arraycopy(this.buffer, fromIndex, buffer, 0, size); - - subBytesArray = new BytesArray(buffer); - } - - else - { - - } - } - - return subBytesArray; - } - - /** - * Get bytes - * - * @return bytes - */ - public byte[] getBytes() - { - return this.buffer; - } - - /** - * Copy - * - * @return copied BytesArray - */ - public BytesArray copy() - { - return new BytesArray(this.buffer); - } - - /** - * Get index of element - * - * @param element - * @return index of element - */ - public List indexesOf(Object element) - { - List indexesList = new ArrayList(); - - if ((null != element) && (element instanceof Number)) - { - Byte byte_element = ((Number) element).byteValue(); - - for (int i = 0; i < this.buffer.length; i++) - { - if (this.buffer[i] == byte_element) - { - indexesList.add(i); - } - } - } - - else - { - // Aucune action - } - - return indexesList; - } - - /** - * - * @param value - * @return true if the BytesArray starts with the given value, else return false - */ - public boolean startsWith(byte value) - { - boolean retVal; - - if ((this.buffer.length > 0) && (this.buffer[0] == value)) - { - retVal = true; - } - - else - { - retVal = false; - } - - return retVal; - } - - /** - * - * @param value - * @return true if the BytesArray ends with the given value, else return false - */ - public boolean endsWith(byte value) - { - boolean retVal; - - if ((this.buffer.length > 0) && (this.buffer[this.buffer.length - 1] == value)) - { - retVal = true; - } - - else - { - retVal = false; - } - - return retVal; - } - - /** - * Get element count - * - * @param element - * @return element count - */ - public int count(Object element) - { - return this.indexesOf(element).size(); - } - - /** - * Split - * - * @param pattern - * @return split BytesArray - */ - public List split(Number pattern) - { - List patternsIndexesList = this.indexesOf(pattern.byteValue()); - List bytesArraysList = new ArrayList(); - - // Si le motif de séparation existe, copier chaque segment dans l'élément associé du tableau - if (false == patternsIndexesList.isEmpty()) - { - int begin_index = 0; - int end_index; - int segment_length; - int nb_segments = patternsIndexesList.size() + 1; - - for (int i = 0; i < nb_segments; i++) - { - end_index = (i < patternsIndexesList.size()) ? patternsIndexesList.get(i) : this.buffer.length; - - segment_length = end_index - begin_index; - - // Si la taille du segment est non nulle, - if (segment_length > 0) - { - byte[] segment = new byte[segment_length]; - - System.arraycopy(this.buffer, begin_index, segment, 0, segment_length); - - bytesArraysList.add(new BytesArray(segment)); - } - - // Sinon, - else - { - bytesArraysList.add(new BytesArray()); - } - - begin_index = end_index + 1; - } - } - - // Sinon, copier dans l'unique élément du tableau l'intégralité des données - else - { - bytesArraysList.add(new BytesArray(this.buffer)); - } - - return bytesArraysList; - } - - /** - * Extract parts in the ByteArray according specific 'begin' and 'end' patterns.
    - * NB : 'begin' and 'end' patterns shall be different. Use split() method if they don't.
    - *
    - * Example:
    - *
    - * BytesArray myArray(new byte[12] {42,14,20,98,34,47,14,61,17,98,110,25});
    - * List mySlicedArray = myArray.slice(14,98);
    - * mySlicedArray.get(0) contains {14,20,98}
    - * mySlicedArray.get(1) contains {14,61,17,98}
    - * - * @param beginPattern - * @param endPattern - * @return slices list - */ - public List slice(Number beginPattern, Number endPattern) - { - return this.slice(beginPattern, endPattern, -1, 0); - } - - /** - * - * @param beginPattern - * @param endPattern - * @param options - * @return slices list - */ - public List slice(Number beginPattern, Number endPattern, int options) - { - return this.slice(beginPattern, endPattern, -1, options); - } - - /** - * - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return slices list - */ - public List slice(Number beginPattern, Number endPattern, int occurences, int options) - { - List bytesArraysList; - - // ... Si l'appel de la fonction est "gourmand" (greedy), il n'y aura au mieux qu'un élément à retourner qui - // correspondra au plus grand segment [begin_pattern, end_pattern] du tableau - // NB le paramètre 'occurences' est alors non signifiant - if ((options & GREEDY) != 0) - { - bytesArraysList = this.sliceGreedy(beginPattern, endPattern, options); - } - - // ... Si l'appel de la fonction est "non gourmand" (not greedy), les éléments du tableau à retourner - // correspondront aux segments [begin_pattern, end_pattern] - // les plus rationnels, dans la limite des n premières occurences demandées - else - { - bytesArraysList = this.sliceNotGreedy(beginPattern, endPattern, occurences, options); - } - - return bytesArraysList; - } - - /** - * Add all byte - * - * @param bytesArray - * @return true if changed has been applied - */ - public boolean addAll(byte[] bytesArray) - { - boolean hasChanged = false; - - if (bytesArray != null && bytesArray.length > 0) - { - byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; - - // copie des données existantes - System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); - - // copie des nouvelles données - System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); - - this.buffer = newBytesArray; - hasChanged = true; - } - - else - { - hasChanged = false; - } - - return hasChanged; - } - - /** - * Add all byte at index - * - * @param index - * @param bytesArray - * @return true if changed has been applied - */ - public boolean addAll(int index, byte[] bytesArray) - { - boolean hasChanged = false; - - if ((index >= 0) && (index <= this.buffer.length)) - { - byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; - - // Cas ou on ajoute au début: - if (0 == index) - { - System.arraycopy(bytesArray, 0, newBytesArray, 0, bytesArray.length); - - // copie des données existantes - System.arraycopy(this.buffer, 0, newBytesArray, bytesArray.length, this.buffer.length); - } - - // Cas où on ajoute à la fin: - else if (this.buffer.length == index) - { - // copie des données existantes - System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); - - // copie des nouvelles données - System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); - } - - // Cas où on ajoute au milieu: - else - { - // copie des données existantes (premier segment) - System.arraycopy(this.buffer, 0, newBytesArray, 0, index); - - // copie des nouvelles données - System.arraycopy(bytesArray, 0, newBytesArray, index, bytesArray.length); - - // copie des données existantes (deuxième segment) - System.arraycopy(this.buffer, index, newBytesArray, index + bytesArray.length, this.buffer.length - index); - } - - this.buffer = newBytesArray; - hasChanged = true; - } - - else - { - hasChanged = false; - } - - return hasChanged; - } - - /** - * Compute the BytesArray checksum on 8bits (Byte) - * - * @return the computed checksum or 'null' if the BytesArray is empty - * - */ - public Byte checksum8() - { - Byte checksum = null; - - if (this.buffer.length > 0) - { - byte checksum_8 = 0; - - for (int i = 0; i < this.buffer.length; i++) - { - checksum_8 += this.buffer[i]; - } - - checksum = new Byte(checksum_8); - } - - else - { - - } - - return checksum; - } - - /** - * Compute the BytesArray checksum on 16 bits (Short) - * - * @return the computed checksum or 'null' if the BytesArray is empty - * - */ - public Short checksum16() - { - Short checksum = null; - - if (this.buffer.length > 0) - { - short checksum_16 = 0; - - for (int i = 0; i < this.buffer.length; i++) - { - checksum_16 += this.buffer[i]; - } - - checksum = new Short(checksum_16); - } - - else - { - - } - - return checksum; - } - - /** - * Compute the BytesArray checksum on 32 bits (Integer) - * - * @return the computed checksum or 'null' if the BytesArray is empty - * - */ - public Integer checksum32() - { - Integer checksum = null; - - if (this.buffer.length > 0) - { - int checksum_32 = 0; - - for (int i = 0; i < this.buffer.length; i++) - { - checksum_32 += this.buffer[i]; - } - - checksum = new Integer(checksum_32); - } - - else - { - - } - - return checksum; - } - - /** - * - * @param beginPattern - * @param endPattern - * @param options - * @return - */ - private List sliceGreedy(Number beginPattern, Number endPattern, int options) - { - List bytesArraysList = new ArrayList(); - - int beginIndex = this.indexOf(beginPattern.byteValue()); - - if (beginIndex >= 0) - { - int endIndex = this.lastIndexOf(endPattern.byteValue()); - - if (endIndex > 0) - { - if ((options & REMOVE_PATTERNS) != 0) - { - beginIndex++; - endIndex--; - } - - int size = endIndex - beginIndex + 1; - - if (size > 0) - { - byte[] bytesBuffer = new byte[size]; - - System.arraycopy(this.buffer, beginIndex, bytesBuffer, 0, size); - - bytesArraysList.add(new BytesArray(bytesBuffer)); - } - - else - { - bytesArraysList.add(new BytesArray(new byte[0])); - } - } - } - - else - { - - } - - return bytesArraysList; - } - - /** - * - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return - */ - private List sliceNotGreedy(Number beginPattern, Number endPattern, int occurences, int options) - { - List bytesArraysList = new ArrayList(); - - // Si la contiguité des segments est requise, - if ((options & CONTIGUOUS) != 0) - { - bytesArraysList = this.sliceContiguous(beginPattern, endPattern, occurences, options); - } - - else - { - bytesArraysList = this.sliceNotContiguous(beginPattern, endPattern, occurences, options); - } - - return bytesArraysList; - } - - /** - * - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return - */ - private List sliceContiguous(Number beginPattern, Number endPattern, int occurences, int options) - { - List bytesArraysList = new ArrayList(); - - int beginIndex = -1; - int endIndex = -1; - - for (int i = 0; i < this.buffer.length; i++) - { - // Traiter un motif DEBUT - if (this.buffer[i] == beginPattern.byteValue()) - { - if ((-1 == endIndex) || (this.buffer[i - 1] == endPattern.byteValue())) - { - beginIndex = i; - } - } - - // Traiter un motif FIN - else if (this.buffer[i] == endPattern.byteValue()) - { - // Si nous ne sommes pas au dernier élément du tableau - if (i < (this.buffer.length - 1)) - { - // Si le prochain caractère est le motif de début de trame - if (this.buffer[i + 1] == beginPattern.byteValue()) - { - endIndex = i; - - this.slicePart(bytesArraysList, beginIndex, endIndex, options); - - // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, - // interrompre le traitement - if ((occurences > 0) && (bytesArraysList.size() >= occurences)) - { - break; - } - } - } - - // Sinon, si nous sommes au dernier élément du tableau - else - { - endIndex = i; - - this.slicePart(bytesArraysList, beginIndex, endIndex, options); - - // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, - // interrompre le traitement - if ((occurences > 0) && (bytesArraysList.size() >= occurences)) - { - break; - } - } - } - - else - { - - } - } - - return bytesArraysList; - } - - /** - * - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return - */ - private List sliceNotContiguous(Number beginPattern, Number endPattern, int occurences, int options) - { - List bytesArraysList = new ArrayList(); - - int beginIndex = -1; - int endIndex = -1; - - for (int i = 0; i < this.buffer.length; i++) - { - if (this.buffer[i] == beginPattern.byteValue()) - { - beginIndex = i; - } - - else if (this.buffer[i] == endPattern.byteValue()) - { - endIndex = i; - - this.slicePart(bytesArraysList, beginIndex, endIndex, options); - - // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, - // interrompre le traitement - if ((occurences > 0) && (bytesArraysList.size() >= occurences)) - { - break; - } - } - - else - { - - } - } - - return bytesArraysList; - } - - /** - * - * @param bytesArraysList - * @param beginIndex - * @param endIndex - * @param options - */ - private void slicePart(List bytesArraysList, int beginIndex, int endIndex, int options) - { - if (beginIndex >= 0) - { - if ((options & REMOVE_PATTERNS) != 0) - { - beginIndex++; - endIndex--; - } - - BytesArray part = this.subList(beginIndex, endIndex); - - if (null != part) - { - bytesArraysList.add(part); - } - } - } - - /** - * @param index - */ - private void removeIndex(int index) - { - byte[] bytesArray = new byte[this.buffer.length - 1]; - - // Si l'index est le premier du tableau - if (0 == index) - { - System.arraycopy(this.buffer, 1, bytesArray, 0, bytesArray.length); - } - - // Sinon, si l'index est le dernier - else if ((this.buffer.length - 1) == index) - { - System.arraycopy(this.buffer, 0, bytesArray, 0, bytesArray.length); - } - - // Sinon, si l'index est au milieu du tableau - else - { - System.arraycopy(this.buffer, 0, bytesArray, 0, index); - - System.arraycopy(this.buffer, index + 1, // src , src_pos - bytesArray, index, // dest, dest_pos - bytesArray.length - index); // size - } - - this.buffer = bytesArray; - } - - private void removeSubList(int fromIndex, int toIndex) - { - byte[] bytesArray = new byte[this.buffer.length - (toIndex - fromIndex + 1)]; - - if (fromIndex > 0) - { - System.arraycopy(this.buffer, 0, bytesArray, 0, fromIndex); - } - if (toIndex + 1 < this.buffer.length) - { - System.arraycopy(this.buffer, toIndex + 1, bytesArray, fromIndex, bytesArray.length - fromIndex); - } - - this.buffer = bytesArray; - } +/** Bytes array */ +public class BytesArray implements List { + /** Default option */ + public static final int DEFAULT_OPTIONS = 0x0000; + + /** Greedy option */ + public static final int GREEDY = 0x0001; + + /** Contiguous option */ + public static final int CONTIGUOUS = 0x0002; + + /** Remove patterns option */ + public static final int REMOVE_PATTERNS = 0x0004; + + /** + * Convert int to byte[] + * + * @param value + * @param numberOfByte + * @return byte[] + * @throws NumberFormatException + */ + public static byte[] intToArrayOfByte(int value, int numberOfByte) throws NumberFormatException { + if (countBytes(value) > numberOfByte) { + throw new NumberFormatException( + "Value " + value + " can't be converted in a byte[" + numberOfByte + "]"); + } + + byte[] byteArray = new byte[numberOfByte]; + + for (int i = 0; i < byteArray.length; i++) { + if (i < Integer.BYTES) { + byteArray[byteArray.length - i - 1] = (byte) (value >> (i * 8)); + } + } + + return byteArray; + } + + /** + * Conver int to BytesArray + * + * @param value + * @param numberOfByte + * @return BytesArray + * @throws Exception + */ + public static BytesArray valueOf(int value, int numberOfByte) throws Exception { + return new BytesArray(intToArrayOfByte(value, numberOfByte)); + } + + private static int countBytes(int value) { + int tmp = Math.abs(value); + int count = 0; + + for (int i = 0; i < Integer.BYTES; i++) { + tmp = tmp >> 8; + count++; + if (tmp == 0) { + break; + } + } + return count; + } + + protected byte[] buffer; + + /** Default constructor */ + public BytesArray() { + this.buffer = new byte[0]; + } + + /** + * Constructor from array of byte + * + * @param bytesArray + */ + public BytesArray(byte[] bytesArray) { + this.buffer = bytesArray.clone(); + } + + @Override + public int size() { + return this.buffer.length; + } + + @Override + public boolean isEmpty() { + return (0 == this.buffer.length) ? true : false; + } + + @Override + public boolean contains(Object element) { + int index = this.indexOf(element); + return (index >= 0) ? true : false; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public Byte[] toArray() { + Byte[] bytesArray = new Byte[this.buffer.length]; + + for (int i = 0; i < this.buffer.length; i++) { + bytesArray[i] = this.buffer[i]; + } + + return bytesArray; + } + + @SuppressWarnings("unchecked") + @Override + public Byte[] toArray(Object[] a) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean add(Byte element) { + byte byte_element = element; + byte[] bytesArray = new byte[this.buffer.length + 1]; + + System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); + bytesArray[bytesArray.length - 1] = byte_element; + this.buffer = bytesArray; + + return true; + } + + @Override + public void add(int index, Byte element) { + if ((index < 0) || (index >= this.buffer.length)) { + this.add(element); + } else { + byte[] bytesArray = new byte[this.buffer.length + 1]; + + // Si l'index est le premier + if (0 == index) { + System.arraycopy(this.buffer, 0, bytesArray, 1, this.buffer.length); + } + + // Sinon, si l'index est le dernier + else if ((this.buffer.length - 1) == index) { + System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length - 1); + + bytesArray[bytesArray.length - 1] = this.buffer[this.buffer.length - 1]; + } + + // Sinon, si l'index est au milieu du tableau + else { + System.arraycopy(this.buffer, 0, bytesArray, 0, index - 1); + + System.arraycopy( + this.buffer, + index, // src , src_pos + bytesArray, + index + 1, // dest, dest_pos + this.buffer.length - index - 1); // size + } + + bytesArray[index] = element; + this.buffer = bytesArray; + } + } + + @Override + public boolean remove(Object element) { + boolean success = true; + + if ((null != element) && (element instanceof Number)) { + // Obtenir l'index de la première occurence de l'élément + int index = this.indexOf(element); + + // Si un tel élément a été trouvé, + if (index >= 0) { + this.removeIndex(index); + } + + // Sinon, + else { + success = false; + } + } else { + success = false; + } + + return success; + } + + @Override + public Byte remove(int index) { + Byte element = null; + + if ((index >= 0) && (index < this.buffer.length)) { + element = new Byte(this.buffer[index]); + this.removeIndex(index); + } else { + // Aucune action + } + + return element; + } + + /** + * Remove the buffer from an index to other + * + * @param fromIndex + * @param toIndex + * @return new length of buffer + */ + public int remove(int fromIndex, int toIndex) { + int length = 0; + + if ((fromIndex >= 0) && (toIndex < this.buffer.length) && (toIndex > fromIndex)) { + this.removeSubList(fromIndex, toIndex); + length = toIndex - fromIndex + 1; + } + + return length; + } + + @Override + public boolean containsAll(Collection c) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean addAll(Collection collection) { + boolean hasChanged; + + if ((null != collection) && (collection.size() > 0)) { + Object[] collectionArray = collection.toArray(); + byte[] bytesArray = new byte[this.buffer.length + collection.size()]; + + System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); + + int j = this.buffer.length; + for (int i = 0; i < collectionArray.length; i++) { + bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); + j++; + } + + this.buffer = bytesArray; + + hasChanged = true; + } else { + hasChanged = false; + } + + return hasChanged; + } + + @Override + public boolean addAll(int index, Collection collection) { + boolean hasChanged; + + if ((null != collection) + && (collection.size() > 0) + && (index >= 0) + && (index <= this.buffer.length)) { + Object[] collectionArray = collection.toArray(); + byte[] bytesArray = new byte[this.buffer.length + collection.size()]; + + // Cas ou on ajoute en début: + if (0 == index) { + for (int i = 0; i < collectionArray.length; i++) { + bytesArray[i] = ((Byte) collectionArray[i]).byteValue(); + } + + System.arraycopy(this.buffer, 0, bytesArray, collectionArray.length, this.buffer.length); + } + + // Cas où on ajoute à la fin: + else if (this.buffer.length == index) { + System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); + + int j = index; + for (int i = 0; i < collectionArray.length; i++) { + bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); + j++; + } + } + + // Cas où on ajoute au milieu: + else { + System.arraycopy(this.buffer, 0, bytesArray, 0, index); + + int j = index; + for (int i = 0; i < collectionArray.length; i++) { + bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); + j++; + } + + System.arraycopy( + this.buffer, + index, + bytesArray, + index + collectionArray.length, + this.buffer.length - index); + } + + this.buffer = bytesArray; + + hasChanged = true; + } else { + hasChanged = false; + } + + return hasChanged; + } + + @Override + public boolean removeAll(Collection c) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean retainAll(Collection c) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void clear() { + this.buffer = new byte[0]; + } + + @Override + public Byte get(int index) { + Byte element = null; + + if ((index >= 0) && (index < this.buffer.length)) { + element = new Byte(this.buffer[index]); + } else { + + } + + return element; + } + + @Override + public Byte set(int index, Byte element) { + Byte previous_element = null; + + if ((index >= 0) && (index < this.buffer.length)) { + previous_element = new Byte(this.buffer[index]); + this.buffer[index] = element; + } else { + previous_element = new Byte(this.buffer[this.buffer.length - 1]); + this.buffer[this.buffer.length - 1] = element; + } + + return new Byte(previous_element); + } + + @Override + public int indexOf(Object element) { + return this.indexOf(element, 0); + } + + /** + * Get the buffer element of the given index + * + * @param element + * @param offset + * @return the element + */ + public int indexOf(Object element, int offset) { + int index = -1; + + if ((null != element) + && (element instanceof Number) + && offset >= 0 + && offset < this.buffer.length) { + Byte byte_element = ((Number) element).byteValue(); + + for (int i = offset; i < this.buffer.length; i++) { + if (this.buffer[i] == byte_element) { + index = i; + break; + } + } + } else { + // Aucune action + } + + return index; + } + + @Override + public int lastIndexOf(Object element) { + int index = -1; + + if ((null != element) && (element instanceof Number)) { + Byte byte_element = ((Number) element).byteValue(); + + for (int i = this.buffer.length - 1; i >= 0; i--) { + if (this.buffer[i] == byte_element) { + index = i; + break; + } + } + } else { + // Aucune action + } + + return index; + } + + @Override + public ListIterator listIterator() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ListIterator listIterator(int index) { + // TODO Auto-generated method stub + return null; + } + + @Override + public BytesArray subList(int fromIndex, int toIndex) { + BytesArray subBytesArray = new BytesArray(new byte[0]); + + if (fromIndex >= 0) { + if ((toIndex > fromIndex) && (toIndex < this.buffer.length)) { + int size = (toIndex - fromIndex) + 1; + byte[] buffer = new byte[size]; + + System.arraycopy(this.buffer, fromIndex, buffer, 0, size); + + subBytesArray = new BytesArray(buffer); + } else if ((toIndex < 0) || (toIndex >= this.buffer.length)) { + int size = this.buffer.length - fromIndex; + byte[] buffer = new byte[size]; + + System.arraycopy(this.buffer, fromIndex, buffer, 0, size); + + subBytesArray = new BytesArray(buffer); + } else { + + } + } + + return subBytesArray; + } + + /** + * Get bytes + * + * @return bytes + */ + public byte[] getBytes() { + return this.buffer; + } + + /** + * Copy + * + * @return copied BytesArray + */ + public BytesArray copy() { + return new BytesArray(this.buffer); + } + + /** + * Get index of element + * + * @param element + * @return index of element + */ + public List indexesOf(Object element) { + List indexesList = new ArrayList(); + + if ((null != element) && (element instanceof Number)) { + Byte byte_element = ((Number) element).byteValue(); + + for (int i = 0; i < this.buffer.length; i++) { + if (this.buffer[i] == byte_element) { + indexesList.add(i); + } + } + } else { + // Aucune action + } + + return indexesList; + } + + /** + * @param value + * @return true if the BytesArray starts with the given value, else return false + */ + public boolean startsWith(byte value) { + boolean retVal; + + if ((this.buffer.length > 0) && (this.buffer[0] == value)) { + retVal = true; + } else { + retVal = false; + } + + return retVal; + } + + /** + * @param value + * @return true if the BytesArray ends with the given value, else return false + */ + public boolean endsWith(byte value) { + boolean retVal; + + if ((this.buffer.length > 0) && (this.buffer[this.buffer.length - 1] == value)) { + retVal = true; + } else { + retVal = false; + } + + return retVal; + } + + /** + * Get element count + * + * @param element + * @return element count + */ + public int count(Object element) { + return this.indexesOf(element).size(); + } + + /** + * Split + * + * @param pattern + * @return split BytesArray + */ + public List split(Number pattern) { + List patternsIndexesList = this.indexesOf(pattern.byteValue()); + List bytesArraysList = new ArrayList(); + + // Si le motif de séparation existe, copier chaque segment dans l'élément associé du tableau + if (false == patternsIndexesList.isEmpty()) { + int begin_index = 0; + int end_index; + int segment_length; + int nb_segments = patternsIndexesList.size() + 1; + + for (int i = 0; i < nb_segments; i++) { + end_index = + (i < patternsIndexesList.size()) ? patternsIndexesList.get(i) : this.buffer.length; + + segment_length = end_index - begin_index; + + // Si la taille du segment est non nulle, + if (segment_length > 0) { + byte[] segment = new byte[segment_length]; + + System.arraycopy(this.buffer, begin_index, segment, 0, segment_length); + + bytesArraysList.add(new BytesArray(segment)); + } + + // Sinon, + else { + bytesArraysList.add(new BytesArray()); + } + + begin_index = end_index + 1; + } + } + + // Sinon, copier dans l'unique élément du tableau l'intégralité des données + else { + bytesArraysList.add(new BytesArray(this.buffer)); + } + + return bytesArraysList; + } + + /** + * Extract parts in the ByteArray according specific 'begin' and 'end' patterns.
    + * NB : 'begin' and 'end' patterns shall be different. Use split() method if they don't.
    + *
    + * Example:
    + *
    + * BytesArray myArray(new byte[12] {42,14,20,98,34,47,14,61,17,98,110,25});
    + * List mySlicedArray = myArray.slice(14,98);
    + * mySlicedArray.get(0) contains {14,20,98}
    + * mySlicedArray.get(1) contains {14,61,17,98}
    + * + * @param beginPattern + * @param endPattern + * @return slices list + */ + public List slice(Number beginPattern, Number endPattern) { + return this.slice(beginPattern, endPattern, -1, 0); + } + + /** + * @param beginPattern + * @param endPattern + * @param options + * @return slices list + */ + public List slice(Number beginPattern, Number endPattern, int options) { + return this.slice(beginPattern, endPattern, -1, options); + } + + /** + * @param beginPattern + * @param endPattern + * @param occurences + * @param options + * @return slices list + */ + public List slice( + Number beginPattern, Number endPattern, int occurences, int options) { + List bytesArraysList; + + // ... Si l'appel de la fonction est "gourmand" (greedy), il n'y aura au mieux qu'un élément à + // retourner qui + // correspondra au plus grand segment [begin_pattern, end_pattern] du tableau + // NB le paramètre 'occurences' est alors non signifiant + if ((options & GREEDY) != 0) { + bytesArraysList = this.sliceGreedy(beginPattern, endPattern, options); + } + + // ... Si l'appel de la fonction est "non gourmand" (not greedy), les éléments du tableau à + // retourner + // correspondront aux segments [begin_pattern, end_pattern] + // les plus rationnels, dans la limite des n premières occurences demandées + else { + bytesArraysList = this.sliceNotGreedy(beginPattern, endPattern, occurences, options); + } + + return bytesArraysList; + } + + /** + * Add all byte + * + * @param bytesArray + * @return true if changed has been applied + */ + public boolean addAll(byte[] bytesArray) { + boolean hasChanged = false; + + if (bytesArray != null && bytesArray.length > 0) { + byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; + + // copie des données existantes + System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); + + // copie des nouvelles données + System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); + + this.buffer = newBytesArray; + hasChanged = true; + } else { + hasChanged = false; + } + + return hasChanged; + } + + /** + * Add all byte at index + * + * @param index + * @param bytesArray + * @return true if changed has been applied + */ + public boolean addAll(int index, byte[] bytesArray) { + boolean hasChanged = false; + + if ((index >= 0) && (index <= this.buffer.length)) { + byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; + + // Cas ou on ajoute au début: + if (0 == index) { + System.arraycopy(bytesArray, 0, newBytesArray, 0, bytesArray.length); + + // copie des données existantes + System.arraycopy(this.buffer, 0, newBytesArray, bytesArray.length, this.buffer.length); + } + + // Cas où on ajoute à la fin: + else if (this.buffer.length == index) { + // copie des données existantes + System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); + + // copie des nouvelles données + System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); + } + + // Cas où on ajoute au milieu: + else { + // copie des données existantes (premier segment) + System.arraycopy(this.buffer, 0, newBytesArray, 0, index); + + // copie des nouvelles données + System.arraycopy(bytesArray, 0, newBytesArray, index, bytesArray.length); + + // copie des données existantes (deuxième segment) + System.arraycopy( + this.buffer, + index, + newBytesArray, + index + bytesArray.length, + this.buffer.length - index); + } + + this.buffer = newBytesArray; + hasChanged = true; + } else { + hasChanged = false; + } + + return hasChanged; + } + + /** + * Compute the BytesArray checksum on 8bits (Byte) + * + * @return the computed checksum or 'null' if the BytesArray is empty + */ + public Byte checksum8() { + Byte checksum = null; + + if (this.buffer.length > 0) { + byte checksum_8 = 0; + + for (int i = 0; i < this.buffer.length; i++) { + checksum_8 += this.buffer[i]; + } + + checksum = new Byte(checksum_8); + } else { + + } + + return checksum; + } + + /** + * Compute the BytesArray checksum on 16 bits (Short) + * + * @return the computed checksum or 'null' if the BytesArray is empty + */ + public Short checksum16() { + Short checksum = null; + + if (this.buffer.length > 0) { + short checksum_16 = 0; + + for (int i = 0; i < this.buffer.length; i++) { + checksum_16 += this.buffer[i]; + } + + checksum = new Short(checksum_16); + } else { + + } + + return checksum; + } + + /** + * Compute the BytesArray checksum on 32 bits (Integer) + * + * @return the computed checksum or 'null' if the BytesArray is empty + */ + public Integer checksum32() { + Integer checksum = null; + + if (this.buffer.length > 0) { + int checksum_32 = 0; + + for (int i = 0; i < this.buffer.length; i++) { + checksum_32 += this.buffer[i]; + } + + checksum = new Integer(checksum_32); + } else { + + } + + return checksum; + } + + /** + * @param beginPattern + * @param endPattern + * @param options + * @return + */ + private List sliceGreedy(Number beginPattern, Number endPattern, int options) { + List bytesArraysList = new ArrayList(); + + int beginIndex = this.indexOf(beginPattern.byteValue()); + + if (beginIndex >= 0) { + int endIndex = this.lastIndexOf(endPattern.byteValue()); + + if (endIndex > 0) { + if ((options & REMOVE_PATTERNS) != 0) { + beginIndex++; + endIndex--; + } + + int size = endIndex - beginIndex + 1; + + if (size > 0) { + byte[] bytesBuffer = new byte[size]; + + System.arraycopy(this.buffer, beginIndex, bytesBuffer, 0, size); + + bytesArraysList.add(new BytesArray(bytesBuffer)); + } else { + bytesArraysList.add(new BytesArray(new byte[0])); + } + } + } else { + + } + + return bytesArraysList; + } + + /** + * @param beginPattern + * @param endPattern + * @param occurences + * @param options + * @return + */ + private List sliceNotGreedy( + Number beginPattern, Number endPattern, int occurences, int options) { + List bytesArraysList = new ArrayList(); + + // Si la contiguité des segments est requise, + if ((options & CONTIGUOUS) != 0) { + bytesArraysList = this.sliceContiguous(beginPattern, endPattern, occurences, options); + } else { + bytesArraysList = this.sliceNotContiguous(beginPattern, endPattern, occurences, options); + } + + return bytesArraysList; + } + + /** + * @param beginPattern + * @param endPattern + * @param occurences + * @param options + * @return + */ + private List sliceContiguous( + Number beginPattern, Number endPattern, int occurences, int options) { + List bytesArraysList = new ArrayList(); + + int beginIndex = -1; + int endIndex = -1; + + for (int i = 0; i < this.buffer.length; i++) { + // Traiter un motif DEBUT + if (this.buffer[i] == beginPattern.byteValue()) { + if ((-1 == endIndex) || (this.buffer[i - 1] == endPattern.byteValue())) { + beginIndex = i; + } + } + + // Traiter un motif FIN + else if (this.buffer[i] == endPattern.byteValue()) { + // Si nous ne sommes pas au dernier élément du tableau + if (i < (this.buffer.length - 1)) { + // Si le prochain caractère est le motif de début de trame + if (this.buffer[i + 1] == beginPattern.byteValue()) { + endIndex = i; + + this.slicePart(bytesArraysList, beginIndex, endIndex, options); + + // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, + // interrompre le traitement + if ((occurences > 0) && (bytesArraysList.size() >= occurences)) { + break; + } + } + } + + // Sinon, si nous sommes au dernier élément du tableau + else { + endIndex = i; + + this.slicePart(bytesArraysList, beginIndex, endIndex, options); + + // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, + // interrompre le traitement + if ((occurences > 0) && (bytesArraysList.size() >= occurences)) { + break; + } + } + } else { + + } + } + + return bytesArraysList; + } + + /** + * @param beginPattern + * @param endPattern + * @param occurences + * @param options + * @return + */ + private List sliceNotContiguous( + Number beginPattern, Number endPattern, int occurences, int options) { + List bytesArraysList = new ArrayList(); + + int beginIndex = -1; + int endIndex = -1; + + for (int i = 0; i < this.buffer.length; i++) { + if (this.buffer[i] == beginPattern.byteValue()) { + beginIndex = i; + } else if (this.buffer[i] == endPattern.byteValue()) { + endIndex = i; + + this.slicePart(bytesArraysList, beginIndex, endIndex, options); + + // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, + // interrompre le traitement + if ((occurences > 0) && (bytesArraysList.size() >= occurences)) { + break; + } + } else { + + } + } + + return bytesArraysList; + } + + /** + * @param bytesArraysList + * @param beginIndex + * @param endIndex + * @param options + */ + private void slicePart( + List bytesArraysList, int beginIndex, int endIndex, int options) { + if (beginIndex >= 0) { + if ((options & REMOVE_PATTERNS) != 0) { + beginIndex++; + endIndex--; + } + + BytesArray part = this.subList(beginIndex, endIndex); + + if (null != part) { + bytesArraysList.add(part); + } + } + } + + /** + * @param index + */ + private void removeIndex(int index) { + byte[] bytesArray = new byte[this.buffer.length - 1]; + + // Si l'index est le premier du tableau + if (0 == index) { + System.arraycopy(this.buffer, 1, bytesArray, 0, bytesArray.length); + } + + // Sinon, si l'index est le dernier + else if ((this.buffer.length - 1) == index) { + System.arraycopy(this.buffer, 0, bytesArray, 0, bytesArray.length); + } + + // Sinon, si l'index est au milieu du tableau + else { + System.arraycopy(this.buffer, 0, bytesArray, 0, index); + + System.arraycopy( + this.buffer, + index + 1, // src , src_pos + bytesArray, + index, // dest, dest_pos + bytesArray.length - index); // size + } + + this.buffer = bytesArray; + } + + private void removeSubList(int fromIndex, int toIndex) { + byte[] bytesArray = new byte[this.buffer.length - (toIndex - fromIndex + 1)]; + + if (fromIndex > 0) { + System.arraycopy(this.buffer, 0, bytesArray, 0, fromIndex); + } + if (toIndex + 1 < this.buffer.length) { + System.arraycopy( + this.buffer, toIndex + 1, bytesArray, fromIndex, bytesArray.length - fromIndex); + } + + this.buffer = bytesArray; + } } diff --git a/src/main/java/enedis/lab/types/DataArrayList.java b/src/main/java/enedis/lab/types/DataArrayList.java index 1f1348a..c86a3ca 100644 --- a/src/main/java/enedis/lab/types/DataArrayList.java +++ b/src/main/java/enedis/lab/types/DataArrayList.java @@ -20,7 +20,6 @@ import java.util.Collection; import java.util.List; import java.util.RandomAccess; - import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -30,308 +29,282 @@ * * @param the type of elements in this data list */ -public class DataArrayList extends ArrayList implements DataList -{ - private static final long serialVersionUID = 3116080161929203526L; - - private static final int JSON_INDENT_FACTOR = 0; - - /** - * Returns a fixed-size data list backed by the specified array. (Changes to - * the returned data list "write through" to the array.) This method acts - * as bridge between array-based and collection-based APIs, in - * combination with {@link Collection#toArray}. The returned list is - * serializable and implements {@link RandomAccess}. - * - *

    This method also provides a convenient way to create a fixed-size - * data list initialized to contain several elements: - *

    -     *     DataList<String> stooges = DataArrayList.asList("Larry", "Moe", "Curly");
    -     * 
    - * - * @param the class of the objects in the array - * @param items the array by which the data list will be backed - * @return a data list view of the specified array - */ - @SafeVarargs - public static DataList asList(E... items) - { - DataList dataList = new DataArrayList(); - - for(E item : items) - { - dataList.add(item); - } - - return dataList; - } - - /** - * Instantiate a data list from a File - * - * @param the class of the objects in the list - * @param file - * @param clazz - * - * @return a data list - * @throws JSONException - * @throws IOException - */ - public static DataList fromFile(File file,Class clazz) throws JSONException, IOException - { - InputStream stream = new FileInputStream(file); - - return fromStream(stream,clazz); - } - - /** - * Instantiate a data list from a Stream - * - * @param the class of the objects in the list - * @param stream - * @param clazz - * - * @return a data list - * @throws JSONException - * @throws IOException - */ - public static DataList fromStream(InputStream stream,Class clazz) throws JSONException, IOException - { - byte[] buffer = new byte[stream.available()]; - stream.read(buffer); - stream.close(); - String text = new String(buffer); - - return fromString(text,clazz); - } - - /** - * Instantiate a data list from a String - * - * @param the class of the objects in the list - * @param text - * @param clazz - * - * @return a data list - * @throws JSONException - */ - public static DataList fromString(String text,Class clazz) throws JSONException - { - if (text == null) - { - return null; - } - - JSONArray jsonArray = new JSONArray(text); - - return fromJSON(jsonArray,clazz); - } - - /** - * Instantiate a data list from a JSONObject - * - * @param the class of the objects in the list - * @param jsonArray - * @param clazz - * - * @return a data list - * @throws JSONException - */ - public static DataList fromJSON(JSONArray jsonArray, Class clazz) throws JSONException - { - if(jsonArray == null) - { - return null; - } - DataList dataList = new DataArrayList(); - for(int i = 0; i < jsonArray.length(); i++) - { - Object jsonItem = jsonArray.get(i); - E dataListItem; - try - { - if(JSONObject.NULL.equals(jsonItem)) - { - dataListItem = null; - } - else if(clazz.isAssignableFrom(jsonItem.getClass())) - { - dataListItem = clazz.cast(jsonItem); - } - else if(Enum.class.isAssignableFrom(clazz)) - { - dataListItem = convertToEnum(jsonItem,clazz); - } - else if(LocalDateTime.class.isAssignableFrom(clazz)) - { - dataListItem = convertToLocalDateTime(jsonItem,clazz); - } - else if(List.class.isAssignableFrom(clazz)) - { - dataListItem = convertToDataList(jsonItem,clazz); - } - else if(DataDictionary.class.isAssignableFrom(clazz)) - { - dataListItem = convertToDataDictionary(jsonItem,clazz); - } - else - { - throw new IllegalArgumentException("Cannot convert " + jsonItem.getClass().getName() + " to " + clazz.getName()); - } - } - catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) - { - throw new IllegalArgumentException("Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getMessage() + ")",e); - } - catch (InvocationTargetException e) - { - throw new IllegalArgumentException("Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getTargetException().getMessage() + ")",e); - } - dataList.add(dataListItem); - } - - return dataList; - } - /** - * Constructs an empty data list with an initial capacity of ten. - */ - public DataArrayList() - { - super(); - } - - /** - * Constructs a data list containing the elements of the specified - * collection, in the order they are returned by the collection's - * iterator. - * - * @param collection the collection whose elements are to be placed into this data list - * @throws NullPointerException if the specified collection is null - */ - public DataArrayList(Collection collection) - { - super(collection); - } - - @Override - public JSONArray toJSON() - { - JSONArray jsonArray = new JSONArray(); - - for(E item : this) - { - Object objectItem; - if(item instanceof DataDictionary) - { - DataDictionary dictionaryItem = (DataDictionary)item; - objectItem = dictionaryItem.toJSON(); - } - else - { - objectItem = item; - } - jsonArray.put(objectItem); - } - - return jsonArray; - } - - @Override - public void toFile(File file, int indentFactor) throws IOException - { - OutputStream stream = new FileOutputStream(file); - - this.toStream(stream, indentFactor); - } - - @Override - public void toStream(OutputStream stream, int indentFactor) throws IOException - { - String text = this.toString(indentFactor); - - stream.write(text.getBytes()); - } - - @Override - public String toString() - { - return this.toString(JSON_INDENT_FACTOR); - } - - @Override - public String toString(int identFactor) - { - return this.toJSON().toString(identFactor); - } - - @SuppressWarnings("unchecked") - private static E convertToDataDictionary(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException - { - E result; - - if(value instanceof JSONObject) - { - Method method = clazz.getMethod("fromJSON",JSONObject.class,Class.class); - result = (E) method.invoke(null,value,clazz); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToDataList(Object value,Class clazz) - { - E result; - - if(value instanceof JSONArray) - { - result = (E) DataArrayList.fromJSON((JSONArray)value, Object.class); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToEnum(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException - { - E result; - - if(value instanceof String) - { - Method method = clazz.getDeclaredMethod("valueOf", String.class); - result = (E) method.invoke(null, (String)value); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToLocalDateTime(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException - { - E result; - - if(value instanceof String) - { - Method method = clazz.getDeclaredMethod("parse", CharSequence.class); - result = (E) method.invoke(null, (String)value); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } +public class DataArrayList extends ArrayList implements DataList { + private static final long serialVersionUID = 3116080161929203526L; + + private static final int JSON_INDENT_FACTOR = 0; + + /** + * Returns a fixed-size data list backed by the specified array. (Changes to the returned data + * list "write through" to the array.) This method acts as bridge between array-based and + * collection-based APIs, in combination with {@link Collection#toArray}. The returned list is + * serializable and implements {@link RandomAccess}. + * + *

    This method also provides a convenient way to create a fixed-size data list initialized to + * contain several elements: + * + *

    +   *     DataList<String> stooges = DataArrayList.asList("Larry", "Moe", "Curly");
    +   * 
    + * + * @param the class of the objects in the array + * @param items the array by which the data list will be backed + * @return a data list view of the specified array + */ + @SafeVarargs + public static DataList asList(E... items) { + DataList dataList = new DataArrayList(); + + for (E item : items) { + dataList.add(item); + } + + return dataList; + } + + /** + * Instantiate a data list from a File + * + * @param the class of the objects in the list + * @param file + * @param clazz + * @return a data list + * @throws JSONException + * @throws IOException + */ + public static DataList fromFile(File file, Class clazz) + throws JSONException, IOException { + InputStream stream = new FileInputStream(file); + + return fromStream(stream, clazz); + } + + /** + * Instantiate a data list from a Stream + * + * @param the class of the objects in the list + * @param stream + * @param clazz + * @return a data list + * @throws JSONException + * @throws IOException + */ + public static DataList fromStream(InputStream stream, Class clazz) + throws JSONException, IOException { + byte[] buffer = new byte[stream.available()]; + stream.read(buffer); + stream.close(); + String text = new String(buffer); + + return fromString(text, clazz); + } + + /** + * Instantiate a data list from a String + * + * @param the class of the objects in the list + * @param text + * @param clazz + * @return a data list + * @throws JSONException + */ + public static DataList fromString(String text, Class clazz) throws JSONException { + if (text == null) { + return null; + } + + JSONArray jsonArray = new JSONArray(text); + + return fromJSON(jsonArray, clazz); + } + + /** + * Instantiate a data list from a JSONObject + * + * @param the class of the objects in the list + * @param jsonArray + * @param clazz + * @return a data list + * @throws JSONException + */ + public static DataList fromJSON(JSONArray jsonArray, Class clazz) throws JSONException { + if (jsonArray == null) { + return null; + } + DataList dataList = new DataArrayList(); + for (int i = 0; i < jsonArray.length(); i++) { + Object jsonItem = jsonArray.get(i); + E dataListItem; + try { + if (JSONObject.NULL.equals(jsonItem)) { + dataListItem = null; + } else if (clazz.isAssignableFrom(jsonItem.getClass())) { + dataListItem = clazz.cast(jsonItem); + } else if (Enum.class.isAssignableFrom(clazz)) { + dataListItem = convertToEnum(jsonItem, clazz); + } else if (LocalDateTime.class.isAssignableFrom(clazz)) { + dataListItem = convertToLocalDateTime(jsonItem, clazz); + } else if (List.class.isAssignableFrom(clazz)) { + dataListItem = convertToDataList(jsonItem, clazz); + } else if (DataDictionary.class.isAssignableFrom(clazz)) { + dataListItem = convertToDataDictionary(jsonItem, clazz); + } else { + throw new IllegalArgumentException( + "Cannot convert " + jsonItem.getClass().getName() + " to " + clazz.getName()); + } + } catch (NoSuchMethodException + | SecurityException + | IllegalAccessException + | IllegalArgumentException e) { + throw new IllegalArgumentException( + "Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getMessage() + ")", + e); + } catch (InvocationTargetException e) { + throw new IllegalArgumentException( + "Cannot convert " + + jsonItem + + " to " + + clazz.getName() + + " (" + + e.getTargetException().getMessage() + + ")", + e); + } + dataList.add(dataListItem); + } + + return dataList; + } + + /** Constructs an empty data list with an initial capacity of ten. */ + public DataArrayList() { + super(); + } + + /** + * Constructs a data list containing the elements of the specified collection, in the order they + * are returned by the collection's iterator. + * + * @param collection the collection whose elements are to be placed into this data list + * @throws NullPointerException if the specified collection is null + */ + public DataArrayList(Collection collection) { + super(collection); + } + + @Override + public JSONArray toJSON() { + JSONArray jsonArray = new JSONArray(); + + for (E item : this) { + Object objectItem; + if (item instanceof DataDictionary) { + DataDictionary dictionaryItem = (DataDictionary) item; + objectItem = dictionaryItem.toJSON(); + } else { + objectItem = item; + } + jsonArray.put(objectItem); + } + + return jsonArray; + } + + @Override + public void toFile(File file, int indentFactor) throws IOException { + OutputStream stream = new FileOutputStream(file); + + this.toStream(stream, indentFactor); + } + + @Override + public void toStream(OutputStream stream, int indentFactor) throws IOException { + String text = this.toString(indentFactor); + + stream.write(text.getBytes()); + } + + @Override + public String toString() { + return this.toString(JSON_INDENT_FACTOR); + } + + @Override + public String toString(int identFactor) { + return this.toJSON().toString(identFactor); + } + + @SuppressWarnings("unchecked") + private static E convertToDataDictionary(Object value, Class clazz) + throws NoSuchMethodException, + SecurityException, + IllegalAccessException, + IllegalArgumentException, + InvocationTargetException { + E result; + + if (value instanceof JSONObject) { + Method method = clazz.getMethod("fromJSON", JSONObject.class, Class.class); + result = (E) method.invoke(null, value, clazz); + } else { + throw new IllegalArgumentException( + "Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } + + @SuppressWarnings("unchecked") + private static E convertToDataList(Object value, Class clazz) { + E result; + + if (value instanceof JSONArray) { + result = (E) DataArrayList.fromJSON((JSONArray) value, Object.class); + } else { + throw new IllegalArgumentException( + "Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } + + @SuppressWarnings("unchecked") + private static E convertToEnum(Object value, Class clazz) + throws NoSuchMethodException, + SecurityException, + IllegalAccessException, + IllegalArgumentException, + InvocationTargetException { + E result; + + if (value instanceof String) { + Method method = clazz.getDeclaredMethod("valueOf", String.class); + result = (E) method.invoke(null, (String) value); + } else { + throw new IllegalArgumentException( + "Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } + + @SuppressWarnings("unchecked") + private static E convertToLocalDateTime(Object value, Class clazz) + throws NoSuchMethodException, + SecurityException, + IllegalAccessException, + IllegalArgumentException, + InvocationTargetException { + E result; + + if (value instanceof String) { + Method method = clazz.getDeclaredMethod("parse", CharSequence.class); + result = (E) method.invoke(null, (String) value); + } else { + throw new IllegalArgumentException( + "Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); + } + + return result; + } } diff --git a/src/main/java/enedis/lab/types/DataDictionary.java b/src/main/java/enedis/lab/types/DataDictionary.java index 56f3bfe..8746b9a 100644 --- a/src/main/java/enedis/lab/types/DataDictionary.java +++ b/src/main/java/enedis/lab/types/DataDictionary.java @@ -11,136 +11,126 @@ import java.io.IOException; import java.io.OutputStream; import java.util.Map; - import org.json.JSONObject; -/** - * Interface used to handle data dictionaries - */ -public interface DataDictionary extends Cloneable -{ - /** - * Check key exists - * - * @param key - * Key to check - * - * @return true if key exists, false otherwise - */ - public boolean exists(String key); - - /** - * Get the datadictionary current map keys list - * - * @return the datadictionary current map keys list - */ - public String[] keys(); - - /** - * Check if the datadictionary map contains the given key - * - * @param key - * @return true if the datadictionary map contains the given key - */ - public boolean existsInKeys(String key); - - /** - * Add the given key in the datadictionary map, if the given key already exists, do nothing - * - * @param key - */ - public void addKey(String key); - - /** - * Remove the given key in the datadictionary map - * - * @param key - */ - public void removeKey(String key); - - /** - * Clear the datadictionary map - */ - public void clear(); - - /** - * Get value of the given key - * - * @param key - * @return value of the given key - */ - public Object get(String key); - - /** - * Set value of the given key - * - * @param key - * @param value - * @throws DataDictionaryException - */ - public void set(String key, Object value) throws DataDictionaryException; - - /** - * Copy an other datadictionary into this one - * - * @param other - * @throws DataDictionaryException - */ - public void copy(DataDictionary other) throws DataDictionaryException; - - /** - * Convert this dataditionary to JSON - * - * @return a JSONObject - */ - public JSONObject toJSON(); - - /** - * Convert this dataditionary to Map - * - * @return a JSONObject - */ - public Map toMap(); - - /** - * Convert the given datadictionary to a File - * - * @param file - * @param indentFactor - * @throws IOException - */ - public void toFile(File file, int indentFactor) throws IOException; - - /** - * Convert the given datadictionary to a Stream - * - * @param stream - * @param indentFactor - * @throws IOException - */ - public void toStream(OutputStream stream, int indentFactor) throws IOException; - - @Override - public String toString(); - - /** - * Convert this dataditionary to a String - * - * @param indentFactor - * @return a JSONObject - */ - public String toString(int indentFactor); - - /** - * Convert this dataditionary to Map - * - * @return a JSONObject - * @throws CloneNotSupportedException - */ - public Object clone() throws CloneNotSupportedException; - - /** - * Print this datactionary in the console - */ - public void print(); +/** Interface used to handle data dictionaries */ +public interface DataDictionary extends Cloneable { + /** + * Check key exists + * + * @param key Key to check + * @return true if key exists, false otherwise + */ + public boolean exists(String key); + + /** + * Get the datadictionary current map keys list + * + * @return the datadictionary current map keys list + */ + public String[] keys(); + + /** + * Check if the datadictionary map contains the given key + * + * @param key + * @return true if the datadictionary map contains the given key + */ + public boolean existsInKeys(String key); + + /** + * Add the given key in the datadictionary map, if the given key already exists, do nothing + * + * @param key + */ + public void addKey(String key); + + /** + * Remove the given key in the datadictionary map + * + * @param key + */ + public void removeKey(String key); + + /** Clear the datadictionary map */ + public void clear(); + + /** + * Get value of the given key + * + * @param key + * @return value of the given key + */ + public Object get(String key); + + /** + * Set value of the given key + * + * @param key + * @param value + * @throws DataDictionaryException + */ + public void set(String key, Object value) throws DataDictionaryException; + + /** + * Copy an other datadictionary into this one + * + * @param other + * @throws DataDictionaryException + */ + public void copy(DataDictionary other) throws DataDictionaryException; + + /** + * Convert this dataditionary to JSON + * + * @return a JSONObject + */ + public JSONObject toJSON(); + + /** + * Convert this dataditionary to Map + * + * @return a JSONObject + */ + public Map toMap(); + + /** + * Convert the given datadictionary to a File + * + * @param file + * @param indentFactor + * @throws IOException + */ + public void toFile(File file, int indentFactor) throws IOException; + + /** + * Convert the given datadictionary to a Stream + * + * @param stream + * @param indentFactor + * @throws IOException + */ + public void toStream(OutputStream stream, int indentFactor) throws IOException; + + @Override + public String toString(); + + /** + * Convert this dataditionary to a String + * + * @param indentFactor + * @return a JSONObject + */ + public String toString(int indentFactor); + + /** + * Convert this dataditionary to Map + * + * @return a JSONObject + * @throws CloneNotSupportedException + */ + public Object clone() throws CloneNotSupportedException; + + /** Print this datactionary in the console */ + public void print(); } diff --git a/src/main/java/enedis/lab/types/DataDictionaryException.java b/src/main/java/enedis/lab/types/DataDictionaryException.java index 516f154..87ba87c 100644 --- a/src/main/java/enedis/lab/types/DataDictionaryException.java +++ b/src/main/java/enedis/lab/types/DataDictionaryException.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,64 +7,54 @@ package enedis.lab.types; -/** - * Data dictionary exception - */ -public class DataDictionaryException extends Exception -{ - - private static final long serialVersionUID = -7967428428453584771L; - - /** - * Default constructor - */ - public DataDictionaryException() - { - super(); - } - - /** - * Constructor using message, cause, enable suppression flag and writable stack trace flag - * - * @param message - * @param cause - * @param enableSuppression - * @param writableStackTrace - */ - public DataDictionaryException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) - { - super(message, cause, enableSuppression, writableStackTrace); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public DataDictionaryException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public DataDictionaryException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public DataDictionaryException(Throwable cause) - { - super(cause); - } - +/** Data dictionary exception */ +public class DataDictionaryException extends Exception { + + private static final long serialVersionUID = -7967428428453584771L; + + /** Default constructor */ + public DataDictionaryException() { + super(); + } + + /** + * Constructor using message, cause, enable suppression flag and writable stack trace flag + * + * @param message + * @param cause + * @param enableSuppression + * @param writableStackTrace + */ + public DataDictionaryException( + String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public DataDictionaryException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public DataDictionaryException(String message) { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public DataDictionaryException(Throwable cause) { + super(cause); + } } diff --git a/src/main/java/enedis/lab/types/DataList.java b/src/main/java/enedis/lab/types/DataList.java index 12eed5f..5266a08 100644 --- a/src/main/java/enedis/lab/types/DataList.java +++ b/src/main/java/enedis/lab/types/DataList.java @@ -11,47 +11,47 @@ import java.io.IOException; import java.io.OutputStream; import java.util.List; - import org.json.JSONArray; + /** * Interface extending Java standard List to provide JSON serialization mechanism * * @param the type of elements in this list */ -public interface DataList extends List -{ - /** - * Write this data list to a File - * - * @param file the output file - * @param indentFactor JSON indentation factor - * @throws IOException if writing file fails - */ - public void toFile(File file, int indentFactor) throws IOException; +public interface DataList extends List { + /** + * Write this data list to a File + * + * @param file the output file + * @param indentFactor JSON indentation factor + * @throws IOException if writing file fails + */ + public void toFile(File file, int indentFactor) throws IOException; + + /** + * Write this data list to a Stream + * + * @param stream the output stream + * @param indentFactor JSON indentation factor + * @throws IOException if writing stream fails + */ + public void toStream(OutputStream stream, int indentFactor) throws IOException; - /** - * Write this data list to a Stream - * - * @param stream the output stream - * @param indentFactor JSON indentation factor - * @throws IOException if writing stream fails - */ - public void toStream(OutputStream stream, int indentFactor) throws IOException; + @Override + public String toString(); - @Override - public String toString(); - /** - * Convert this data list to a String - * - * @param indentFactor JSON indentation factor - * @return JSON string representation - */ - public String toString(int indentFactor); + /** + * Convert this data list to a String + * + * @param indentFactor JSON indentation factor + * @return JSON string representation + */ + public String toString(int indentFactor); - /** - * Convert this data list to JSON array - * - * @return JSON array - */ - public JSONArray toJSON(); + /** + * Convert this data list to JSON array + * + * @return JSON array + */ + public JSONArray toJSON(); } diff --git a/src/main/java/enedis/lab/types/ExceptionBase.java b/src/main/java/enedis/lab/types/ExceptionBase.java index b871cc9..6fc213a 100644 --- a/src/main/java/enedis/lab/types/ExceptionBase.java +++ b/src/main/java/enedis/lab/types/ExceptionBase.java @@ -7,71 +7,61 @@ package enedis.lab.types; -/** - * Exception base - */ +/** Exception base */ @SuppressWarnings("serial") -public class ExceptionBase extends Exception -{ - protected int code; - protected String info; +public class ExceptionBase extends Exception { + protected int code; + protected String info; - /** - * Constructor - * - * @param code - * @param info - */ - public ExceptionBase(int code, String info) - { - this.setErrorCode(code); - this.setErrorInfo(info); - } + /** + * Constructor + * + * @param code + * @param info + */ + public ExceptionBase(int code, String info) { + this.setErrorCode(code); + this.setErrorInfo(info); + } - @Override - public String getMessage() - { - return this.info + " (" + this.code + ")"; - } + @Override + public String getMessage() { + return this.info + " (" + this.code + ")"; + } - /** - * Get error code - * - * @return error code - */ - public int getErrorCode() - { - return this.code; - } + /** + * Get error code + * + * @return error code + */ + public int getErrorCode() { + return this.code; + } - /** - * Set error code - * - * @param errorCode - */ - public void setErrorCode(int errorCode) - { - this.code = errorCode; - } + /** + * Set error code + * + * @param errorCode + */ + public void setErrorCode(int errorCode) { + this.code = errorCode; + } - /** - * Get error info - * - * @return error info - */ - public String getErrorInfo() - { - return this.info; - } - - /** - * Set error info - * - * @param errorInfo - */ - public void setErrorInfo(String errorInfo) - { - this.info = errorInfo; - } + /** + * Get error info + * + * @return error info + */ + public String getErrorInfo() { + return this.info; + } + /** + * Set error info + * + * @param errorInfo + */ + public void setErrorInfo(String errorInfo) { + this.info = errorInfo; + } } diff --git a/src/main/java/enedis/lab/types/configuration/Configuration.java b/src/main/java/enedis/lab/types/configuration/Configuration.java index e483dbb..b3ec2c4 100644 --- a/src/main/java/enedis/lab/types/configuration/Configuration.java +++ b/src/main/java/enedis/lab/types/configuration/Configuration.java @@ -7,45 +7,37 @@ package enedis.lab.types.configuration; -import java.io.File; - import enedis.lab.types.DataDictionary; +import java.io.File; -/** - * Interface used to handle any configuration - */ -public interface Configuration extends DataDictionary -{ - - /** - * Load configuration from file - * - * @throws ConfigurationException - * load file failed - */ - public void load() throws ConfigurationException; - - /** - * Save configuration to file - * - * @throws ConfigurationException - * save file failed - */ - public void save() throws ConfigurationException; - - /** - * Get config name - * - * @return configuration name - */ - public String getConfigName(); - - /** - * Get config file - * - * @return configuration file reference - */ - public File getConfigFile(); - - +/** Interface used to handle any configuration */ +public interface Configuration extends DataDictionary { + + /** + * Load configuration from file + * + * @throws ConfigurationException load file failed + */ + public void load() throws ConfigurationException; + + /** + * Save configuration to file + * + * @throws ConfigurationException save file failed + */ + public void save() throws ConfigurationException; + + /** + * Get config name + * + * @return configuration name + */ + public String getConfigName(); + + /** + * Get config file + * + * @return configuration file reference + */ + public File getConfigFile(); } diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java b/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java index 4e25ff4..2884274 100644 --- a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java +++ b/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,125 +7,120 @@ package enedis.lab.types.configuration; -import java.io.File; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; +import java.io.File; +import java.util.Map; -/** - * Basic implementation of Configuration - */ -public class ConfigurationBase extends DataDictionaryBase implements Configuration -{ - private String name = null; - private File file = null; - - protected ConfigurationBase() - { - super(); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ConfigurationBase(DataDictionary other) throws DataDictionaryException - { - super(other); - } - - /** - * Constructor using Map - * - * @param map - * @throws DataDictionaryException - */ - public ConfigurationBase(Map map) throws DataDictionaryException - { - super(map); - } - - /** - * Constructor using name and file - * - * @param name - * @param file - */ - public ConfigurationBase(String name, File file) - { - super(); - this.init(name, file); - } - - @Override - public void load() throws ConfigurationException - { - DataDictionary configuration; - - try - { - configuration = DataDictionaryBase.fromFile(this.file, this.getClass()); - } - catch (Exception exception) - { - throw new ConfigurationException("Cannot load configuration '" + ((this.name == null) ? "" : this.name) + "' from file '" + ((this.file == null) ? "" : this.file) - + "' (" + exception.getMessage() + ") " + exception.getClass().getSimpleName()); - } - try - { - this.copy(configuration); - } - catch (Exception exception) - { - throw new ConfigurationException("Cannot copy configuration '" + ((this.name == null) ? "" : this.name) + "' from file '" + ((this.file == null) ? "" : this.file) - + "' (" + exception.getMessage() + ")"); - } - } - - @Override - public void save() throws ConfigurationException - { - try - { - this.toFile(this.file, 2); - } - catch (Exception exception) - { - throw new ConfigurationException("Cannot save configuration '" + ((this.name == null) ? "" : this.name) + "' to file '" + ((this.file == null) ? "" : this.file) + "' (" - + exception.getMessage() + ")"); - } - } - - @Override - public String getConfigName() - { - return this.name; - } - - @Override - public File getConfigFile() - { - return this.file; - } - - /** - * Set config name - * - * @param configName - */ - public void setConfigName(String configName) - { - this.name = configName; - } - - protected void init(String name, File file) - { - this.name = name; - this.file = file; - } - +/** Basic implementation of Configuration */ +public class ConfigurationBase extends DataDictionaryBase implements Configuration { + private String name = null; + private File file = null; + + protected ConfigurationBase() { + super(); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ConfigurationBase(DataDictionary other) throws DataDictionaryException { + super(other); + } + + /** + * Constructor using Map + * + * @param map + * @throws DataDictionaryException + */ + public ConfigurationBase(Map map) throws DataDictionaryException { + super(map); + } + + /** + * Constructor using name and file + * + * @param name + * @param file + */ + public ConfigurationBase(String name, File file) { + super(); + this.init(name, file); + } + + @Override + public void load() throws ConfigurationException { + DataDictionary configuration; + + try { + configuration = DataDictionaryBase.fromFile(this.file, this.getClass()); + } catch (Exception exception) { + throw new ConfigurationException( + "Cannot load configuration '" + + ((this.name == null) ? "" : this.name) + + "' from file '" + + ((this.file == null) ? "" : this.file) + + "' (" + + exception.getMessage() + + ") " + + exception.getClass().getSimpleName()); + } + try { + this.copy(configuration); + } catch (Exception exception) { + throw new ConfigurationException( + "Cannot copy configuration '" + + ((this.name == null) ? "" : this.name) + + "' from file '" + + ((this.file == null) ? "" : this.file) + + "' (" + + exception.getMessage() + + ")"); + } + } + + @Override + public void save() throws ConfigurationException { + try { + this.toFile(this.file, 2); + } catch (Exception exception) { + throw new ConfigurationException( + "Cannot save configuration '" + + ((this.name == null) ? "" : this.name) + + "' to file '" + + ((this.file == null) ? "" : this.file) + + "' (" + + exception.getMessage() + + ")"); + } + } + + @Override + public String getConfigName() { + return this.name; + } + + @Override + public File getConfigFile() { + return this.file; + } + + /** + * Set config name + * + * @param configName + */ + public void setConfigName(String configName) { + this.name = configName; + } + + protected void init(String name, File file) { + this.name = name; + this.file = file; + } } diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java b/src/main/java/enedis/lab/types/configuration/ConfigurationException.java index cfdf7c3..7762c40 100644 --- a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java +++ b/src/main/java/enedis/lab/types/configuration/ConfigurationException.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -9,109 +9,99 @@ import enedis.lab.types.DataDictionaryException; -/** - * Configuration exception - */ -public class ConfigurationException extends DataDictionaryException -{ - private static final long serialVersionUID = 8090072693974075297L; - - /** - * Create missing parameter exception - * - * @param parameter - * @return configurationException - */ - public static ConfigurationException createMissingParameterException(String parameter) - { - return new ConfigurationException("Parameter \'" + parameter + "\' is missing"); - } +/** Configuration exception */ +public class ConfigurationException extends DataDictionaryException { + private static final long serialVersionUID = 8090072693974075297L; - /** - * Create unknown parameter exception - * - * @param parameter - * @return configurationException - */ - public static ConfigurationException createUnknownParameterException(String parameter) - { - return new ConfigurationException("Parameter \'" + parameter + "\' is unknown"); - } + /** + * Create missing parameter exception + * + * @param parameter + * @return configurationException + */ + public static ConfigurationException createMissingParameterException(String parameter) { + return new ConfigurationException("Parameter \'" + parameter + "\' is missing"); + } - /** - * Create invalid parameter value exception - * - * @param parameter - * @param info - * @return configurationException - */ - public static ConfigurationException createInvalidParameterValueException(String parameter, String info) - { - return new ConfigurationException("Parameter \'" + parameter + "\' has an invalid value : " + info); - } + /** + * Create unknown parameter exception + * + * @param parameter + * @return configurationException + */ + public static ConfigurationException createUnknownParameterException(String parameter) { + return new ConfigurationException("Parameter \'" + parameter + "\' is unknown"); + } - /** - * Create invalid paramter type exception - * - * @param parameter - * @param type - * @return configurationException - */ - public static ConfigurationException createInvalidParameterTypeException(String parameter, Class type) - { - return new ConfigurationException("Parameter \'" + parameter + "\' has an invalid type : " + type.getName()); - } + /** + * Create invalid parameter value exception + * + * @param parameter + * @param info + * @return configurationException + */ + public static ConfigurationException createInvalidParameterValueException( + String parameter, String info) { + return new ConfigurationException( + "Parameter \'" + parameter + "\' has an invalid value : " + info); + } - /** - * Default constructor - */ - public ConfigurationException() - { - super(); - } + /** + * Create invalid paramter type exception + * + * @param parameter + * @param type + * @return configurationException + */ + public static ConfigurationException createInvalidParameterTypeException( + String parameter, Class type) { + return new ConfigurationException( + "Parameter \'" + parameter + "\' has an invalid type : " + type.getName()); + } - /** - * Constructor using message, cause, enable suppression flag and writable stack trace flag - * - * @param message - * @param cause - * @param enableSuppression - * @param writableStackTrace - */ - public ConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) - { - super(message, cause, enableSuppression, writableStackTrace); - } + /** Default constructor */ + public ConfigurationException() { + super(); + } - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public ConfigurationException(String message, Throwable cause) - { - super(message, cause); - } + /** + * Constructor using message, cause, enable suppression flag and writable stack trace flag + * + * @param message + * @param cause + * @param enableSuppression + * @param writableStackTrace + */ + public ConfigurationException( + String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } - /** - * Constructor using message - * - * @param message - */ - public ConfigurationException(String message) - { - super(message); - } + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } - /** - * Constructor using cause - * - * @param cause - */ - public ConfigurationException(Throwable cause) - { - super(cause); - } + /** + * Constructor using message + * + * @param message + */ + public ConfigurationException(String message) { + super(message); + } + /** + * Constructor using cause + * + * @param cause + */ + public ConfigurationException(Throwable cause) { + super(cause); + } } diff --git a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java b/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java index fb4cf55..a2c3def 100644 --- a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java +++ b/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java @@ -7,6 +7,10 @@ package enedis.lab.types.datadictionary; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.DataList; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -25,644 +29,514 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; - import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * DataDictionary basic implementation - */ -public class DataDictionaryBase implements DataDictionary -{ - private static final int JSON_INDENT_FACTOR = 0; - - /** - * Instantiate a datadictionary from a File - * - * @param file - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - * @throws IOException - */ - public static DataDictionary fromFile(File file, Class clazz) throws JSONException, DataDictionaryException, IOException - { - if (file == null) - { - throw new DataDictionaryException("Can't load file from null"); - } - - InputStream stream = new FileInputStream(file); - - return DataDictionaryBase.fromStream(stream, clazz); - } - - /** - * Instantiate a datadictionary from a Stream - * - * @param stream - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - * @throws IOException - */ - public static DataDictionary fromStream(InputStream stream, Class clazz) throws JSONException, DataDictionaryException, IOException - { - if (stream == null) - { - return null; - } - - byte[] buffer = new byte[stream.available()]; - stream.read(buffer); - stream.close(); - String text = new String(buffer); - - return DataDictionaryBase.fromString(text, clazz); - } - - /** - * Instantiate a datadictionary from a String - * - * @param text - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromString(String text, Class clazz) throws JSONException, DataDictionaryException - { - if (text == null) - { - return null; - } - - JSONObject jsonObject = new JSONObject(text); - - return DataDictionaryBase.fromJSON(jsonObject, clazz); - } - - /** - * Instantiate a datadictionary from a JSONObject - * - * @param jsonObject - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromJSON(JSONObject jsonObject, Class clazz) throws JSONException, DataDictionaryException - { - if (jsonObject == null) - { - return null; - } - - return fromMap(jsonObject.toMap(), clazz); - } - - /** - * Instantiate a datadictionary from a Map - * - * @param map - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromMap(Map map, Class clazz) throws DataDictionaryException - { - if (map == null) - { - return null; - } - if (clazz == null) - { - return fromMap(map); - } - DataDictionary dataDictionnary; - try - { - Constructor constructor = clazz.getConstructor(Map.class); - dataDictionnary = constructor.newInstance(map); - } - catch (InvocationTargetException exception) - { - throw new DataDictionaryException("Cannot create instance of " + clazz.getName() + " : " + exception.getTargetException().getMessage()); - } - catch (Exception exception) - { - throw new DataDictionaryException("Cannot create instance of " + clazz.getName() + " : " + exception.getMessage()); - } - - return dataDictionnary; - } - - /** - * Instantiate a datadictionary from a Map - * - * @param map - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromMap(Map map) throws DataDictionaryException - { - if (map == null) - { - return null; - } - DataDictionaryBase dataDictionnary = new DataDictionaryBase(); - for (String key : map.keySet()) - { - Object value = map.get(key); - if (value instanceof JSONObject) - { - JSONObject jsonObjectValue = (JSONObject) value; - DataDictionary dataDictionaryValue = fromMap(jsonObjectValue.toMap()); - dataDictionnary.set(key, dataDictionaryValue); - } - else if (value instanceof JSONArray) - { - JSONArray jsonArrayValue = (JSONArray) value; - DataList listValue = new DataArrayList(jsonArrayValue.toList()); - dataDictionnary.set(key, listValue); - } - else - { - dataDictionnary.set(key, value); - } - } - - return dataDictionnary; - } - - private List> keyDescriptors; - protected HashMap data; - - /** - * Default constructor - */ - public DataDictionaryBase() - { - this.init(); - } - - /** - * Constructor using map and setting key not defined allowed flag - * - * @param map - * @throws DataDictionaryException - */ - public DataDictionaryBase(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using dataDictionary and setting key not defined allowed flag - * - * @param other - * @throws DataDictionaryException - */ - public DataDictionaryBase(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - @Override - public final boolean exists(String key) - { - return this.data.containsKey(key); - } - - @Override - public final boolean existsInKeys(String key) - { - // @formatter:off - return this.keyDescriptors.stream() - .filter(k -> k.getName().equals(key)) - .findAny() - .isPresent(); - // @formatter:on - } - - @Override - public final String[] keys() - { - Set setKeys = this.data.keySet(); - String[] keys = new String[setKeys.size()]; - - setKeys.toArray(keys); - - return keys; - } - - @Override - public final void addKey(String key) - { - if (key != null) - { - this.data.putIfAbsent(key, null); - } - } - - @Override - public final void removeKey(String key) - { - if (key != null) - { - this.data.remove(key); - } - } - - @Override - public final Object get(String key) - { - return this.data.get(key); - } - - @Override - public final void set(String key, Object value) throws DataDictionaryException - { - if (key == null) - { - throw new DataDictionaryException("Key null not allowed"); - } - - if (this.keyDescriptors.isEmpty()) - { - this.data.put(key, value); - } - else - { - Optional> keyDescriptor = this.getKeyDescriptor(key); - if (keyDescriptor.isPresent()) - { - try - { - String setterName = this.getSetterName(key); - // @formatter:off - List methodNameList = Arrays.asList(this.getClass().getDeclaredMethods()) - .stream() - .map(m -> m.getName()) - .collect(Collectors.toList()); - // @formatter:on - if (methodNameList.contains(setterName)) - { - Method method = this.getClass().getDeclaredMethod(setterName, Object.class); - method.setAccessible(true); - method.invoke(this, value); - } - else - { - this.data.put(key, keyDescriptor.get().convert(value)); - } - } - catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) - { - throw new DataDictionaryException("Set key " + key + " failed : " + e.getClass().getSimpleName() + " -> " + e.getMessage()); - } - catch (InvocationTargetException e) - { - throw new DataDictionaryException( - "Set key " + key + " failed : " + e.getTargetException().getClass().getSimpleName() + " -> " + e.getTargetException().getMessage()); - } - - } - else - { - throw new DataDictionaryException("Key " + key + " not allowed"); - } - } - } - - @Override - public void copy(DataDictionary other) throws DataDictionaryException - { - String[] otherKeys = other.keys(); - - this.clear(); - for (int i = 0; i < otherKeys.length; i++) - { - this.set(otherKeys[i], other.get(otherKeys[i])); - } - - this.checkAndUpdate(); - } - - @Override - public final void clear() - { - this.data.clear(); - } - - @Override - public final int hashCode() - { - final int prime = 31; - int result = 1; - result = prime * result + ((this.data == null) ? 0 : this.data.hashCode()); - return result; - } - - @Override - public final boolean equals(Object object) - { - if (object == null) - { - return false; - } - if (this == object) - { - return true; - } - if (!(object instanceof DataDictionary)) - { - return false; - } - DataDictionaryBase other = (DataDictionaryBase) object; - - if (this.data == null) - { - if (other.data != null) - { - return false; - } - } - else - { - if (!this.data.keySet().equals(other.data.keySet())) - { - return false; - } - for (String key : this.data.keySet()) - { - Object thisValue = this.data.get(key); - Object otherValue = other.data.get(key); - - if (thisValue == null) - { - if (otherValue != null) - { - return false; - } - else - { - continue; - } - } - else if (otherValue == null) - { - return false; - } - if (!thisValue.getClass().equals(otherValue.getClass())) - { - if ((thisValue instanceof Number) && (otherValue instanceof Number)) - { - double thisDoubleValue = ((Number) thisValue).doubleValue(); - double otherDoubleValue = ((Number) otherValue).doubleValue(); - return thisDoubleValue == otherDoubleValue; - } - else - { - return false; - } - } - if (thisValue.getClass().isArray()) - { - if (thisValue instanceof boolean[]) - { - if (!Arrays.equals((boolean[]) thisValue, (boolean[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof byte[]) - { - if (!Arrays.equals((byte[]) thisValue, (byte[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof char[]) - { - if (!Arrays.equals((char[]) thisValue, (char[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof short[]) - { - if (!Arrays.equals((short[]) thisValue, (short[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof int[]) - { - if (!Arrays.equals((int[]) thisValue, (int[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof long[]) - { - if (!Arrays.equals((long[]) thisValue, (long[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof float[]) - { - if (!Arrays.equals((float[]) thisValue, (float[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof double[]) - { - if (!Arrays.equals((double[]) thisValue, (double[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof Object[]) - { - if (!Arrays.equals((Object[]) thisValue, (Object[]) otherValue)) - { - return false; - } - } - } - else - { - if (!thisValue.equals(otherValue)) - { - return false; - } - } - } - } - - return true; - } - - @Override - public final void toFile(File file, int indentFactor) throws IOException - { - OutputStream stream = new FileOutputStream(file); - - this.toStream(stream, indentFactor); - } - - @Override - public final void toStream(OutputStream stream, int indentFactor) throws IOException - { - String text = this.toString(indentFactor); - - stream.write(text.getBytes()); - } - - @Override - public JSONObject toJSON() - { - JSONObject jsonObject = new JSONObject(); - String[] keys = this.keys(); - - for (int i = 0; i < keys.length; i++) - { - Object value = this.get(keys[i]); - if (value instanceof DataDictionaryBase) - { - DataDictionaryBase dataDictionary = (DataDictionaryBase) value; - jsonObject.put(keys[i], dataDictionary.toJSON()); - } - else if (value instanceof LocalDateTime) - { - String textValue = ((LocalDateTime) value).format(KeyDescriptorLocalDateTime.DEFAULT_FORMATTER); - jsonObject.put(keys[i], textValue); - } - else - { - jsonObject.put(keys[i], value); - } - } - - return jsonObject; - } - - @SuppressWarnings("unchecked") - @Override - public final Map toMap() - { - return (Map) this.data.clone(); - } - - @Override - public String toString() - { - return this.toString(DataDictionaryBase.JSON_INDENT_FACTOR); - } - - @Override - public String toString(int identFactor) - { - return this.toJSON().toString(identFactor); - } - - @Override - public DataDictionaryBase clone() - { - try - { - Constructor constructor = this.getClass().getConstructor(DataDictionary.class); - return this.getClass().cast(constructor.newInstance(this)); - } - catch (Exception e) - { - throw new RuntimeException(e.getMessage(), e); - } - } - - @Override - public void print() - { - System.out.println(this.toString(2)); - } - - protected final void checkAndUpdate() throws DataDictionaryException - { - this.updateOptionalParameters(); - this.checkMandatoryParameters(); - } - - protected void checkMandatoryParameters() throws DataDictionaryException - { - for (KeyDescriptor key : this.keyDescriptors) - { - if (key.isMandatory() && !this.exists(key.getName())) - { - throw new DataDictionaryException("Mandatory key " + key.getName() + " not defined"); - } - } - } - - protected void updateOptionalParameters() throws DataDictionaryException - { - } - - protected void addKeyDescriptor(KeyDescriptor keyDescriptor) throws DataDictionaryException - { - if (this.existsInKeys(keyDescriptor.getName())) - { - throw new DataDictionaryException("Key " + keyDescriptor.getName() + "already exists"); - } - this.keyDescriptors.add(keyDescriptor); - } - - protected void addAllKeyDescriptor(List> keyDescriptor) throws DataDictionaryException - { - for (KeyDescriptor key : keyDescriptor) - { - this.addKeyDescriptor(key); - } - } - - protected Optional> getKeyDescriptor(String name) - { - // @formatter:off - return this.keyDescriptors.stream() - .filter(k -> k.getName().equals(name)) - .findFirst(); - // @formatter:on - } - - private String getSetterName(String key) - { - return "set" + key.substring(0, 1).toUpperCase() + key.substring(1); - } - - private void init() - { - this.keyDescriptors = new ArrayList>(); - this.data = new HashMap(); - } - +/** DataDictionary basic implementation */ +public class DataDictionaryBase implements DataDictionary { + private static final int JSON_INDENT_FACTOR = 0; + + /** + * Instantiate a datadictionary from a File + * + * @param file + * @param clazz + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + * @throws IOException + */ + public static DataDictionary fromFile(File file, Class clazz) + throws JSONException, DataDictionaryException, IOException { + if (file == null) { + throw new DataDictionaryException("Can't load file from null"); + } + + InputStream stream = new FileInputStream(file); + + return DataDictionaryBase.fromStream(stream, clazz); + } + + /** + * Instantiate a datadictionary from a Stream + * + * @param stream + * @param clazz + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + * @throws IOException + */ + public static DataDictionary fromStream(InputStream stream, Class clazz) + throws JSONException, DataDictionaryException, IOException { + if (stream == null) { + return null; + } + + byte[] buffer = new byte[stream.available()]; + stream.read(buffer); + stream.close(); + String text = new String(buffer); + + return DataDictionaryBase.fromString(text, clazz); + } + + /** + * Instantiate a datadictionary from a String + * + * @param text + * @param clazz + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromString(String text, Class clazz) + throws JSONException, DataDictionaryException { + if (text == null) { + return null; + } + + JSONObject jsonObject = new JSONObject(text); + + return DataDictionaryBase.fromJSON(jsonObject, clazz); + } + + /** + * Instantiate a datadictionary from a JSONObject + * + * @param jsonObject + * @param clazz + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromJSON( + JSONObject jsonObject, Class clazz) + throws JSONException, DataDictionaryException { + if (jsonObject == null) { + return null; + } + + return fromMap(jsonObject.toMap(), clazz); + } + + /** + * Instantiate a datadictionary from a Map + * + * @param map + * @param clazz + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromMap( + Map map, Class clazz) + throws DataDictionaryException { + if (map == null) { + return null; + } + if (clazz == null) { + return fromMap(map); + } + DataDictionary dataDictionnary; + try { + Constructor constructor = clazz.getConstructor(Map.class); + dataDictionnary = constructor.newInstance(map); + } catch (InvocationTargetException exception) { + throw new DataDictionaryException( + "Cannot create instance of " + + clazz.getName() + + " : " + + exception.getTargetException().getMessage()); + } catch (Exception exception) { + throw new DataDictionaryException( + "Cannot create instance of " + clazz.getName() + " : " + exception.getMessage()); + } + + return dataDictionnary; + } + + /** + * Instantiate a datadictionary from a Map + * + * @param map + * @return a datadictionary + * @throws JSONException + * @throws DataDictionaryException + */ + public static DataDictionary fromMap(Map map) throws DataDictionaryException { + if (map == null) { + return null; + } + DataDictionaryBase dataDictionnary = new DataDictionaryBase(); + for (String key : map.keySet()) { + Object value = map.get(key); + if (value instanceof JSONObject) { + JSONObject jsonObjectValue = (JSONObject) value; + DataDictionary dataDictionaryValue = fromMap(jsonObjectValue.toMap()); + dataDictionnary.set(key, dataDictionaryValue); + } else if (value instanceof JSONArray) { + JSONArray jsonArrayValue = (JSONArray) value; + DataList listValue = new DataArrayList(jsonArrayValue.toList()); + dataDictionnary.set(key, listValue); + } else { + dataDictionnary.set(key, value); + } + } + + return dataDictionnary; + } + + private List> keyDescriptors; + protected HashMap data; + + /** Default constructor */ + public DataDictionaryBase() { + this.init(); + } + + /** + * Constructor using map and setting key not defined allowed flag + * + * @param map + * @throws DataDictionaryException + */ + public DataDictionaryBase(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using dataDictionary and setting key not defined allowed flag + * + * @param other + * @throws DataDictionaryException + */ + public DataDictionaryBase(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + @Override + public final boolean exists(String key) { + return this.data.containsKey(key); + } + + @Override + public final boolean existsInKeys(String key) { + // @formatter:off + return this.keyDescriptors.stream().filter(k -> k.getName().equals(key)).findAny().isPresent(); + // @formatter:on + } + + @Override + public final String[] keys() { + Set setKeys = this.data.keySet(); + String[] keys = new String[setKeys.size()]; + + setKeys.toArray(keys); + + return keys; + } + + @Override + public final void addKey(String key) { + if (key != null) { + this.data.putIfAbsent(key, null); + } + } + + @Override + public final void removeKey(String key) { + if (key != null) { + this.data.remove(key); + } + } + + @Override + public final Object get(String key) { + return this.data.get(key); + } + + @Override + public final void set(String key, Object value) throws DataDictionaryException { + if (key == null) { + throw new DataDictionaryException("Key null not allowed"); + } + + if (this.keyDescriptors.isEmpty()) { + this.data.put(key, value); + } else { + Optional> keyDescriptor = this.getKeyDescriptor(key); + if (keyDescriptor.isPresent()) { + try { + String setterName = this.getSetterName(key); + // @formatter:off + List methodNameList = + Arrays.asList(this.getClass().getDeclaredMethods()).stream() + .map(m -> m.getName()) + .collect(Collectors.toList()); + // @formatter:on + if (methodNameList.contains(setterName)) { + Method method = this.getClass().getDeclaredMethod(setterName, Object.class); + method.setAccessible(true); + method.invoke(this, value); + } else { + this.data.put(key, keyDescriptor.get().convert(value)); + } + } catch (NoSuchMethodException + | SecurityException + | IllegalAccessException + | IllegalArgumentException e) { + throw new DataDictionaryException( + "Set key " + + key + + " failed : " + + e.getClass().getSimpleName() + + " -> " + + e.getMessage()); + } catch (InvocationTargetException e) { + throw new DataDictionaryException( + "Set key " + + key + + " failed : " + + e.getTargetException().getClass().getSimpleName() + + " -> " + + e.getTargetException().getMessage()); + } + + } else { + throw new DataDictionaryException("Key " + key + " not allowed"); + } + } + } + + @Override + public void copy(DataDictionary other) throws DataDictionaryException { + String[] otherKeys = other.keys(); + + this.clear(); + for (int i = 0; i < otherKeys.length; i++) { + this.set(otherKeys[i], other.get(otherKeys[i])); + } + + this.checkAndUpdate(); + } + + @Override + public final void clear() { + this.data.clear(); + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.data == null) ? 0 : this.data.hashCode()); + return result; + } + + @Override + public final boolean equals(Object object) { + if (object == null) { + return false; + } + if (this == object) { + return true; + } + if (!(object instanceof DataDictionary)) { + return false; + } + DataDictionaryBase other = (DataDictionaryBase) object; + + if (this.data == null) { + if (other.data != null) { + return false; + } + } else { + if (!this.data.keySet().equals(other.data.keySet())) { + return false; + } + for (String key : this.data.keySet()) { + Object thisValue = this.data.get(key); + Object otherValue = other.data.get(key); + + if (thisValue == null) { + if (otherValue != null) { + return false; + } else { + continue; + } + } else if (otherValue == null) { + return false; + } + if (!thisValue.getClass().equals(otherValue.getClass())) { + if ((thisValue instanceof Number) && (otherValue instanceof Number)) { + double thisDoubleValue = ((Number) thisValue).doubleValue(); + double otherDoubleValue = ((Number) otherValue).doubleValue(); + return thisDoubleValue == otherDoubleValue; + } else { + return false; + } + } + if (thisValue.getClass().isArray()) { + if (thisValue instanceof boolean[]) { + if (!Arrays.equals((boolean[]) thisValue, (boolean[]) otherValue)) { + return false; + } + } else if (thisValue instanceof byte[]) { + if (!Arrays.equals((byte[]) thisValue, (byte[]) otherValue)) { + return false; + } + } else if (thisValue instanceof char[]) { + if (!Arrays.equals((char[]) thisValue, (char[]) otherValue)) { + return false; + } + } else if (thisValue instanceof short[]) { + if (!Arrays.equals((short[]) thisValue, (short[]) otherValue)) { + return false; + } + } else if (thisValue instanceof int[]) { + if (!Arrays.equals((int[]) thisValue, (int[]) otherValue)) { + return false; + } + } else if (thisValue instanceof long[]) { + if (!Arrays.equals((long[]) thisValue, (long[]) otherValue)) { + return false; + } + } else if (thisValue instanceof float[]) { + if (!Arrays.equals((float[]) thisValue, (float[]) otherValue)) { + return false; + } + } else if (thisValue instanceof double[]) { + if (!Arrays.equals((double[]) thisValue, (double[]) otherValue)) { + return false; + } + } else if (thisValue instanceof Object[]) { + if (!Arrays.equals((Object[]) thisValue, (Object[]) otherValue)) { + return false; + } + } + } else { + if (!thisValue.equals(otherValue)) { + return false; + } + } + } + } + + return true; + } + + @Override + public final void toFile(File file, int indentFactor) throws IOException { + OutputStream stream = new FileOutputStream(file); + + this.toStream(stream, indentFactor); + } + + @Override + public final void toStream(OutputStream stream, int indentFactor) throws IOException { + String text = this.toString(indentFactor); + + stream.write(text.getBytes()); + } + + @Override + public JSONObject toJSON() { + JSONObject jsonObject = new JSONObject(); + String[] keys = this.keys(); + + for (int i = 0; i < keys.length; i++) { + Object value = this.get(keys[i]); + if (value instanceof DataDictionaryBase) { + DataDictionaryBase dataDictionary = (DataDictionaryBase) value; + jsonObject.put(keys[i], dataDictionary.toJSON()); + } else if (value instanceof LocalDateTime) { + String textValue = + ((LocalDateTime) value).format(KeyDescriptorLocalDateTime.DEFAULT_FORMATTER); + jsonObject.put(keys[i], textValue); + } else { + jsonObject.put(keys[i], value); + } + } + + return jsonObject; + } + + @SuppressWarnings("unchecked") + @Override + public final Map toMap() { + return (Map) this.data.clone(); + } + + @Override + public String toString() { + return this.toString(DataDictionaryBase.JSON_INDENT_FACTOR); + } + + @Override + public String toString(int identFactor) { + return this.toJSON().toString(identFactor); + } + + @Override + public DataDictionaryBase clone() { + try { + Constructor constructor = this.getClass().getConstructor(DataDictionary.class); + return this.getClass().cast(constructor.newInstance(this)); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public void print() { + System.out.println(this.toString(2)); + } + + protected final void checkAndUpdate() throws DataDictionaryException { + this.updateOptionalParameters(); + this.checkMandatoryParameters(); + } + + protected void checkMandatoryParameters() throws DataDictionaryException { + for (KeyDescriptor key : this.keyDescriptors) { + if (key.isMandatory() && !this.exists(key.getName())) { + throw new DataDictionaryException("Mandatory key " + key.getName() + " not defined"); + } + } + } + + protected void updateOptionalParameters() throws DataDictionaryException {} + + protected void addKeyDescriptor(KeyDescriptor keyDescriptor) throws DataDictionaryException { + if (this.existsInKeys(keyDescriptor.getName())) { + throw new DataDictionaryException("Key " + keyDescriptor.getName() + "already exists"); + } + this.keyDescriptors.add(keyDescriptor); + } + + protected void addAllKeyDescriptor(List> keyDescriptor) + throws DataDictionaryException { + for (KeyDescriptor key : keyDescriptor) { + this.addKeyDescriptor(key); + } + } + + protected Optional> getKeyDescriptor(String name) { + // @formatter:off + return this.keyDescriptors.stream().filter(k -> k.getName().equals(name)).findFirst(); + // @formatter:on + } + + private String getSetterName(String key) { + return "set" + key.substring(0, 1).toUpperCase() + key.substring(1); + } + + private void init() { + this.keyDescriptors = new ArrayList>(); + this.data = new HashMap(); + } } diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java index 1596b47..5d62b32 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java @@ -14,53 +14,50 @@ * * @param */ -public interface KeyDescriptor -{ - /** - * Get key name - * - * @return key name - */ - public String getName(); - - /** - * Get mandatory flag - * - * @return mandatory flag - */ - public boolean isMandatory(); - - /** - * Set mandatory flag - * - * @param mandatory - */ - public void setMandatory(boolean mandatory); - - /** - * Set a list of accepted values - * - * @param acceptedValues - */ - @SuppressWarnings("unchecked") - public void setAcceptedValues(T... acceptedValues); - - /** - * Convert a Object value to a T value - * - * @param value - * object to convert - * @return value converted to T type - * @throws DataDictionaryException - */ - public T convert(Object value) throws DataDictionaryException; - - /** - * Convert a T value to String - * - * @param value - * value to convert to String - * @return String representation of the value - */ - public String toString(T value); +public interface KeyDescriptor { + /** + * Get key name + * + * @return key name + */ + public String getName(); + + /** + * Get mandatory flag + * + * @return mandatory flag + */ + public boolean isMandatory(); + + /** + * Set mandatory flag + * + * @param mandatory + */ + public void setMandatory(boolean mandatory); + + /** + * Set a list of accepted values + * + * @param acceptedValues + */ + @SuppressWarnings("unchecked") + public void setAcceptedValues(T... acceptedValues); + + /** + * Convert a Object value to a T value + * + * @param value object to convert + * @return value converted to T type + * @throws DataDictionaryException + */ + public T convert(Object value) throws DataDictionaryException; + + /** + * Convert a T value to String + * + * @param value value to convert to String + * @return String representation of the value + */ + public String toString(T value); } diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java index 6809abd..c81459f 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,149 +7,130 @@ package enedis.lab.types.datadictionary; +import enedis.lab.types.DataDictionaryException; import java.util.Arrays; import java.util.List; -import enedis.lab.types.DataDictionaryException; - /** * DataDictionary key descriptor base * * @param */ -public abstract class KeyDescriptorBase implements KeyDescriptor -{ - private String name; - private boolean mandatory; - protected List acceptedValues; - - /** - * Constructor setting all attributes - * - * @param name - * @param mandatory - */ - public KeyDescriptorBase(String name, boolean mandatory) - { - super(); - this.name = name; - this.mandatory = mandatory; - } - - @Override - public final T convert(Object value) throws DataDictionaryException - { - T convertedValue = null; - - if (value == null) - { - this.handleNullValue(); - return null; - } - - convertedValue = this.convertValue(value); - - this.checkAcceptedValues(convertedValue); - - return convertedValue; - } - - @Override - public String toString(T value) - { - if (value == null) - { - return null; - } - - return value.toString(); - } - - /** - * Get key name - * - * @return key name - */ - @Override - public String getName() - { - return this.name; - } - - /** - * Set key name - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Get mandatory flag - * - * @return mandatory flag - */ - @Override - public boolean isMandatory() - { - return this.mandatory; - } - - /** - * Set mandatory flag - * - * @param mandatory - */ - @Override - public void setMandatory(boolean mandatory) - { - this.mandatory = mandatory; - } - - /** - * Set mandatory value - * - * @param acceptedValues - */ - @SuppressWarnings("unchecked") - @Override - public void setAcceptedValues(T... acceptedValues) - { - this.acceptedValues = Arrays.asList(acceptedValues); - } - - protected abstract T convertValue(Object value) throws DataDictionaryException; - - protected final void handleNullValue() throws DataDictionaryException - { - if (this.isMandatory()) - { - throw new DataDictionaryException("Cannot set null " + this.getName()); - } - } - - protected void checkAcceptedValues(T value) throws DataDictionaryException - { - if (this.acceptedValues != null) - { - boolean accepted = false; - - for (T v : this.acceptedValues) - { - if (v.equals(value)) - { - accepted = true; - break; - } - } - - if (!accepted) - { - throw new DataDictionaryException("Key " + this.getName() + " doesn't respect mandatory value"); - } - } - } - +public abstract class KeyDescriptorBase implements KeyDescriptor { + private String name; + private boolean mandatory; + protected List acceptedValues; + + /** + * Constructor setting all attributes + * + * @param name + * @param mandatory + */ + public KeyDescriptorBase(String name, boolean mandatory) { + super(); + this.name = name; + this.mandatory = mandatory; + } + + @Override + public final T convert(Object value) throws DataDictionaryException { + T convertedValue = null; + + if (value == null) { + this.handleNullValue(); + return null; + } + + convertedValue = this.convertValue(value); + + this.checkAcceptedValues(convertedValue); + + return convertedValue; + } + + @Override + public String toString(T value) { + if (value == null) { + return null; + } + + return value.toString(); + } + + /** + * Get key name + * + * @return key name + */ + @Override + public String getName() { + return this.name; + } + + /** + * Set key name + * + * @param name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Get mandatory flag + * + * @return mandatory flag + */ + @Override + public boolean isMandatory() { + return this.mandatory; + } + + /** + * Set mandatory flag + * + * @param mandatory + */ + @Override + public void setMandatory(boolean mandatory) { + this.mandatory = mandatory; + } + + /** + * Set mandatory value + * + * @param acceptedValues + */ + @SuppressWarnings("unchecked") + @Override + public void setAcceptedValues(T... acceptedValues) { + this.acceptedValues = Arrays.asList(acceptedValues); + } + + protected abstract T convertValue(Object value) throws DataDictionaryException; + + protected final void handleNullValue() throws DataDictionaryException { + if (this.isMandatory()) { + throw new DataDictionaryException("Cannot set null " + this.getName()); + } + } + + protected void checkAcceptedValues(T value) throws DataDictionaryException { + if (this.acceptedValues != null) { + boolean accepted = false; + + for (T v : this.acceptedValues) { + if (v.equals(value)) { + accepted = true; + break; + } + } + + if (!accepted) { + throw new DataDictionaryException( + "Key " + this.getName() + " doesn't respect mandatory value"); + } + } + } } diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java index f5fec56..23fe95c 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java @@ -7,92 +7,110 @@ package enedis.lab.types.datadictionary; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Map; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; - /** * DataDictionary key descriptor DataDictionary - * + * * @param */ -public class KeyDescriptorDataDictionary extends KeyDescriptorBase -{ - private Class dataDictionaryClass; - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param dataDictionaryClass - */ - public KeyDescriptorDataDictionary(String name, boolean mandatory, Class dataDictionaryClass) - { - super(name, mandatory); - this.dataDictionaryClass = dataDictionaryClass; - } +public class KeyDescriptorDataDictionary + extends KeyDescriptorBase { + private Class dataDictionaryClass; - @SuppressWarnings("unchecked") - @Override - public T convertValue(Object value) throws DataDictionaryException - { - T convertedValue = null; + /** + * Default constructor + * + * @param name + * @param mandatory + * @param dataDictionaryClass + */ + public KeyDescriptorDataDictionary(String name, boolean mandatory, Class dataDictionaryClass) { + super(name, mandatory); + this.dataDictionaryClass = dataDictionaryClass; + } - if (this.dataDictionaryClass.isAssignableFrom(value.getClass())) - { - convertedValue = this.dataDictionaryClass.cast(value); - } - else if (value instanceof String) - { - convertedValue = (T) DataDictionaryBase.fromString((String) value, this.dataDictionaryClass); - } - else if (value instanceof DataDictionary) - { - try - { - Constructor constructor = this.dataDictionaryClass.getConstructor(DataDictionary.class); - convertedValue = constructor.newInstance((DataDictionary) value); - } - catch (InvocationTargetException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getTargetException().getMessage()); - } - catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getMessage()); - } - } - else if (value instanceof Map) - { - try - { - Constructor constructor = this.dataDictionaryClass.getConstructor(Map.class); - convertedValue = constructor.newInstance((Map) value); - } - catch (InvocationTargetException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getTargetException().getMessage()); - } - catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getMessage()); - } - } - else - { - throw new DataDictionaryException( - "Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + this.dataDictionaryClass.getSimpleName()); - } + @SuppressWarnings("unchecked") + @Override + public T convertValue(Object value) throws DataDictionaryException { + T convertedValue = null; - return convertedValue; - } + if (this.dataDictionaryClass.isAssignableFrom(value.getClass())) { + convertedValue = this.dataDictionaryClass.cast(value); + } else if (value instanceof String) { + convertedValue = (T) DataDictionaryBase.fromString((String) value, this.dataDictionaryClass); + } else if (value instanceof DataDictionary) { + try { + Constructor constructor = this.dataDictionaryClass.getConstructor(DataDictionary.class); + convertedValue = constructor.newInstance((DataDictionary) value); + } catch (InvocationTargetException e) { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": Cannot convert type " + + value.getClass().getSimpleName() + + " to " + + this.dataDictionaryClass.getSimpleName() + + " : " + + e.getTargetException().getMessage()); + } catch (NoSuchMethodException + | SecurityException + | InstantiationException + | IllegalAccessException + | IllegalArgumentException e) { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": Cannot convert type " + + value.getClass().getSimpleName() + + " to " + + this.dataDictionaryClass.getSimpleName() + + " : " + + e.getMessage()); + } + } else if (value instanceof Map) { + try { + Constructor constructor = this.dataDictionaryClass.getConstructor(Map.class); + convertedValue = constructor.newInstance((Map) value); + } catch (InvocationTargetException e) { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": Cannot convert type " + + value.getClass().getSimpleName() + + " to " + + this.dataDictionaryClass.getSimpleName() + + " : " + + e.getTargetException().getMessage()); + } catch (NoSuchMethodException + | SecurityException + | InstantiationException + | IllegalAccessException + | IllegalArgumentException e) { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": Cannot convert type " + + value.getClass().getSimpleName() + + " to " + + this.dataDictionaryClass.getSimpleName() + + " : " + + e.getMessage()); + } + } else { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": Cannot convert type " + + value.getClass().getSimpleName() + + " to " + + this.dataDictionaryClass.getSimpleName()); + } + return convertedValue; + } } diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java index 9e523f3..cc1f9f9 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java @@ -11,77 +11,75 @@ /** * DataDictionary key descriptor Enum - * + * * @param */ @SuppressWarnings("rawtypes") -public class KeyDescriptorEnum extends KeyDescriptorBase -{ - private Class enumClass; - private String prefix; +public class KeyDescriptorEnum extends KeyDescriptorBase { + private Class enumClass; + private String prefix; - /** - * Default constructor - * - * @param name - * @param mandatory - * @param enumClass - */ - public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass) - { - this(name, mandatory, enumClass, ""); - } - - /** - * Constructor setting all parameters - * - * @param name - * @param mandatory - * @param enumClass - * @param prefix - */ - public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass, String prefix) - { - super(name, mandatory); - this.enumClass = enumClass; - this.prefix = prefix; - } + /** + * Default constructor + * + * @param name + * @param mandatory + * @param enumClass + */ + public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass) { + this(name, mandatory, enumClass, ""); + } - @Override - public T convertValue(Object value) throws DataDictionaryException - { - T convertedValue = null; + /** + * Constructor setting all parameters + * + * @param name + * @param mandatory + * @param enumClass + * @param prefix + */ + public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass, String prefix) { + super(name, mandatory); + this.enumClass = enumClass; + this.prefix = prefix; + } - if (this.enumClass.isAssignableFrom(value.getClass())) - { - convertedValue = this.enumClass.cast(value); - } - else if (value instanceof String) - { - convertedValue = this.toEnum((String) value); - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + this.enumClass.getSimpleName()); - } + @Override + public T convertValue(Object value) throws DataDictionaryException { + T convertedValue = null; - return convertedValue; - } + if (this.enumClass.isAssignableFrom(value.getClass())) { + convertedValue = this.enumClass.cast(value); + } else if (value instanceof String) { + convertedValue = this.toEnum((String) value); + } else { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": Cannot convert type " + + value.getClass().getSimpleName() + + " to " + + this.enumClass.getSimpleName()); + } - @SuppressWarnings("unchecked") - protected T toEnum(String value) throws DataDictionaryException - { - T convertedValue = null; - try - { - String preparedValue = this.prefix + value.toUpperCase().replace(".", "_").replace("-", "_"); - convertedValue = (T) Enum.valueOf(this.enumClass, preparedValue); - } - catch (IllegalArgumentException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": " + this.enumClass.getSimpleName() + " doesn't contain " + value); - } - return convertedValue; - } - + return convertedValue; + } + + @SuppressWarnings("unchecked") + protected T toEnum(String value) throws DataDictionaryException { + T convertedValue = null; + try { + String preparedValue = this.prefix + value.toUpperCase().replace(".", "_").replace("-", "_"); + convertedValue = (T) Enum.valueOf(this.enumClass, preparedValue); + } catch (IllegalArgumentException e) { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": " + + this.enumClass.getSimpleName() + + " doesn't contain " + + value); + } + return convertedValue; + } } diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java index 758a03a..bf7e71c 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,6 +7,8 @@ package enedis.lab.types.datadictionary; +import enedis.lab.types.DataDictionary; +import enedis.lab.types.DataDictionaryException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -14,165 +16,144 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.json.JSONObject; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; - /** * DataDictionary key descriptor List - * + * * @param */ -public class KeyDescriptorList extends KeyDescriptorBase> -{ - private Class itemClass; +public class KeyDescriptorList extends KeyDescriptorBase> { + private Class itemClass; - /** - * Default constructor - * - * @param name - * @param mandatory - * @param itemClass - */ - public KeyDescriptorList(String name, boolean mandatory, Class itemClass) - { - super(name, mandatory); - this.itemClass = itemClass; - } + /** + * Default constructor + * + * @param name + * @param mandatory + * @param itemClass + */ + public KeyDescriptorList(String name, boolean mandatory, Class itemClass) { + super(name, mandatory); + this.itemClass = itemClass; + } - @Override - public List convertValue(Object value) throws DataDictionaryException - { - List convertedValue = new ArrayList(); + @Override + public List convertValue(Object value) throws DataDictionaryException { + List convertedValue = new ArrayList(); - if (this.itemClass.isAssignableFrom(value.getClass())) - { - T convertedItem = this.convertItem(value); - convertedValue.add(convertedItem); - } - else if (value instanceof HashMap) - { - T convertedItem = this.convertItem(value); - convertedValue.add(convertedItem); - } - else if (value instanceof List) - { - for (Object item : (List) value) - { - T convertedItem = this.convertItem(item); - convertedValue.add(convertedItem); - } - } - else if (value instanceof Object[]) - { - for (Object item : (Object[]) value) - { - T convertedItem = this.convertItem(item); - convertedValue.add(convertedItem); - } - } - else - { - throw new DataDictionaryException( - "Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to List<" + this.itemClass.getSimpleName() + ">"); - } - return convertedValue; - } + if (this.itemClass.isAssignableFrom(value.getClass())) { + T convertedItem = this.convertItem(value); + convertedValue.add(convertedItem); + } else if (value instanceof HashMap) { + T convertedItem = this.convertItem(value); + convertedValue.add(convertedItem); + } else if (value instanceof List) { + for (Object item : (List) value) { + T convertedItem = this.convertItem(item); + convertedValue.add(convertedItem); + } + } else if (value instanceof Object[]) { + for (Object item : (Object[]) value) { + T convertedItem = this.convertItem(item); + convertedValue.add(convertedItem); + } + } else { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": Cannot convert type " + + value.getClass().getSimpleName() + + " to List<" + + this.itemClass.getSimpleName() + + ">"); + } + return convertedValue; + } - private T convertItem(Object item) throws DataDictionaryException - { - T convertedItem = null; - if (this.itemClass.isAssignableFrom(item.getClass())) - { - convertedItem = this.itemClass.cast(item); - } - else if (DataDictionary.class.isAssignableFrom(this.itemClass)) - { - convertedItem = this.convertDataDictionaryItem(item); - } - else if (Enum.class.isAssignableFrom(this.itemClass)) - { - convertedItem = this.convertEnumItem(item); - } - else - { - throw new DataDictionaryException( - "Key " + this.getName() + ": at least on item isn't a " + this.itemClass.getSimpleName() + ", type received :" + item.getClass().getSimpleName()); - } - return convertedItem; - } + private T convertItem(Object item) throws DataDictionaryException { + T convertedItem = null; + if (this.itemClass.isAssignableFrom(item.getClass())) { + convertedItem = this.itemClass.cast(item); + } else if (DataDictionary.class.isAssignableFrom(this.itemClass)) { + convertedItem = this.convertDataDictionaryItem(item); + } else if (Enum.class.isAssignableFrom(this.itemClass)) { + convertedItem = this.convertEnumItem(item); + } else { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": at least on item isn't a " + + this.itemClass.getSimpleName() + + ", type received :" + + item.getClass().getSimpleName()); + } + return convertedItem; + } - private T convertDataDictionaryItem(Object item) throws DataDictionaryException - { - T convertedItem = null; - if (item instanceof JSONObject) - { - JSONObject itemJsonObject = (JSONObject) item; - try - { - Constructor constructor = this.itemClass.getConstructor(Map.class); - convertedItem = (T) constructor.newInstance(itemJsonObject.toMap()); - } - catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) - { - e.printStackTrace(); - } - catch (InvocationTargetException e) - { - e.printStackTrace(); - } - } - else if (item instanceof Map) - { - try - { - Constructor constructor = this.itemClass.getConstructor(Map.class); - convertedItem = (T) constructor.newInstance((Map) item); - } - catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) - { - e.printStackTrace(); - } - catch (InvocationTargetException e) - { - e.printStackTrace(); - } - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": at least on item isn't a JSONObject, type received :" + item.getClass().getSimpleName()); - } - return convertedItem; - } + private T convertDataDictionaryItem(Object item) throws DataDictionaryException { + T convertedItem = null; + if (item instanceof JSONObject) { + JSONObject itemJsonObject = (JSONObject) item; + try { + Constructor constructor = this.itemClass.getConstructor(Map.class); + convertedItem = (T) constructor.newInstance(itemJsonObject.toMap()); + } catch (SecurityException + | InstantiationException + | IllegalAccessException + | IllegalArgumentException + | NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } else if (item instanceof Map) { + try { + Constructor constructor = this.itemClass.getConstructor(Map.class); + convertedItem = (T) constructor.newInstance((Map) item); + } catch (SecurityException + | InstantiationException + | IllegalAccessException + | IllegalArgumentException + | NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } else { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": at least on item isn't a JSONObject, type received :" + + item.getClass().getSimpleName()); + } + return convertedItem; + } - @SuppressWarnings("unchecked") - private T convertEnumItem(Object item) throws DataDictionaryException - { - T convertedItem = null; - if (item instanceof String) - { - String itemString = (String) item; - try - { - Method valueOf = this.itemClass.getMethod("valueOf", String.class); - convertedItem = (T) valueOf.invoke(null, itemString.toUpperCase()); - } - catch (SecurityException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) - { - e.printStackTrace(); - } - catch (InvocationTargetException e) - { - e.printStackTrace(); - } + @SuppressWarnings("unchecked") + private T convertEnumItem(Object item) throws DataDictionaryException { + T convertedItem = null; + if (item instanceof String) { + String itemString = (String) item; + try { + Method valueOf = this.itemClass.getMethod("valueOf", String.class); + convertedItem = (T) valueOf.invoke(null, itemString.toUpperCase()); + } catch (SecurityException + | IllegalAccessException + | IllegalArgumentException + | NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": at least on item isn't a JSONObject, type received :" + item.getClass().getSimpleName()); - } - return convertedItem; - } + } else { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": at least on item isn't a JSONObject, type received :" + + item.getClass().getSimpleName()); + } + return convertedItem; + } } diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java index b8b3695..68cbd3d 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java @@ -7,109 +7,97 @@ package enedis.lab.types.datadictionary; -import java.util.List; - import enedis.lab.types.DataDictionaryException; import enedis.lab.util.MinMaxChecker; +import java.util.List; /** * DataDictionary key descriptor Number min max - * + * * @param */ -public class KeyDescriptorListMinMaxSize extends KeyDescriptorList -{ - private MinMaxChecker minMaxChecker; +public class KeyDescriptorListMinMaxSize extends KeyDescriptorList { + private MinMaxChecker minMaxChecker; - /** - * Default constructor - * - * @param name - * @param mandatory - * @param itemClass - */ - public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass) - { - this(name, mandatory, itemClass, null, null); - } + /** + * Default constructor + * + * @param name + * @param mandatory + * @param itemClass + */ + public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass) { + this(name, mandatory, itemClass, null, null); + } - /** - * Constructor setting all attributes - * - * @param name - * @param mandatory - * @param itemClass - * @param min - * @param max - */ - public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass, Integer min, Integer max) - { - super(name, mandatory, itemClass); - this.minMaxChecker = new MinMaxChecker(min, max); - } + /** + * Constructor setting all attributes + * + * @param name + * @param mandatory + * @param itemClass + * @param min + * @param max + */ + public KeyDescriptorListMinMaxSize( + String name, boolean mandatory, Class itemClass, Integer min, Integer max) { + super(name, mandatory, itemClass); + this.minMaxChecker = new MinMaxChecker(min, max); + } - @Override - public List convertValue(Object value) throws DataDictionaryException - { - List convertedValue = super.convertValue(value); + @Override + public List convertValue(Object value) throws DataDictionaryException { + List convertedValue = super.convertValue(value); - this.check(convertedValue); + this.check(convertedValue); - return convertedValue; - } + return convertedValue; + } - /** - * Get min - * - * @return min - */ - public Integer getMin() - { - return this.minMaxChecker.getMin().intValue(); - } + /** + * Get min + * + * @return min + */ + public Integer getMin() { + return this.minMaxChecker.getMin().intValue(); + } - /** - * Set min - * - * @param min - * @throws IllegalArgumentException - * if min is greater than max - */ - public void setMin(Integer min) - { - this.minMaxChecker.setMin(min); - } + /** + * Set min + * + * @param min + * @throws IllegalArgumentException if min is greater than max + */ + public void setMin(Integer min) { + this.minMaxChecker.setMin(min); + } - /** - * Get max - * - * @return max - */ - public Integer getMax() - { - return this.minMaxChecker.getMax().intValue(); - } + /** + * Get max + * + * @return max + */ + public Integer getMax() { + return this.minMaxChecker.getMax().intValue(); + } - /** - * Set max - * - * @param max - * @throws IllegalArgumentException - * if max is smaller than min - */ - public void setMax(Integer max) - { - this.minMaxChecker.setMax(max); - } + /** + * Set max + * + * @param max + * @throws IllegalArgumentException if max is smaller than min + */ + public void setMax(Integer max) { + this.minMaxChecker.setMax(max); + } - private void check(List list) throws DataDictionaryException - { - if (list != null) - { - if (!this.minMaxChecker.check(list.size())) - { - throw new DataDictionaryException("Key " + this.getName() + ": value size (" + list.size() + ") out of bound"); - } - } - } + private void check(List list) throws DataDictionaryException { + if (list != null) { + if (!this.minMaxChecker.check(list.size())) { + throw new DataDictionaryException( + "Key " + this.getName() + ": value size (" + list.size() + ") out of bound"); + } + } + } } diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java index e083324..8e53fb2 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java @@ -7,92 +7,81 @@ package enedis.lab.types.datadictionary; +import enedis.lab.types.DataDictionaryException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor LocalDateTime - */ -public class KeyDescriptorLocalDateTime extends KeyDescriptorBase -{ - /** Default pattern used */ - public static final String DEFAULT_PATTERN = "dd/MM/yyyy HH:mm:ss"; - /** Default formatter used */ - public static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_PATTERN); +/** DataDictionary key descriptor LocalDateTime */ +public class KeyDescriptorLocalDateTime extends KeyDescriptorBase { + /** Default pattern used */ + public static final String DEFAULT_PATTERN = "dd/MM/yyyy HH:mm:ss"; - private DateTimeFormatter formatter; + /** Default formatter used */ + public static final DateTimeFormatter DEFAULT_FORMATTER = + DateTimeFormatter.ofPattern(DEFAULT_PATTERN); - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorLocalDateTime(String name, boolean mandatory) - { - this(name, mandatory, DEFAULT_PATTERN); - } + private DateTimeFormatter formatter; - /** - * Default constructor - * - * @param name - * @param mandatory - * @param formatterPattern - */ - public KeyDescriptorLocalDateTime(String name, boolean mandatory, String formatterPattern) - { - super(name, mandatory); - this.formatter = DateTimeFormatter.ofPattern(formatterPattern); - } + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorLocalDateTime(String name, boolean mandatory) { + this(name, mandatory, DEFAULT_PATTERN); + } - @Override - public String toString(LocalDateTime value) - { - if (value == null) - { - return null; - } + /** + * Default constructor + * + * @param name + * @param mandatory + * @param formatterPattern + */ + public KeyDescriptorLocalDateTime(String name, boolean mandatory, String formatterPattern) { + super(name, mandatory); + this.formatter = DateTimeFormatter.ofPattern(formatterPattern); + } - return this.formatter.format(value); - } + @Override + public String toString(LocalDateTime value) { + if (value == null) { + return null; + } - @Override - public LocalDateTime convertValue(Object value) throws DataDictionaryException - { - LocalDateTime convertedValue = null; + return this.formatter.format(value); + } - if (value instanceof LocalDateTime) - { - convertedValue = (LocalDateTime) value; - } - else if (value instanceof String) - { - convertedValue = this.toLocalDateTime((String) value); - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to LocalDateTime"); - } + @Override + public LocalDateTime convertValue(Object value) throws DataDictionaryException { + LocalDateTime convertedValue = null; - return convertedValue; - } + if (value instanceof LocalDateTime) { + convertedValue = (LocalDateTime) value; + } else if (value instanceof String) { + convertedValue = this.toLocalDateTime((String) value); + } else { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": Cannot convert type " + + value.getClass().getSimpleName() + + " to LocalDateTime"); + } - private LocalDateTime toLocalDateTime(String value) throws DataDictionaryException - { - LocalDateTime convertedValue = null; - try - { - convertedValue = LocalDateTime.parse(value, this.formatter); - } - catch (DateTimeParseException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": string " + value + " cannot be converted to LocalDateTime"); - } - return convertedValue; - } + return convertedValue; + } + private LocalDateTime toLocalDateTime(String value) throws DataDictionaryException { + LocalDateTime convertedValue = null; + try { + convertedValue = LocalDateTime.parse(value, this.formatter); + } catch (DateTimeParseException e) { + throw new DataDictionaryException( + "Key " + this.getName() + ": string " + value + " cannot be converted to LocalDateTime"); + } + return convertedValue; + } } diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java index 24e44e4..c337601 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java @@ -9,76 +9,68 @@ import enedis.lab.types.DataDictionaryException; -/** - * DataDictionary key descriptor Number - */ -public class KeyDescriptorNumber extends KeyDescriptorBase -{ - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorNumber(String name, boolean mandatory) - { - super(name, mandatory); - } +/** DataDictionary key descriptor Number */ +public class KeyDescriptorNumber extends KeyDescriptorBase { + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorNumber(String name, boolean mandatory) { + super(name, mandatory); + } - @Override - public Number convertValue(Object value) throws DataDictionaryException - { - Number convertedValue = null; + @Override + public Number convertValue(Object value) throws DataDictionaryException { + Number convertedValue = null; - if (value instanceof Number) - { - convertedValue = (Number) value; - } - else if (value instanceof String) - { - convertedValue = this.toNumber((String) value); - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to Number"); - } + if (value instanceof Number) { + convertedValue = (Number) value; + } else if (value instanceof String) { + convertedValue = this.toNumber((String) value); + } else { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": Cannot convert type " + + value.getClass().getSimpleName() + + " to Number"); + } - return convertedValue; - } + return convertedValue; + } - protected void checkAcceptedValues(Number value) throws DataDictionaryException - { - if (this.acceptedValues != null) - { - boolean accepted = false; - - for(Number v : this.acceptedValues) - { - if(v.doubleValue() == value.doubleValue()) - { - accepted = true; - break; - } - } + protected void checkAcceptedValues(Number value) throws DataDictionaryException { + if (this.acceptedValues != null) { + boolean accepted = false; - if(!accepted) - { - throw new DataDictionaryException("Key " + this.getName() + " doesn't respect mandatory value"); - } - } - } - - private Number toNumber(String value) throws DataDictionaryException - { - Number out = null; - try - { - out = Double.valueOf((String) value); - } - catch (NumberFormatException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to Number"); - } - return out; - } + for (Number v : this.acceptedValues) { + if (v.doubleValue() == value.doubleValue()) { + accepted = true; + break; + } + } + + if (!accepted) { + throw new DataDictionaryException( + "Key " + this.getName() + " doesn't respect mandatory value"); + } + } + } + + private Number toNumber(String value) throws DataDictionaryException { + Number out = null; + try { + out = Double.valueOf((String) value); + } catch (NumberFormatException e) { + throw new DataDictionaryException( + "Key " + + this.getName() + + ": Cannot convert type " + + value.getClass().getSimpleName() + + " to Number"); + } + return out; + } } diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java index 041efc0..1a81202 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java @@ -10,100 +10,86 @@ import enedis.lab.types.DataDictionaryException; import enedis.lab.util.MinMaxChecker; -/** - * DataDictionary key descriptor Number min max - */ -public class KeyDescriptorNumberMinMax extends KeyDescriptorNumber -{ - private MinMaxChecker minMaxChecker; +/** DataDictionary key descriptor Number min max */ +public class KeyDescriptorNumberMinMax extends KeyDescriptorNumber { + private MinMaxChecker minMaxChecker; - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorNumberMinMax(String name, boolean mandatory) - { - this(name, mandatory, null, null); - } + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorNumberMinMax(String name, boolean mandatory) { + this(name, mandatory, null, null); + } - /** - * Constructor setting all attributes - * - * @param name - * @param mandatory - * @param min - * @param max - */ - public KeyDescriptorNumberMinMax(String name, boolean mandatory, Number min, Number max) - { - super(name, mandatory); - this.minMaxChecker = new MinMaxChecker(min, max); - } + /** + * Constructor setting all attributes + * + * @param name + * @param mandatory + * @param min + * @param max + */ + public KeyDescriptorNumberMinMax(String name, boolean mandatory, Number min, Number max) { + super(name, mandatory); + this.minMaxChecker = new MinMaxChecker(min, max); + } - @Override - public Number convertValue(Object value) throws DataDictionaryException - { - Number convertedValue = super.convertValue(value); + @Override + public Number convertValue(Object value) throws DataDictionaryException { + Number convertedValue = super.convertValue(value); - this.check(convertedValue); + this.check(convertedValue); - return convertedValue; - } + return convertedValue; + } - /** - * Get min - * - * @return min - */ - public Number getMin() - { - return this.minMaxChecker.getMin(); - } + /** + * Get min + * + * @return min + */ + public Number getMin() { + return this.minMaxChecker.getMin(); + } - /** - * Set min - * - * @param min - * @throws IllegalArgumentException - * if min is greater than max - */ - public void setMin(Number min) - { - this.minMaxChecker.setMin(min); - } + /** + * Set min + * + * @param min + * @throws IllegalArgumentException if min is greater than max + */ + public void setMin(Number min) { + this.minMaxChecker.setMin(min); + } - /** - * Get max - * - * @return max - */ - public Number getMax() - { - return this.minMaxChecker.getMax(); - } + /** + * Get max + * + * @return max + */ + public Number getMax() { + return this.minMaxChecker.getMax(); + } - /** - * Set max - * - * @param max - * @throws IllegalArgumentException - * if max is smaller than min - */ - public void setMax(Number max) - { - this.minMaxChecker.setMax(max); - } + /** + * Set max + * + * @param max + * @throws IllegalArgumentException if max is smaller than min + */ + public void setMax(Number max) { + this.minMaxChecker.setMax(max); + } - private void check(Number convertedValue) throws DataDictionaryException - { - if (convertedValue != null) - { - if (!this.minMaxChecker.check(convertedValue)) - { - throw new DataDictionaryException("Key " + this.getName() + ": value (" + convertedValue + ") out of bound"); - } - } - } + private void check(Number convertedValue) throws DataDictionaryException { + if (convertedValue != null) { + if (!this.minMaxChecker.check(convertedValue)) { + throw new DataDictionaryException( + "Key " + this.getName() + ": value (" + convertedValue + ") out of bound"); + } + } + } } diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java index 0de5b9f..9268fd1 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java @@ -9,77 +9,66 @@ import enedis.lab.types.DataDictionaryException; -/** - * DataDictionary key descriptor String - */ -public class KeyDescriptorString extends KeyDescriptorBase -{ - private static final boolean DEFAULT_EMPTY_ALLOW_FLAG = true; +/** DataDictionary key descriptor String */ +public class KeyDescriptorString extends KeyDescriptorBase { + private static final boolean DEFAULT_EMPTY_ALLOW_FLAG = true; - private boolean emptyAllow; + private boolean emptyAllow; - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorString(String name, boolean mandatory) - { - this(name, mandatory, DEFAULT_EMPTY_ALLOW_FLAG); - } + /** + * Default constructor + * + * @param name + * @param mandatory + */ + public KeyDescriptorString(String name, boolean mandatory) { + this(name, mandatory, DEFAULT_EMPTY_ALLOW_FLAG); + } - /** - * Default constructor - * - * @param name - * @param mandatory - * @param emptyAllow - */ - public KeyDescriptorString(String name, boolean mandatory, boolean emptyAllow) - { - super(name, mandatory); - this.emptyAllow = emptyAllow; - } + /** + * Default constructor + * + * @param name + * @param mandatory + * @param emptyAllow + */ + public KeyDescriptorString(String name, boolean mandatory, boolean emptyAllow) { + super(name, mandatory); + this.emptyAllow = emptyAllow; + } - @Override - public String convertValue(Object value) throws DataDictionaryException - { - String convertedValue = null; + @Override + public String convertValue(Object value) throws DataDictionaryException { + String convertedValue = null; - convertedValue = value.toString(); + convertedValue = value.toString(); - this.check(convertedValue); + this.check(convertedValue); - return convertedValue; - } + return convertedValue; + } - /** - * Get empty allow flag - * - * @return empty allow flag - */ - public boolean isEmptyAllow() - { - return this.emptyAllow; - } + /** + * Get empty allow flag + * + * @return empty allow flag + */ + public boolean isEmptyAllow() { + return this.emptyAllow; + } - /** - * Set empty allow flag - * - * @param emptyAllow - */ - public void setEmptyAllow(boolean emptyAllow) - { - this.emptyAllow = emptyAllow; - } - - private void check(String value) throws DataDictionaryException - { - if (!this.emptyAllow && value.trim().isEmpty()) - { - throw new DataDictionaryException("Key " + this.getName() + ": value can't be empty"); - } - } + /** + * Set empty allow flag + * + * @param emptyAllow + */ + public void setEmptyAllow(boolean emptyAllow) { + this.emptyAllow = emptyAllow; + } + private void check(String value) throws DataDictionaryException { + if (!this.emptyAllow && value.trim().isEmpty()) { + throw new DataDictionaryException("Key " + this.getName() + ": value can't be empty"); + } + } } diff --git a/src/main/java/enedis/lab/util/MinMaxChecker.java b/src/main/java/enedis/lab/util/MinMaxChecker.java index 6354bcd..cb3281f 100644 --- a/src/main/java/enedis/lab/util/MinMaxChecker.java +++ b/src/main/java/enedis/lab/util/MinMaxChecker.java @@ -7,124 +7,97 @@ package enedis.lab.util; -/** - * Min max class - */ -public class MinMaxChecker -{ - private Number min; - private Number max; +/** Min max class */ +public class MinMaxChecker { + private Number min; + private Number max; - /** - * Default constructor - */ - public MinMaxChecker() - { - super(); - } + /** Default constructor */ + public MinMaxChecker() { + super(); + } - /** - * Constructor setting parameters - * - * @param min - * @param max - */ - public MinMaxChecker(Number min, Number max) - { - this(); - this.setMin(min); - this.setMax(max); - } + /** + * Constructor setting parameters + * + * @param min + * @param max + */ + public MinMaxChecker(Number min, Number max) { + this(); + this.setMin(min); + this.setMax(max); + } - /** - * Get min - * - * @return min - */ - public Number getMin() - { - return this.min; - } + /** + * Get min + * + * @return min + */ + public Number getMin() { + return this.min; + } - /** - * Set min - * - * @param min - * @throws IllegalArgumentException - * if min is greater than max - */ - public void setMin(Number min) - { - if (min != null) - { - if (this.max != null && min.doubleValue() > this.max.doubleValue()) - { - throw new IllegalArgumentException("min can't be greater than max"); - } - } - this.min = min; - } + /** + * Set min + * + * @param min + * @throws IllegalArgumentException if min is greater than max + */ + public void setMin(Number min) { + if (min != null) { + if (this.max != null && min.doubleValue() > this.max.doubleValue()) { + throw new IllegalArgumentException("min can't be greater than max"); + } + } + this.min = min; + } - /** - * Get max - * - * @return max - */ - public Number getMax() - { - return this.max; - } + /** + * Get max + * + * @return max + */ + public Number getMax() { + return this.max; + } - /** - * Set max - * - * @param max - * @throws IllegalArgumentException - * if max is smaller than min - */ - public void setMax(Number max) - { - if (max != null) - { - if (this.min != null && max.doubleValue() < this.min.doubleValue()) - { - throw new IllegalArgumentException("max can't be smaller than min"); - } - } - this.max = max; - } - - /** - * Check the given value - * - * @param value - * @return true if the value is in [min, max] - */ - public boolean check(Number value) - { - if (value != null) - { - if (this.min != null) - { - if (value.doubleValue() < this.min.doubleValue()) - { - return false; - } - } - if (this.max != null) - { - if (value.doubleValue() > this.max.doubleValue()) - { - return false; - } - } - return true; - } - else - { - return false; - } - - } + /** + * Set max + * + * @param max + * @throws IllegalArgumentException if max is smaller than min + */ + public void setMax(Number max) { + if (max != null) { + if (this.min != null && max.doubleValue() < this.min.doubleValue()) { + throw new IllegalArgumentException("max can't be smaller than min"); + } + } + this.max = max; + } + /** + * Check the given value + * + * @param value + * @return true if the value is in [min, max] + */ + public boolean check(Number value) { + if (value != null) { + if (this.min != null) { + if (value.doubleValue() < this.min.doubleValue()) { + return false; + } + } + if (this.max != null) { + if (value.doubleValue() > this.max.doubleValue()) { + return false; + } + } + return true; + } else { + return false; + } + } } diff --git a/src/main/java/enedis/lab/util/SystemError.java b/src/main/java/enedis/lab/util/SystemError.java index d348446..882afe1 100644 --- a/src/main/java/enedis/lab/util/SystemError.java +++ b/src/main/java/enedis/lab/util/SystemError.java @@ -7,59 +7,45 @@ package enedis.lab.util; -import org.apache.commons.lang3.SystemUtils; - import com.sun.jna.Native; import com.sun.jna.platform.win32.Kernel32Util; +import org.apache.commons.lang3.SystemUtils; -/** - * Class for system error - * - */ -public class SystemError -{ - /** - * Get system last error code - * - * @return System last error code - */ - static public int getCode() - { - return Native.getLastError(); - } - - /** - * Get system last error message - * - * @return System last error message - */ - static public String getMessage() - { - return getMessage(getCode()); - } - - /** - * Get system error message associated with code - * - * @param code - * the system error code - * - * @return System error message - */ - static public String getMessage(int code) - { - String message = null; - - if (SystemUtils.IS_OS_WINDOWS) - { - message = Kernel32Util.formatMessage(code); - } - else if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_UNIX || SystemUtils.IS_OS_MAC_OSX) - { - message = SystemLibC.INSTANCE.strerror(code); - } - - return message; - } - +/** Class for system error */ +public class SystemError { + /** + * Get system last error code + * + * @return System last error code + */ + public static int getCode() { + return Native.getLastError(); + } + + /** + * Get system last error message + * + * @return System last error message + */ + public static String getMessage() { + return getMessage(getCode()); + } + + /** + * Get system error message associated with code + * + * @param code the system error code + * @return System error message + */ + public static String getMessage(int code) { + String message = null; + + if (SystemUtils.IS_OS_WINDOWS) { + message = Kernel32Util.formatMessage(code); + } else if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_UNIX || SystemUtils.IS_OS_MAC_OSX) { + message = SystemLibC.INSTANCE.strerror(code); + } + + return message; + } } diff --git a/src/main/java/enedis/lab/util/SystemLibC.java b/src/main/java/enedis/lab/util/SystemLibC.java index 49592d5..13f97dd 100644 --- a/src/main/java/enedis/lab/util/SystemLibC.java +++ b/src/main/java/enedis/lab/util/SystemLibC.java @@ -10,22 +10,16 @@ import com.sun.jna.Library; import com.sun.jna.Native; -/** - * Interface for system of C library - * - */ -public interface SystemLibC extends Library -{ - /** - * Instance - */ - SystemLibC INSTANCE = Native.load("c", SystemLibC.class); +/** Interface for system of C library */ +public interface SystemLibC extends Library { + /** Instance */ + SystemLibC INSTANCE = Native.load("c", SystemLibC.class); - /** - * Get string error from code - * - * @param code - * @return string error - */ - public String strerror(int code); + /** + * Get string error from code + * + * @param code + * @return string error + */ + public String strerror(int code); } diff --git a/src/main/java/enedis/lab/util/message/Event.java b/src/main/java/enedis/lab/util/message/Event.java index 6d07741..0e7ed75 100644 --- a/src/main/java/enedis/lab/util/message/Event.java +++ b/src/main/java/enedis/lab/util/message/Event.java @@ -7,128 +7,113 @@ package enedis.lab.util.message; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * Event class - * - * Generated + * + *

    Generated */ -public abstract class Event extends Message -{ - protected static final String KEY_DATE_TIME = "dateTime"; - - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.EVENT; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorLocalDateTime kDateTime; - - protected Event() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Event(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Event(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param dateTime - * @throws DataDictionaryException - */ - public Event(String name, LocalDateTime dateTime) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setDateTime(dateTime); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - /** - * Get date time - * - * @return the date time - */ - public LocalDateTime getDateTime() - { - return (LocalDateTime) this.data.get(KEY_DATE_TIME); - } - - /** - * Set date time - * - * @param dateTime - * @throws DataDictionaryException - */ - public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException - { - this.setDateTime((Object) dateTime); - } - - protected void setDateTime(Object dateTime) throws DataDictionaryException - { - this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); - } - - private void loadKeyDescriptors() - { - try - { - this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); - this.keys.add(this.kDateTime); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public abstract class Event extends Message { + protected static final String KEY_DATE_TIME = "dateTime"; + + private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.EVENT; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorLocalDateTime kDateTime; + + protected Event() { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Event(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Event(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param dateTime + * @throws DataDictionaryException + */ + public Event(String name, LocalDateTime dateTime) throws DataDictionaryException { + this(); + + this.setName(name); + this.setDateTime(dateTime); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_TYPE)) { + this.setType(TYPE_ACCEPTED_VALUE); + } + super.updateOptionalParameters(); + } + + /** + * Get date time + * + * @return the date time + */ + public LocalDateTime getDateTime() { + return (LocalDateTime) this.data.get(KEY_DATE_TIME); + } + + /** + * Set date time + * + * @param dateTime + * @throws DataDictionaryException + */ + public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException { + this.setDateTime((Object) dateTime); + } + + protected void setDateTime(Object dateTime) throws DataDictionaryException { + this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); + } + + private void loadKeyDescriptors() { + try { + this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); + this.keys.add(this.kDateTime); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/lab/util/message/Message.java b/src/main/java/enedis/lab/util/message/Message.java index 74ebb9e..64e6235 100644 --- a/src/main/java/enedis/lab/util/message/Message.java +++ b/src/main/java/enedis/lab/util/message/Message.java @@ -7,155 +7,138 @@ package enedis.lab.util.message; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorEnum; import enedis.lab.types.datadictionary.KeyDescriptorString; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * Message class * - * Generated + *

    Generated */ -public class Message extends DataDictionaryBase -{ - /** Key type */ - public static final String KEY_TYPE = "type"; - /** Key name */ - public static final String KEY_NAME = "name"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kType; - protected KeyDescriptorString kName; - - protected Message() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Message(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Message(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @throws DataDictionaryException - */ - public Message(MessageType type, String name) throws DataDictionaryException - { - this(); - - this.setType(type); - this.setName(name); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /** - * Get type - * - * @return the type - */ - public MessageType getType() - { - return (MessageType) this.data.get(KEY_TYPE); - } - - /** - * Get name - * - * @return the name - */ - public String getName() - { - return (String) this.data.get(KEY_NAME); - } - - /** - * Set type - * - * @param type - * @throws DataDictionaryException - */ - public void setType(MessageType type) throws DataDictionaryException - { - this.setType((Object) type); - } - - /** - * Set name - * - * @param name - * @throws DataDictionaryException - */ - public void setName(String name) throws DataDictionaryException - { - this.setName((Object) name); - } - - protected void setType(Object type) throws DataDictionaryException - { - this.data.put(KEY_TYPE, this.kType.convert(type)); - } - - protected void setName(Object name) throws DataDictionaryException - { - this.data.put(KEY_NAME, this.kName.convert(name)); - } - - private void loadKeyDescriptors() - { - try - { - this.kType = new KeyDescriptorEnum(KEY_TYPE, true, MessageType.class); - this.keys.add(this.kType); - - this.kName = new KeyDescriptorString(KEY_NAME, true, false); - this.keys.add(this.kName); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class Message extends DataDictionaryBase { + /** Key type */ + public static final String KEY_TYPE = "type"; + + /** Key name */ + public static final String KEY_NAME = "name"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorEnum kType; + protected KeyDescriptorString kName; + + protected Message() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Message(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Message(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param type + * @param name + * @throws DataDictionaryException + */ + public Message(MessageType type, String name) throws DataDictionaryException { + this(); + + this.setType(type); + this.setName(name); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + super.updateOptionalParameters(); + } + + /** + * Get type + * + * @return the type + */ + public MessageType getType() { + return (MessageType) this.data.get(KEY_TYPE); + } + + /** + * Get name + * + * @return the name + */ + public String getName() { + return (String) this.data.get(KEY_NAME); + } + + /** + * Set type + * + * @param type + * @throws DataDictionaryException + */ + public void setType(MessageType type) throws DataDictionaryException { + this.setType((Object) type); + } + + /** + * Set name + * + * @param name + * @throws DataDictionaryException + */ + public void setName(String name) throws DataDictionaryException { + this.setName((Object) name); + } + + protected void setType(Object type) throws DataDictionaryException { + this.data.put(KEY_TYPE, this.kType.convert(type)); + } + + protected void setName(Object name) throws DataDictionaryException { + this.data.put(KEY_NAME, this.kName.convert(name)); + } + + private void loadKeyDescriptors() { + try { + this.kType = new KeyDescriptorEnum(KEY_TYPE, true, MessageType.class); + this.keys.add(this.kType); + + this.kName = new KeyDescriptorString(KEY_NAME, true, false); + this.keys.add(this.kName); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/lab/util/message/MessageType.java b/src/main/java/enedis/lab/util/message/MessageType.java index 6441f34..fdec4e4 100644 --- a/src/main/java/enedis/lab/util/message/MessageType.java +++ b/src/main/java/enedis/lab/util/message/MessageType.java @@ -7,15 +7,12 @@ package enedis.lab.util.message; -/** - * Message type - */ -public enum MessageType -{ - /** Event */ - EVENT, - /** Request */ - REQUEST, - /** Response */ - RESPONSE; +/** Message type */ +public enum MessageType { + /** Event */ + EVENT, + /** Request */ + REQUEST, + /** Response */ + RESPONSE; } diff --git a/src/main/java/enedis/lab/util/message/Request.java b/src/main/java/enedis/lab/util/message/Request.java index b27b774..b7a7950 100644 --- a/src/main/java/enedis/lab/util/message/Request.java +++ b/src/main/java/enedis/lab/util/message/Request.java @@ -7,94 +7,82 @@ package enedis.lab.util.message; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * Request class - * - * Generated + * + *

    Generated */ -public abstract class Request extends Message -{ - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.REQUEST; +public abstract class Request extends Message { + private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.REQUEST; - private List> keys = new ArrayList>(); + private List> keys = new ArrayList>(); - protected Request() - { - super(); - this.loadKeyDescriptors(); + protected Request() { + super(); + this.loadKeyDescriptors(); - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + } - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Request(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Request(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Request(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Request(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @throws DataDictionaryException - */ - public Request(MessageType type, String name) throws DataDictionaryException - { - this(); + /** + * Constructor setting parameters to specific values + * + * @param type + * @param name + * @throws DataDictionaryException + */ + public Request(MessageType type, String name) throws DataDictionaryException { + this(); - this.setType(type); - this.setName(name); + this.setType(type); + this.setName(name); - this.checkAndUpdate(); - } + this.checkAndUpdate(); + } - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_TYPE)) { + this.setType(TYPE_ACCEPTED_VALUE); + } + super.updateOptionalParameters(); + } - private void loadKeyDescriptors() - { - try - { + private void loadKeyDescriptors() { + try { - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/lab/util/message/Response.java b/src/main/java/enedis/lab/util/message/Response.java index 67e4ec8..0c1cf55 100644 --- a/src/main/java/enedis/lab/util/message/Response.java +++ b/src/main/java/enedis/lab/util/message/Response.java @@ -7,198 +7,179 @@ package enedis.lab.util.message; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; import enedis.lab.types.datadictionary.KeyDescriptorNumber; import enedis.lab.types.datadictionary.KeyDescriptorString; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * Response class * - * Generated + *

    Generated */ -public abstract class Response extends Message -{ - protected static final String KEY_DATE_TIME = "dateTime"; - protected static final String KEY_ERROR_CODE = "errorCode"; - protected static final String KEY_ERROR_MESSAGE = "errorMessage"; - - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.RESPONSE; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorLocalDateTime kDateTime; - protected KeyDescriptorNumber kErrorCode; - protected KeyDescriptorString kErrorMessage; - - protected Response() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Response(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Response(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public Response(MessageType type, String name, LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(); - - this.setType(type); - this.setName(name); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - /** - * Get date time - * - * @return the date time - */ - public LocalDateTime getDateTime() - { - return (LocalDateTime) this.data.get(KEY_DATE_TIME); - } - - /** - * Get error code - * - * @return the error code - */ - public Number getErrorCode() - { - return (Number) this.data.get(KEY_ERROR_CODE); - } - - /** - * Get error message - * - * @return the error message - */ - public String getErrorMessage() - { - return (String) this.data.get(KEY_ERROR_MESSAGE); - } - - /** - * Set date time - * - * @param dateTime - * @throws DataDictionaryException - */ - public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException - { - this.setDateTime((Object) dateTime); - } - - /** - * Set error code - * - * @param errorCode - * @throws DataDictionaryException - */ - public void setErrorCode(Number errorCode) throws DataDictionaryException - { - this.setErrorCode((Object) errorCode); - } - - /** - * Set error message - * - * @param errorMessage - * @throws DataDictionaryException - */ - public void setErrorMessage(String errorMessage) throws DataDictionaryException - { - this.setErrorMessage((Object) errorMessage); - } - - protected void setDateTime(Object dateTime) throws DataDictionaryException - { - this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); - } - - protected void setErrorCode(Object errorCode) throws DataDictionaryException - { - this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); - } - - protected void setErrorMessage(Object errorMessage) throws DataDictionaryException - { - this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); - } - - private void loadKeyDescriptors() - { - try - { - this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); - this.keys.add(this.kDateTime); - - this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); - this.keys.add(this.kErrorCode); - - this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, false, true); - this.keys.add(this.kErrorMessage); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public abstract class Response extends Message { + protected static final String KEY_DATE_TIME = "dateTime"; + protected static final String KEY_ERROR_CODE = "errorCode"; + protected static final String KEY_ERROR_MESSAGE = "errorMessage"; + + private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.RESPONSE; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorLocalDateTime kDateTime; + protected KeyDescriptorNumber kErrorCode; + protected KeyDescriptorString kErrorMessage; + + protected Response() { + super(); + this.loadKeyDescriptors(); + + this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public Response(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public Response(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param type + * @param name + * @param dateTime + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public Response( + MessageType type, String name, LocalDateTime dateTime, Number errorCode, String errorMessage) + throws DataDictionaryException { + this(); + + this.setType(type); + this.setName(name); + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_TYPE)) { + this.setType(TYPE_ACCEPTED_VALUE); + } + super.updateOptionalParameters(); + } + + /** + * Get date time + * + * @return the date time + */ + public LocalDateTime getDateTime() { + return (LocalDateTime) this.data.get(KEY_DATE_TIME); + } + + /** + * Get error code + * + * @return the error code + */ + public Number getErrorCode() { + return (Number) this.data.get(KEY_ERROR_CODE); + } + + /** + * Get error message + * + * @return the error message + */ + public String getErrorMessage() { + return (String) this.data.get(KEY_ERROR_MESSAGE); + } + + /** + * Set date time + * + * @param dateTime + * @throws DataDictionaryException + */ + public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException { + this.setDateTime((Object) dateTime); + } + + /** + * Set error code + * + * @param errorCode + * @throws DataDictionaryException + */ + public void setErrorCode(Number errorCode) throws DataDictionaryException { + this.setErrorCode((Object) errorCode); + } + + /** + * Set error message + * + * @param errorMessage + * @throws DataDictionaryException + */ + public void setErrorMessage(String errorMessage) throws DataDictionaryException { + this.setErrorMessage((Object) errorMessage); + } + + protected void setDateTime(Object dateTime) throws DataDictionaryException { + this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); + } + + protected void setErrorCode(Object errorCode) throws DataDictionaryException { + this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); + } + + protected void setErrorMessage(Object errorMessage) throws DataDictionaryException { + this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); + } + + private void loadKeyDescriptors() { + try { + this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); + this.keys.add(this.kDateTime); + + this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); + this.keys.add(this.kErrorCode); + + this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, false, true); + this.keys.add(this.kErrorMessage); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/lab/util/message/ResponseBase.java b/src/main/java/enedis/lab/util/message/ResponseBase.java index cd253e9..0ac4de4 100644 --- a/src/main/java/enedis/lab/util/message/ResponseBase.java +++ b/src/main/java/enedis/lab/util/message/ResponseBase.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,128 +7,121 @@ package enedis.lab.util.message; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * ResponseBase class - * - * Generated + * + *

    Generated */ -public class ResponseBase extends Response -{ - protected static final String KEY_DATA = "data"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - protected ResponseBase() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseBase(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseBase(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseBase(String name, LocalDateTime dateTime, Number errorCode, String errorMessage, DataDictionaryBase data) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /** - * Get data - * - * @return the data - */ - public DataDictionaryBase getData() - { - return (DataDictionaryBase) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(DataDictionaryBase data) throws DataDictionaryException - { - this.setData((Object) data); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, DataDictionaryBase.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class ResponseBase extends Response { + protected static final String KEY_DATA = "data"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + protected ResponseBase() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseBase(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseBase(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseBase( + String name, + LocalDateTime dateTime, + Number errorCode, + String errorMessage, + DataDictionaryBase data) + throws DataDictionaryException { + this(); + + this.setName(name); + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + public DataDictionaryBase getData() { + return (DataDictionaryBase) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(DataDictionaryBase data) throws DataDictionaryException { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() { + try { + this.kData = + new KeyDescriptorDataDictionary( + KEY_DATA, false, DataDictionaryBase.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/lab/util/message/exception/MessageException.java b/src/main/java/enedis/lab/util/message/exception/MessageException.java index 985289f..9b06c40 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageException.java @@ -7,51 +7,41 @@ package enedis.lab.util.message.exception; -/** - * Message exception - */ -public class MessageException extends Exception -{ - - private static final long serialVersionUID = -2263755971102386572L; - - /** - * Default constructor - */ - public MessageException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageException(Throwable cause) - { - super(cause); - } - +/** Message exception */ +public class MessageException extends Exception { + + private static final long serialVersionUID = -2263755971102386572L; + + /** Default constructor */ + public MessageException() { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageException(String message) { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageException(Throwable cause) { + super(cause); + } } diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java index a8e2d62..f1fed8b 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,51 +7,41 @@ package enedis.lab.util.message.exception; -/** - * Unvalid message format exception - */ -public class MessageInvalidContentException extends MessageException -{ - - private static final long serialVersionUID = -2263755971102386572L; - - /** - * Default constructor - */ - public MessageInvalidContentException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageInvalidContentException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageInvalidContentException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageInvalidContentException(Throwable cause) - { - super(cause); - } - +/** Unvalid message format exception */ +public class MessageInvalidContentException extends MessageException { + + private static final long serialVersionUID = -2263755971102386572L; + + /** Default constructor */ + public MessageInvalidContentException() { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageInvalidContentException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageInvalidContentException(String message) { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageInvalidContentException(Throwable cause) { + super(cause); + } } diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java index 1e17c57..87127fb 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java @@ -7,51 +7,41 @@ package enedis.lab.util.message.exception; -/** - * Unvalid message format exception - */ -public class MessageInvalidFormatException extends MessageException -{ - - private static final long serialVersionUID = -2263755971102386572L; - - /** - * Default constructor - */ - public MessageInvalidFormatException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageInvalidFormatException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageInvalidFormatException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageInvalidFormatException(Throwable cause) - { - super(cause); - } - +/** Unvalid message format exception */ +public class MessageInvalidFormatException extends MessageException { + + private static final long serialVersionUID = -2263755971102386572L; + + /** Default constructor */ + public MessageInvalidFormatException() { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageInvalidFormatException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageInvalidFormatException(String message) { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageInvalidFormatException(Throwable cause) { + super(cause); + } } diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java index a351317..c5a6a2e 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java @@ -7,51 +7,41 @@ package enedis.lab.util.message.exception; -/** - * Unvalid message format exception - */ -public class MessageInvalidTypeException extends MessageException -{ - - private static final long serialVersionUID = -2263755971102386572L; - - /** - * Default constructor - */ - public MessageInvalidTypeException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageInvalidTypeException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageInvalidTypeException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageInvalidTypeException(Throwable cause) - { - super(cause); - } - +/** Unvalid message format exception */ +public class MessageInvalidTypeException extends MessageException { + + private static final long serialVersionUID = -2263755971102386572L; + + /** Default constructor */ + public MessageInvalidTypeException() { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageInvalidTypeException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageInvalidTypeException(String message) { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageInvalidTypeException(Throwable cause) { + super(cause); + } } diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java index faed525..443ac5e 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java @@ -7,51 +7,41 @@ package enedis.lab.util.message.exception; -/** - * Unvalid message format exception - */ -public class MessageKeyNameDoesntExistException extends MessageException -{ - - private static final long serialVersionUID = -2263755971102386572L; - - /** - * Default constructor - */ - public MessageKeyNameDoesntExistException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageKeyNameDoesntExistException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageKeyNameDoesntExistException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageKeyNameDoesntExistException(Throwable cause) - { - super(cause); - } - +/** Unvalid message format exception */ +public class MessageKeyNameDoesntExistException extends MessageException { + + private static final long serialVersionUID = -2263755971102386572L; + + /** Default constructor */ + public MessageKeyNameDoesntExistException() { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageKeyNameDoesntExistException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageKeyNameDoesntExistException(String message) { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageKeyNameDoesntExistException(Throwable cause) { + super(cause); + } } diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java index ea030a7..ee427c9 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,51 +7,41 @@ package enedis.lab.util.message.exception; -/** - * Unvalid message format exception - */ -public class MessageKeyTypeDoesntExistException extends MessageException -{ - - private static final long serialVersionUID = -2263755971102386572L; - - /** - * Default constructor - */ - public MessageKeyTypeDoesntExistException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageKeyTypeDoesntExistException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageKeyTypeDoesntExistException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageKeyTypeDoesntExistException(Throwable cause) - { - super(cause); - } - +/** Unvalid message format exception */ +public class MessageKeyTypeDoesntExistException extends MessageException { + + private static final long serialVersionUID = -2263755971102386572L; + + /** Default constructor */ + public MessageKeyTypeDoesntExistException() { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public MessageKeyTypeDoesntExistException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public MessageKeyTypeDoesntExistException(String message) { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public MessageKeyTypeDoesntExistException(Throwable cause) { + super(cause); + } } diff --git a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java index 62cb26f..6bafbce 100644 --- a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java +++ b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java @@ -7,51 +7,41 @@ package enedis.lab.util.message.exception; -/** - * Unvalid message format exception - */ -public class UnsupportedMessageException extends MessageException -{ - - private static final long serialVersionUID = -2263755971102386572L; - - /** - * Default constructor - */ - public UnsupportedMessageException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public UnsupportedMessageException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public UnsupportedMessageException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public UnsupportedMessageException(Throwable cause) - { - super(cause); - } - +/** Unvalid message format exception */ +public class UnsupportedMessageException extends MessageException { + + private static final long serialVersionUID = -2263755971102386572L; + + /** Default constructor */ + public UnsupportedMessageException() { + super(); + } + + /** + * Constructor using message and cause + * + * @param message + * @param cause + */ + public UnsupportedMessageException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor using message + * + * @param message + */ + public UnsupportedMessageException(String message) { + super(message); + } + + /** + * Constructor using cause + * + * @param cause + */ + public UnsupportedMessageException(Throwable cause) { + super(cause); + } } diff --git a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java index f041ece..7451122 100644 --- a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,92 +7,89 @@ package enedis.lab.util.message.factory; -import java.util.HashMap; -import java.util.Map; - -import org.json.JSONException; - import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; import enedis.lab.util.message.Message; import enedis.lab.util.message.exception.MessageInvalidContentException; import enedis.lab.util.message.exception.MessageInvalidFormatException; import enedis.lab.util.message.exception.UnsupportedMessageException; +import java.util.HashMap; +import java.util.Map; +import org.json.JSONException; /** * Message factory - * + * * @param */ -public class AbstractMessageFactory -{ - private Class clazz; - private Map> messageClasses; - - /** - * Default constructor - * - * @param clazz - */ - public AbstractMessageFactory(Class clazz) - { - this.clazz = clazz; - this.messageClasses = new HashMap>(); - } +public class AbstractMessageFactory { + private Class clazz; + private Map> messageClasses; - /** - * Get message from text - * - * @param text - * @param name - * @return message - * @throws UnsupportedMessageException - * @throws MessageInvalidFormatException - * @throws MessageInvalidContentException - */ - public final T getMessage(String text, String name) throws UnsupportedMessageException, MessageInvalidFormatException, MessageInvalidContentException - { - T message = null; + /** + * Default constructor + * + * @param clazz + */ + public AbstractMessageFactory(Class clazz) { + this.clazz = clazz; + this.messageClasses = new HashMap>(); + } - try - { - Class messageClazz = this.messageClasses.get(name); + /** + * Get message from text + * + * @param text + * @param name + * @return message + * @throws UnsupportedMessageException + * @throws MessageInvalidFormatException + * @throws MessageInvalidContentException + */ + public final T getMessage(String text, String name) + throws UnsupportedMessageException, + MessageInvalidFormatException, + MessageInvalidContentException { + T message = null; - if (messageClazz != null) - { - message = messageClazz.cast(DataDictionaryBase.fromString(text, messageClazz)); - } - else - { - throw new UnsupportedMessageException("Unsupported " + this.clazz.getSimpleName() + " : " + name); - } - } - catch (JSONException e) - { - throw new MessageInvalidFormatException("Invalid " + this.clazz.getSimpleName() + " " + name + " format, it should be JSON : " + e.getMessage(), e); - } - catch (DataDictionaryException e) - { - throw new MessageInvalidContentException("Invalid " + this.clazz.getSimpleName() + " " + name + " content : " + e.getMessage(), e); - } + try { + Class messageClazz = this.messageClasses.get(name); - return message; - } + if (messageClazz != null) { + message = messageClazz.cast(DataDictionaryBase.fromString(text, messageClazz)); + } else { + throw new UnsupportedMessageException( + "Unsupported " + this.clazz.getSimpleName() + " : " + name); + } + } catch (JSONException e) { + throw new MessageInvalidFormatException( + "Invalid " + + this.clazz.getSimpleName() + + " " + + name + + " format, it should be JSON : " + + e.getMessage(), + e); + } catch (DataDictionaryException e) { + throw new MessageInvalidContentException( + "Invalid " + this.clazz.getSimpleName() + " " + name + " content : " + e.getMessage(), e); + } - /** - * Add a Message class to decode message with the given name - * - * @param name - * @param messageClazz - */ - public final void addMessageClass(String name, Class messageClazz) - { - if (name == null || messageClazz == null) - { - throw new IllegalArgumentException("Name and " + this.clazz.getSimpleName() + " class can't be null"); - } + return message; + } - this.messageClasses.put(name, messageClazz); - } + /** + * Add a Message class to decode message with the given name + * + * @param name + * @param messageClazz + */ + public final void addMessageClass(String name, Class messageClazz) { + if (name == null || messageClazz == null) { + throw new IllegalArgumentException( + "Name and " + this.clazz.getSimpleName() + " class can't be null"); + } -} \ No newline at end of file + this.messageClasses.put(name, messageClazz); + } +} diff --git a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java index 84a8e75..3169c87 100644 --- a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java @@ -7,9 +7,6 @@ package enedis.lab.util.message.factory; -import org.json.JSONException; -import org.json.JSONObject; - import enedis.lab.types.DataDictionaryException; import enedis.lab.util.message.Message; import enedis.lab.util.message.MessageType; @@ -17,102 +14,88 @@ import enedis.lab.util.message.exception.MessageInvalidTypeException; import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; +import org.json.JSONException; +import org.json.JSONObject; -/** - * Message factory - */ -public abstract class BasicMessageFactory -{ - /** - * Get message from text - * - * @param text - * @return message - * @throws MessageInvalidFormatException - * @throws MessageKeyTypeDoesntExistException - * @throws MessageKeyNameDoesntExistException - * @throws MessageInvalidTypeException - */ - public static Message getMessage(String text) - throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, MessageInvalidTypeException - { - JSONObject messageJson = convertTextToJson(text); +/** Message factory */ +public abstract class BasicMessageFactory { + /** + * Get message from text + * + * @param text + * @return message + * @throws MessageInvalidFormatException + * @throws MessageKeyTypeDoesntExistException + * @throws MessageKeyNameDoesntExistException + * @throws MessageInvalidTypeException + */ + public static Message getMessage(String text) + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException { + JSONObject messageJson = convertTextToJson(text); - MessageType type = extractType(messageJson); - String name = extractName(messageJson); + MessageType type = extractType(messageJson); + String name = extractName(messageJson); - return createMessage(type, name); - } + return createMessage(type, name); + } - private static JSONObject convertTextToJson(String text) throws MessageInvalidFormatException - { - try - { - return new JSONObject(text); - } - catch (JSONException e) - { - throw new MessageInvalidFormatException("Invalid format, it should be JSON : " + e.getMessage()); - } - } + private static JSONObject convertTextToJson(String text) throws MessageInvalidFormatException { + try { + return new JSONObject(text); + } catch (JSONException e) { + throw new MessageInvalidFormatException( + "Invalid format, it should be JSON : " + e.getMessage()); + } + } - private static MessageType extractType(JSONObject jsonObj) throws MessageKeyTypeDoesntExistException, MessageInvalidFormatException, MessageInvalidTypeException - { - try - { - checkKeyTypeExists(jsonObj); - String typeStr = jsonObj.getString(Message.KEY_TYPE); - MessageType type = MessageType.valueOf(typeStr); - return type; - } - catch (JSONException e) - { - throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); - } - catch (IllegalArgumentException e) - { - throw new MessageInvalidTypeException("Invalid format : " + e.getMessage()); - } - } + private static MessageType extractType(JSONObject jsonObj) + throws MessageKeyTypeDoesntExistException, + MessageInvalidFormatException, + MessageInvalidTypeException { + try { + checkKeyTypeExists(jsonObj); + String typeStr = jsonObj.getString(Message.KEY_TYPE); + MessageType type = MessageType.valueOf(typeStr); + return type; + } catch (JSONException e) { + throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); + } catch (IllegalArgumentException e) { + throw new MessageInvalidTypeException("Invalid format : " + e.getMessage()); + } + } - private static void checkKeyTypeExists(JSONObject jsonObj) throws MessageKeyTypeDoesntExistException - { - if (!jsonObj.has(Message.KEY_TYPE)) - { - throw new MessageKeyTypeDoesntExistException("Key type missing"); - } - } + private static void checkKeyTypeExists(JSONObject jsonObj) + throws MessageKeyTypeDoesntExistException { + if (!jsonObj.has(Message.KEY_TYPE)) { + throw new MessageKeyTypeDoesntExistException("Key type missing"); + } + } - private static String extractName(JSONObject jsonObj) throws MessageInvalidFormatException, MessageKeyNameDoesntExistException - { - try - { - checkKeyNameExists(jsonObj); - return jsonObj.getString(Message.KEY_NAME); - } - catch (JSONException e) - { - throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); - } - } + private static String extractName(JSONObject jsonObj) + throws MessageInvalidFormatException, MessageKeyNameDoesntExistException { + try { + checkKeyNameExists(jsonObj); + return jsonObj.getString(Message.KEY_NAME); + } catch (JSONException e) { + throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); + } + } - private static void checkKeyNameExists(JSONObject jsonObj) throws MessageKeyNameDoesntExistException - { - if (!jsonObj.has(Message.KEY_NAME)) - { - throw new MessageKeyNameDoesntExistException("Key name missing"); - } - } + private static void checkKeyNameExists(JSONObject jsonObj) + throws MessageKeyNameDoesntExistException { + if (!jsonObj.has(Message.KEY_NAME)) { + throw new MessageKeyNameDoesntExistException("Key name missing"); + } + } - private static Message createMessage(MessageType type, String name) - { - try - { - return new Message(type, name); - } - catch (DataDictionaryException e) - { - return null; - } - } + private static Message createMessage(MessageType type, String name) { + try { + return new Message(type, name); + } catch (DataDictionaryException e) { + return null; + } + } } diff --git a/src/main/java/enedis/lab/util/message/factory/EventFactory.java b/src/main/java/enedis/lab/util/message/factory/EventFactory.java index f349021..5613aca 100644 --- a/src/main/java/enedis/lab/util/message/factory/EventFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/EventFactory.java @@ -9,16 +9,10 @@ import enedis.lab.util.message.Event; -/** - * Request factory - */ -public class EventFactory extends AbstractMessageFactory -{ - /** - * Default constructor - */ - public EventFactory() - { - super(Event.class); - } +/** Request factory */ +public class EventFactory extends AbstractMessageFactory { + /** Default constructor */ + public EventFactory() { + super(Event.class); + } } diff --git a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java index 1e6f606..2d647b8 100644 --- a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -15,108 +15,106 @@ import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; import enedis.lab.util.message.exception.UnsupportedMessageException; -/** - * Message factory - */ -public class MessageFactory -{ - private RequestFactory requestFactory; - private ResponseFactory responseFactory; - private EventFactory eventFactory; - - /** - * Default constructor - */ - public MessageFactory() - { - super(); - } +/** Message factory */ +public class MessageFactory { + private RequestFactory requestFactory; + private ResponseFactory responseFactory; + private EventFactory eventFactory; - /** - * Constructor using field - * - * @param requestFactory - * @param responseFactory - * @param eventFactory - */ - public MessageFactory(RequestFactory requestFactory, ResponseFactory responseFactory, EventFactory eventFactory) - { - super(); - this.requestFactory = requestFactory; - this.responseFactory = responseFactory; - this.eventFactory = eventFactory; - } + /** Default constructor */ + public MessageFactory() { + super(); + } - /** - * Get message from String - * - * @param text - * @return message - * @throws MessageInvalidTypeException - * @throws MessageKeyNameDoesntExistException - * @throws MessageKeyTypeDoesntExistException - * @throws MessageInvalidFormatException - * @throws MessageInvalidContentException - * @throws UnsupportedMessageException - */ - public Message getMessage(String text) throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - this.checkSubFactoryReferences(); + /** + * Constructor using field + * + * @param requestFactory + * @param responseFactory + * @param eventFactory + */ + public MessageFactory( + RequestFactory requestFactory, ResponseFactory responseFactory, EventFactory eventFactory) { + super(); + this.requestFactory = requestFactory; + this.responseFactory = responseFactory; + this.eventFactory = eventFactory; + } - Message genericMessage = BasicMessageFactory.getMessage(text); - Message message = null; + /** + * Get message from String + * + * @param text + * @return message + * @throws MessageInvalidTypeException + * @throws MessageKeyNameDoesntExistException + * @throws MessageKeyTypeDoesntExistException + * @throws MessageInvalidFormatException + * @throws MessageInvalidContentException + * @throws UnsupportedMessageException + */ + public Message getMessage(String text) + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + this.checkSubFactoryReferences(); - // @formatter:off - switch (genericMessage.getType()) - { - case REQUEST: message = this.requestFactory.getMessage(text, genericMessage.getName()); break; - case RESPONSE: message = this.responseFactory.getMessage(text, genericMessage.getName()); break; - case EVENT: message = this.eventFactory.getMessage(text, genericMessage.getName()); break; - default: - break; - } - // @formatter:on + Message genericMessage = BasicMessageFactory.getMessage(text); + Message message = null; - return message; - } + // @formatter:off + switch (genericMessage.getType()) { + case REQUEST: + message = this.requestFactory.getMessage(text, genericMessage.getName()); + break; + case RESPONSE: + message = this.responseFactory.getMessage(text, genericMessage.getName()); + break; + case EVENT: + message = this.eventFactory.getMessage(text, genericMessage.getName()); + break; + default: + break; + } + // @formatter:on - /** - * Set request factory - * - * @param requestFactory - */ - public void setRequestFactory(RequestFactory requestFactory) - { - this.requestFactory = requestFactory; - } + return message; + } - /** - * Set response factory - * - * @param responseFactory - */ - public void setResponseFactory(ResponseFactory responseFactory) - { - this.responseFactory = responseFactory; - } + /** + * Set request factory + * + * @param requestFactory + */ + public void setRequestFactory(RequestFactory requestFactory) { + this.requestFactory = requestFactory; + } - /** - * Set event factory - * - * @param eventFactory - */ - public void setEventFactory(EventFactory eventFactory) - { - this.eventFactory = eventFactory; - } + /** + * Set response factory + * + * @param responseFactory + */ + public void setResponseFactory(ResponseFactory responseFactory) { + this.responseFactory = responseFactory; + } - private void checkSubFactoryReferences() - { - if (this.requestFactory == null || this.responseFactory == null || this.eventFactory == null) - { - throw new IllegalStateException("requestFactory, responseFactory and eventFactory have to be set"); - } - } + /** + * Set event factory + * + * @param eventFactory + */ + public void setEventFactory(EventFactory eventFactory) { + this.eventFactory = eventFactory; + } + private void checkSubFactoryReferences() { + if (this.requestFactory == null || this.responseFactory == null || this.eventFactory == null) { + throw new IllegalStateException( + "requestFactory, responseFactory and eventFactory have to be set"); + } + } } diff --git a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java index 4df5690..974a7c3 100644 --- a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java @@ -9,16 +9,10 @@ import enedis.lab.util.message.Request; -/** - * Request factory - */ -public class RequestFactory extends AbstractMessageFactory -{ - /** - * Default constructor - */ - public RequestFactory() - { - super(Request.class); - } +/** Request factory */ +public class RequestFactory extends AbstractMessageFactory { + /** Default constructor */ + public RequestFactory() { + super(Request.class); + } } diff --git a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java index eda280e..4f7aaac 100644 --- a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java @@ -9,16 +9,10 @@ import enedis.lab.util.message.Response; -/** - * Request factory - */ -public class ResponseFactory extends AbstractMessageFactory -{ - /** - * Default constructor - */ - public ResponseFactory() - { - super(Response.class); - } +/** Request factory */ +public class ResponseFactory extends AbstractMessageFactory { + /** Default constructor */ + public ResponseFactory() { + super(Response.class); + } } diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifier.java b/src/main/java/enedis/lab/util/task/FilteredNotifier.java index bf86a63..9ca7ea5 100644 --- a/src/main/java/enedis/lab/util/task/FilteredNotifier.java +++ b/src/main/java/enedis/lab/util/task/FilteredNotifier.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -13,101 +13,83 @@ /** * Notifier interface with filter * - * @param - * the filter - * @param - * the subscriber + * @param the filter + * @param the subscriber */ -public interface FilteredNotifier extends Notifier -{ - /** - * Add a subscriber with filter - * - * @param filter - * the filter used for subscription - * @param listener - * the subscriber reference - * @throws Exception - * on subscription failure - */ - public void subscribe(F filter, T listener) throws Exception; +public interface FilteredNotifier extends Notifier { + /** + * Add a subscriber with filter + * + * @param filter the filter used for subscription + * @param listener the subscriber reference + * @throws Exception on subscription failure + */ + public void subscribe(F filter, T listener) throws Exception; - /** - * Remove a subscriber with filter - * - * @param filter - * the filter used for subscription - * @param listener - * the subscriber reference - * @throws Exception - * if filter or listener not found - */ - public void unsubscribe(F filter, T listener) throws Exception; + /** + * Remove a subscriber with filter + * + * @param filter the filter used for subscription + * @param listener the subscriber reference + * @throws Exception if filter or listener not found + */ + public void unsubscribe(F filter, T listener) throws Exception; - /** - * Check if the given filter has a given subscriber - * - * @param filter - * the filter used for subscription - * @param listener - * the subscriber reference - * @return true if the given subscriber exists - */ - public boolean hasSubscriber(F filter, T listener); + /** + * Check if the given filter has a given subscriber + * + * @param filter the filter used for subscription + * @param listener the subscriber reference + * @return true if the given subscriber exists + */ + public boolean hasSubscriber(F filter, T listener); - /** - * Get subscribers associated with filter - * - * @param filter - * the filter used for subscription - * @return The subscribers collection - */ - public Collection getSubscribers(F filter); + /** + * Get subscribers associated with filter + * + * @param filter the filter used for subscription + * @return The subscribers collection + */ + public Collection getSubscribers(F filter); - /** - * Get subscribers including global and/or filters - * - * @param includeGlobal - * indicates if includes global subscribers - * @param includeFilter - * indicates if includes filters subscribers - * @return The subscribers collection - */ - public Collection getSubscribers(boolean includeGlobal, boolean includeFilter); + /** + * Get subscribers including global and/or filters + * + * @param includeGlobal indicates if includes global subscribers + * @param includeFilter indicates if includes filters subscribers + * @return The subscribers collection + */ + public Collection getSubscribers(boolean includeGlobal, boolean includeFilter); - /** - * Get subscribers associated with predicate filter - * - * @param predicate - * the predicate used to test with filters used for subscription - * @param includeGlobal - * indicates if includes global subscribers - * @return The subscribers collection - */ - public Collection getSubscribers(Predicate predicate, boolean includeGlobal); + /** + * Get subscribers associated with predicate filter + * + * @param predicate the predicate used to test with filters used for subscription + * @param includeGlobal indicates if includes global subscribers + * @return The subscribers collection + */ + public Collection getSubscribers(Predicate predicate, boolean includeGlobal); - /** - * Check if the given filter has been set - * - * @param filter - * the filter used for subscription - * @return true if the given filter exists - */ - public boolean hasFilter(F filter); + /** + * Check if the given filter has been set + * + * @param filter the filter used for subscription + * @return true if the given filter exists + */ + public boolean hasFilter(F filter); - /** - * Get filters - * - * @return The filters collection - */ - public Collection getFilters(); + /** + * Get filters + * + * @return The filters collection + */ + public Collection getFilters(); - /** - * Get filters associated with listener - * - * @param listener - * the subscriber reference - * @return The filters collection - */ - public Collection getFilters(T listener); + /** + * Get filters associated with listener + * + * @param listener the subscriber reference + * @return The filters collection + */ + public Collection getFilters(T listener); } diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java index 0bda613..6a4d708 100644 --- a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java +++ b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -19,229 +19,188 @@ /** * Filtered notifier with subscribers basic implementation * - * @param - * the filter type - * @param - * the subscriber type + * @param the filter type + * @param the subscriber type */ -public class FilteredNotifierBase implements FilteredNotifier -{ - protected Map> subscribersFiltered; - private Lock subscribersFilteredLock = new ReentrantLock(); - protected Collection subscribersUnfiltered; - private Lock subscribersUnfilteredLock = new ReentrantLock(); - - /** - * Default constructor - */ - public FilteredNotifierBase() - { - super(); - this.subscribersFiltered = new ConcurrentHashMap>(); - this.subscribersUnfiltered = new CopyOnWriteArraySet(); - } - - @Override - public void subscribe(T listener) - { - if (listener == null) - { - return; - } - this.subscribersUnfilteredLock.lock(); - this.subscribersUnfiltered.add(listener); - this.subscribersUnfilteredLock.unlock(); - } - - @Override - public void unsubscribe(T listener) - { - if (listener == null) - { - return; - } - for (F filter : this.getFilters()) - { - this.unsubscribe(filter, listener); - } - this.subscribersUnfilteredLock.lock(); - this.subscribersUnfiltered.remove(listener); - this.subscribersUnfilteredLock.unlock(); - } - - @Override - public boolean hasSubscriber(T listener) - { - for (F filter : this.getFilters()) - { - if (this.hasSubscriber(filter, listener)) - { - return true; - } - } - - this.subscribersUnfilteredLock.lock(); - boolean hasSubscriber = this.subscribersUnfiltered.contains(listener); - this.subscribersUnfilteredLock.unlock(); - - return hasSubscriber; - } - - @Override - public Collection getSubscribers() - { - return this.getSubscribers(true, true); - } - - @Override - public void subscribe(F filter, T listener) - { - if (filter == null || listener == null) - { - return; - } - Collection subscribers; - if (this.hasFilter(filter)) - { - subscribers = this.getSubscribers(filter); - if (!subscribers.contains(listener)) - { - subscribers.add(listener); - } - } - else - { - subscribers = new HashSet(); - subscribers.add(listener); - } - this.subscribersFilteredLock.lock(); - this.subscribersFiltered.put(filter, subscribers); - this.subscribersFilteredLock.unlock(); - } - - @Override - public void unsubscribe(F filter, T listener) - { - if (filter == null || listener == null) - { - return; - } - Collection subscribers = this.getSubscribers(filter); - if (subscribers.contains(listener)) - { - subscribers.remove(listener); - if (subscribers.size() == 0) - { - this.subscribersFilteredLock.lock(); - this.subscribersFiltered.remove(filter); - this.subscribersFilteredLock.unlock(); - } - } - } - - @Override - public boolean hasSubscriber(F filter, T listener) - { - Collection subscribers = this.getSubscribers(filter); - - return subscribers.contains(listener); - } - - @Override - public Collection getSubscribers(F filter) - { - this.subscribersFilteredLock.lock(); - Collection subscribersFiltered = (this.hasFilter(filter)) ? this.subscribersFiltered.get(filter) : new HashSet(); - this.subscribersFilteredLock.unlock(); - - return subscribersFiltered; - } - - @Override - public Collection getSubscribers(boolean includeGlobal, boolean includeFilter) - { - Collection subscribers = new HashSet(); - - if (includeFilter) - { - for (F filter : this.getFilters()) - { - Collection filterSubscribers = this.getSubscribers(filter); - subscribers.addAll(filterSubscribers); - } - } - if (includeGlobal) - { - this.subscribersUnfilteredLock.lock(); - subscribers.addAll(this.subscribersUnfiltered); - this.subscribersUnfilteredLock.unlock(); - } - - return subscribers; - } - - @Override - public Collection getSubscribers(Predicate predicate, boolean includeGlobal) - { - Collection subscribers = new HashSet(); - - if (predicate != null) - { - this.subscribersFilteredLock.lock(); - for (F filter : this.subscribersFiltered.keySet()) - { - if (predicate.test(filter)) - { - subscribers.addAll(this.subscribersFiltered.get(filter)); - } - } - this.subscribersFilteredLock.unlock(); - } - if (includeGlobal) - { - this.subscribersUnfilteredLock.lock(); - subscribers.addAll(this.subscribersUnfiltered); - this.subscribersUnfilteredLock.unlock(); - } - - return subscribers; - } - - @Override - public boolean hasFilter(F filter) - { - this.subscribersFilteredLock.lock(); - boolean hasFilter = (filter != null) ? this.subscribersFiltered.containsKey(filter) : false; - this.subscribersFilteredLock.unlock(); - - return hasFilter; - } - - @Override - public Collection getFilters() - { - this.subscribersFilteredLock.lock(); - Collection subscribersFiltered = this.subscribersFiltered.keySet(); - this.subscribersFilteredLock.unlock(); - - return subscribersFiltered; - } - - @Override - public Collection getFilters(T listener) - { - Collection filters = new HashSet(); - - for (F filter : this.getFilters()) - { - Collection subscribers = this.getSubscribers(filter); - if (subscribers.contains(listener)) - { - filters.add(filter); - } - } - - return filters; - } - -} \ No newline at end of file +public class FilteredNotifierBase implements FilteredNotifier { + protected Map> subscribersFiltered; + private Lock subscribersFilteredLock = new ReentrantLock(); + protected Collection subscribersUnfiltered; + private Lock subscribersUnfilteredLock = new ReentrantLock(); + + /** Default constructor */ + public FilteredNotifierBase() { + super(); + this.subscribersFiltered = new ConcurrentHashMap>(); + this.subscribersUnfiltered = new CopyOnWriteArraySet(); + } + + @Override + public void subscribe(T listener) { + if (listener == null) { + return; + } + this.subscribersUnfilteredLock.lock(); + this.subscribersUnfiltered.add(listener); + this.subscribersUnfilteredLock.unlock(); + } + + @Override + public void unsubscribe(T listener) { + if (listener == null) { + return; + } + for (F filter : this.getFilters()) { + this.unsubscribe(filter, listener); + } + this.subscribersUnfilteredLock.lock(); + this.subscribersUnfiltered.remove(listener); + this.subscribersUnfilteredLock.unlock(); + } + + @Override + public boolean hasSubscriber(T listener) { + for (F filter : this.getFilters()) { + if (this.hasSubscriber(filter, listener)) { + return true; + } + } + + this.subscribersUnfilteredLock.lock(); + boolean hasSubscriber = this.subscribersUnfiltered.contains(listener); + this.subscribersUnfilteredLock.unlock(); + + return hasSubscriber; + } + + @Override + public Collection getSubscribers() { + return this.getSubscribers(true, true); + } + + @Override + public void subscribe(F filter, T listener) { + if (filter == null || listener == null) { + return; + } + Collection subscribers; + if (this.hasFilter(filter)) { + subscribers = this.getSubscribers(filter); + if (!subscribers.contains(listener)) { + subscribers.add(listener); + } + } else { + subscribers = new HashSet(); + subscribers.add(listener); + } + this.subscribersFilteredLock.lock(); + this.subscribersFiltered.put(filter, subscribers); + this.subscribersFilteredLock.unlock(); + } + + @Override + public void unsubscribe(F filter, T listener) { + if (filter == null || listener == null) { + return; + } + Collection subscribers = this.getSubscribers(filter); + if (subscribers.contains(listener)) { + subscribers.remove(listener); + if (subscribers.size() == 0) { + this.subscribersFilteredLock.lock(); + this.subscribersFiltered.remove(filter); + this.subscribersFilteredLock.unlock(); + } + } + } + + @Override + public boolean hasSubscriber(F filter, T listener) { + Collection subscribers = this.getSubscribers(filter); + + return subscribers.contains(listener); + } + + @Override + public Collection getSubscribers(F filter) { + this.subscribersFilteredLock.lock(); + Collection subscribersFiltered = + (this.hasFilter(filter)) ? this.subscribersFiltered.get(filter) : new HashSet(); + this.subscribersFilteredLock.unlock(); + + return subscribersFiltered; + } + + @Override + public Collection getSubscribers(boolean includeGlobal, boolean includeFilter) { + Collection subscribers = new HashSet(); + + if (includeFilter) { + for (F filter : this.getFilters()) { + Collection filterSubscribers = this.getSubscribers(filter); + subscribers.addAll(filterSubscribers); + } + } + if (includeGlobal) { + this.subscribersUnfilteredLock.lock(); + subscribers.addAll(this.subscribersUnfiltered); + this.subscribersUnfilteredLock.unlock(); + } + + return subscribers; + } + + @Override + public Collection getSubscribers(Predicate predicate, boolean includeGlobal) { + Collection subscribers = new HashSet(); + + if (predicate != null) { + this.subscribersFilteredLock.lock(); + for (F filter : this.subscribersFiltered.keySet()) { + if (predicate.test(filter)) { + subscribers.addAll(this.subscribersFiltered.get(filter)); + } + } + this.subscribersFilteredLock.unlock(); + } + if (includeGlobal) { + this.subscribersUnfilteredLock.lock(); + subscribers.addAll(this.subscribersUnfiltered); + this.subscribersUnfilteredLock.unlock(); + } + + return subscribers; + } + + @Override + public boolean hasFilter(F filter) { + this.subscribersFilteredLock.lock(); + boolean hasFilter = (filter != null) ? this.subscribersFiltered.containsKey(filter) : false; + this.subscribersFilteredLock.unlock(); + + return hasFilter; + } + + @Override + public Collection getFilters() { + this.subscribersFilteredLock.lock(); + Collection subscribersFiltered = this.subscribersFiltered.keySet(); + this.subscribersFilteredLock.unlock(); + + return subscribersFiltered; + } + + @Override + public Collection getFilters(T listener) { + Collection filters = new HashSet(); + + for (F filter : this.getFilters()) { + Collection subscribers = this.getSubscribers(filter); + if (subscribers.contains(listener)) { + filters.add(filter); + } + } + + return filters; + } +} diff --git a/src/main/java/enedis/lab/util/task/Notifier.java b/src/main/java/enedis/lab/util/task/Notifier.java index e8ec73e..f764b14 100644 --- a/src/main/java/enedis/lab/util/task/Notifier.java +++ b/src/main/java/enedis/lab/util/task/Notifier.java @@ -14,34 +14,33 @@ * * @param */ -public interface Notifier -{ - /** - * Add a subscriber - * - * @param subscriber - */ - public void subscribe(T subscriber); +public interface Notifier { + /** + * Add a subscriber + * + * @param subscriber + */ + public void subscribe(T subscriber); - /** - * Remove a subscriber - * - * @param subscriber - */ - public void unsubscribe(T subscriber); + /** + * Remove a subscriber + * + * @param subscriber + */ + public void unsubscribe(T subscriber); - /** - * Check if the given subscriber has been added - * - * @param subscriber - * @return true if the given subscriber has been added - */ - public boolean hasSubscriber(T subscriber); + /** + * Check if the given subscriber has been added + * + * @param subscriber + * @return true if the given subscriber has been added + */ + public boolean hasSubscriber(T subscriber); - /** - * Get subscribers - * - * @return The subscribers collection - */ - public Collection getSubscribers(); + /** + * Get subscribers + * + * @return The subscribers collection + */ + public Collection getSubscribers(); } diff --git a/src/main/java/enedis/lab/util/task/NotifierBase.java b/src/main/java/enedis/lab/util/task/NotifierBase.java index a1e95ae..6515f7e 100644 --- a/src/main/java/enedis/lab/util/task/NotifierBase.java +++ b/src/main/java/enedis/lab/util/task/NotifierBase.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -14,50 +14,38 @@ /** * Notifier with subscribers basic implementation * - * @param - * the subscriber type + * @param the subscriber type */ -public class NotifierBase implements Notifier -{ - protected Set subscribers; - - /** - * Default constructor - */ - public NotifierBase() - { - super(); - this.subscribers = new CopyOnWriteArraySet(); - } - - @Override - public void subscribe(T listener) - { - if (listener != null && !this.subscribers.contains(listener)) - { - this.subscribers.add(listener); - } - } - - @Override - public void unsubscribe(T listener) - { - if (listener != null && this.subscribers.contains(listener)) - { - this.subscribers.remove(listener); - } - } - - @Override - public boolean hasSubscriber(T listener) - { - return this.subscribers.contains(listener); - } - - @Override - public Collection getSubscribers() - { - return this.subscribers; - } - -} \ No newline at end of file +public class NotifierBase implements Notifier { + protected Set subscribers; + + /** Default constructor */ + public NotifierBase() { + super(); + this.subscribers = new CopyOnWriteArraySet(); + } + + @Override + public void subscribe(T listener) { + if (listener != null && !this.subscribers.contains(listener)) { + this.subscribers.add(listener); + } + } + + @Override + public void unsubscribe(T listener) { + if (listener != null && this.subscribers.contains(listener)) { + this.subscribers.remove(listener); + } + } + + @Override + public boolean hasSubscriber(T listener) { + return this.subscribers.contains(listener); + } + + @Override + public Collection getSubscribers() { + return this.subscribers; + } +} diff --git a/src/main/java/enedis/lab/util/task/Subscriber.java b/src/main/java/enedis/lab/util/task/Subscriber.java index 253daec..9e1dd91 100644 --- a/src/main/java/enedis/lab/util/task/Subscriber.java +++ b/src/main/java/enedis/lab/util/task/Subscriber.java @@ -7,10 +7,5 @@ package enedis.lab.util.task; -/** - * Subscriber interface - */ -public interface Subscriber -{ - -} +/** Subscriber interface */ +public interface Subscriber {} diff --git a/src/main/java/enedis/lab/util/task/Task.java b/src/main/java/enedis/lab/util/task/Task.java index d3ead6f..85cd5b8 100644 --- a/src/main/java/enedis/lab/util/task/Task.java +++ b/src/main/java/enedis/lab/util/task/Task.java @@ -7,25 +7,18 @@ package enedis.lab.util.task; -/** - * Task interface - */ -public interface Task -{ - /** - * Start task - */ - public void start(); +/** Task interface */ +public interface Task { + /** Start task */ + public void start(); - /** - * Stop task - */ - public void stop(); + /** Stop task */ + public void stop(); - /** - * Get the task running state - * - * @return true if the task running - */ - public boolean isRunning(); + /** + * Get the task running state + * + * @return true if the task running + */ + public boolean isRunning(); } diff --git a/src/main/java/enedis/lab/util/task/TaskBase.java b/src/main/java/enedis/lab/util/task/TaskBase.java index 4a96367..fb8428f 100644 --- a/src/main/java/enedis/lab/util/task/TaskBase.java +++ b/src/main/java/enedis/lab/util/task/TaskBase.java @@ -8,144 +8,101 @@ package enedis.lab.util.task; import java.util.concurrent.atomic.AtomicBoolean; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -/** - * Task basic implementation - */ -public abstract class TaskBase implements Task, Runnable -{ - private AtomicBoolean stopRequired; - private Thread task; - protected static Logger logger = LogManager.getLogger(); - - /** - * Default constructor - */ - public TaskBase() - { - super(); - this.stopRequired = new AtomicBoolean(); - } - - @Override - public void start() - { - if (this.task == null || !this.task.isAlive()) - { - this.stopRequired.set(false); - this.task = new Thread(this); - this.task.start(); - } - } - - @Override - public void stop() - { - try - { - if (this.task != null && this.task.isAlive()) - { - this.stopRequired.set(true); - if (this.task != Thread.currentThread()) - { - this.task.join(); - } - } - } - catch (InterruptedException exception) - { - logger.error("Stop task interrupted", exception); - } - } - - @Override - public final boolean isRunning() - { - return this.task == null ? false : this.task.isAlive(); - } - - @Override - public void run() - { - this.runOnStart(); - this.runProcess(); - this.runOnTerminate(); - } - - protected final boolean isStopRequired() - { - return this.stopRequired.get(); - } - - protected final void runOnStart() - { - try - { - this.onStart(); - } - catch (Exception exception) - { - logger.error("Task on start aborted", exception); - this.runOnError(exception); - } - } - - protected final void runProcess() - { - try - { - this.process(); - } - catch (Exception exception) - { - logger.error("Task process aborted", exception); - this.runOnError(exception); - } - } - - protected final void runOnTerminate() - { - try - { - this.onTerminate(); - } - catch (Exception exception) - { - logger.error("Task on terminate aborted", exception); - this.runOnError(exception); - } - } - - protected final void runOnError(Exception exception) - { - try - { - this.onError(exception); - } - catch (Exception onErrorException) - { - logger.error("Task on error aborted", exception); - } - } - - /** - * Task core process method - */ - protected abstract void process(); - - protected void onStart() - { - } - - protected void onTerminate() - { - } - - protected void onError(Exception exception) - { - } - +/** Task basic implementation */ +public abstract class TaskBase implements Task, Runnable { + private AtomicBoolean stopRequired; + private Thread task; + protected static Logger logger = LogManager.getLogger(); + + /** Default constructor */ + public TaskBase() { + super(); + this.stopRequired = new AtomicBoolean(); + } + + @Override + public void start() { + if (this.task == null || !this.task.isAlive()) { + this.stopRequired.set(false); + this.task = new Thread(this); + this.task.start(); + } + } + + @Override + public void stop() { + try { + if (this.task != null && this.task.isAlive()) { + this.stopRequired.set(true); + if (this.task != Thread.currentThread()) { + this.task.join(); + } + } + } catch (InterruptedException exception) { + logger.error("Stop task interrupted", exception); + } + } + + @Override + public final boolean isRunning() { + return this.task == null ? false : this.task.isAlive(); + } + + @Override + public void run() { + this.runOnStart(); + this.runProcess(); + this.runOnTerminate(); + } + + protected final boolean isStopRequired() { + return this.stopRequired.get(); + } + + protected final void runOnStart() { + try { + this.onStart(); + } catch (Exception exception) { + logger.error("Task on start aborted", exception); + this.runOnError(exception); + } + } + + protected final void runProcess() { + try { + this.process(); + } catch (Exception exception) { + logger.error("Task process aborted", exception); + this.runOnError(exception); + } + } + + protected final void runOnTerminate() { + try { + this.onTerminate(); + } catch (Exception exception) { + logger.error("Task on terminate aborted", exception); + this.runOnError(exception); + } + } + + protected final void runOnError(Exception exception) { + try { + this.onError(exception); + } catch (Exception onErrorException) { + logger.error("Task on error aborted", exception); + } + } + + /** Task core process method */ + protected abstract void process(); + + protected void onStart() {} + + protected void onTerminate() {} + + protected void onError(Exception exception) {} } diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodic.java b/src/main/java/enedis/lab/util/task/TaskPeriodic.java index e404755..a9ba56f 100644 --- a/src/main/java/enedis/lab/util/task/TaskPeriodic.java +++ b/src/main/java/enedis/lab/util/task/TaskPeriodic.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,90 +7,72 @@ package enedis.lab.util.task; -import java.util.concurrent.atomic.AtomicLong; - import enedis.lab.util.time.Time; +import java.util.concurrent.atomic.AtomicLong; -/** - * Periodic task with configurable period - */ -public abstract class TaskPeriodic extends TaskBase -{ - /** - * Default period (in milliseconds) used to execute process - */ - public static final long DEFAULT_PERIOD = 1000; +/** Periodic task with configurable period */ +public abstract class TaskPeriodic extends TaskBase { + /** Default period (in milliseconds) used to execute process */ + public static final long DEFAULT_PERIOD = 1000; - private AtomicLong period = new AtomicLong(); + private AtomicLong period = new AtomicLong(); - /** - * Constructor with default parameter - * - * @see #DEFAULT_PERIOD - */ - public TaskPeriodic() - { - super(); - this.setPeriod(DEFAULT_PERIOD); - } + /** + * Constructor with default parameter + * + * @see #DEFAULT_PERIOD + */ + public TaskPeriodic() { + super(); + this.setPeriod(DEFAULT_PERIOD); + } - /** - * Constructor with polling period - * - * @param period - * the period (in milliseconds) used to execute process - */ - public TaskPeriodic(long period) - { - super(); - this.setPeriod(period); - } + /** + * Constructor with polling period + * + * @param period the period (in milliseconds) used to execute process + */ + public TaskPeriodic(long period) { + super(); + this.setPeriod(period); + } - @Override - public void run() - { - this.runOnStart(); - while (!this.isStopRequired()) - { - this.runProcess(); - if (this.isStopRequired()) - { - break; - } - this.waitPeriod(); - } - this.runOnTerminate(); - } + @Override + public void run() { + this.runOnStart(); + while (!this.isStopRequired()) { + this.runProcess(); + if (this.isStopRequired()) { + break; + } + this.waitPeriod(); + } + this.runOnTerminate(); + } - /** - * Get polling period - * - * @return The period (in milliseconds) used to execute process - */ - public long getPeriod() - { - return this.period.get(); - } + /** + * Get polling period + * + * @return The period (in milliseconds) used to execute process + */ + public long getPeriod() { + return this.period.get(); + } - /** - * Set the polling period - * - * @param period - * the period (in milliseconds) used to execute process - * @throws IllegalArgumentException - * if period <= 0 - */ - public void setPeriod(long period) - { - if (period <= 0) - { - throw new IllegalArgumentException("Cannot set period " + period + " : must be > 0"); - } - this.period.set(period); - } + /** + * Set the polling period + * + * @param period the period (in milliseconds) used to execute process + * @throws IllegalArgumentException if period <= 0 + */ + public void setPeriod(long period) { + if (period <= 0) { + throw new IllegalArgumentException("Cannot set period " + period + " : must be > 0"); + } + this.period.set(period); + } - private void waitPeriod() - { - Time.sleep(this.getPeriod()); - } + private void waitPeriod() { + Time.sleep(this.getPeriod()); + } } diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java index 048c14d..c469612 100644 --- a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java +++ b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -12,56 +12,45 @@ /** * Periodic task with subscribers basic implementation * - * @param - * the subscriber type + * @param the subscriber type */ -public abstract class TaskPeriodicWithSubscribers extends TaskPeriodic implements Notifier -{ - protected Notifier notifier; - - /** - * Default constructor - */ - public TaskPeriodicWithSubscribers() - { - super(); - this.notifier = new NotifierBase(); - } - - /** - * Constructor setting period - * - * @param period - * the period in milliseconds - */ - public TaskPeriodicWithSubscribers(long period) - { - super(period); - this.notifier = new NotifierBase(); - } - - @Override - public void subscribe(T listener) - { - this.notifier.subscribe(listener); - } - - @Override - public void unsubscribe(T listener) - { - this.notifier.unsubscribe(listener); - } - - @Override - public boolean hasSubscriber(T listener) - { - return this.notifier.hasSubscriber(listener); - } - - @Override - public Collection getSubscribers() - { - return this.notifier.getSubscribers(); - } - +public abstract class TaskPeriodicWithSubscribers extends TaskPeriodic + implements Notifier { + protected Notifier notifier; + + /** Default constructor */ + public TaskPeriodicWithSubscribers() { + super(); + this.notifier = new NotifierBase(); + } + + /** + * Constructor setting period + * + * @param period the period in milliseconds + */ + public TaskPeriodicWithSubscribers(long period) { + super(period); + this.notifier = new NotifierBase(); + } + + @Override + public void subscribe(T listener) { + this.notifier.subscribe(listener); + } + + @Override + public void unsubscribe(T listener) { + this.notifier.unsubscribe(listener); + } + + @Override + public boolean hasSubscriber(T listener) { + return this.notifier.hasSubscriber(listener); + } + + @Override + public Collection getSubscribers() { + return this.notifier.getSubscribers(); + } } diff --git a/src/main/java/enedis/lab/util/time/Time.java b/src/main/java/enedis/lab/util/time/Time.java index 134a26a..d3dcb4b 100644 --- a/src/main/java/enedis/lab/util/time/Time.java +++ b/src/main/java/enedis/lab/util/time/Time.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -11,98 +11,86 @@ import java.text.SimpleDateFormat; import java.util.Date; -/** - * Time implementation - */ -public abstract class Time -{ - /** Millisecond */ - public final static int MILLISECOND = 1; - /** Second */ - public final static int SECOND = 1000 * MILLISECOND; - /** Minute */ - public final static int MINUTE = 60 * SECOND; - /** Hour */ - public final static int HOUR = 60 * MINUTE; - /** Day */ - public final static int DAY = 24 * HOUR; +/** Time implementation */ +public abstract class Time { + /** Millisecond */ + public static final int MILLISECOND = 1; + + /** Second */ + public static final int SECOND = 1000 * MILLISECOND; + + /** Minute */ + public static final int MINUTE = 60 * SECOND; + + /** Hour */ + public static final int HOUR = 60 * MINUTE; - /* Default format for displaying the date/time */ - private static final String DFLT_DATETIME_FORMAT = "yyyy/MM/dd HH:mm:ss.SSS"; + /** Day */ + public static final int DAY = 24 * HOUR; - /** - * Wait the given duration in millisecond - * - * @param duration - */ - public static void sleep(long duration) - { - if (duration <= 0L) - { - return; - } - try - { - Thread.sleep(duration); - } - catch (InterruptedException e) - { - e.printStackTrace(); - } - } + /* Default format for displaying the date/time */ + private static final String DFLT_DATETIME_FORMAT = "yyyy/MM/dd HH:mm:ss.SSS"; - /** - * Convert date/time from long number (since an origin) to a string in the default format - * - * @param timestamp - * a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) - * @return the time/date string in the default format - */ - public static String timestampToDateTimeStr(long timestamp) - { - return timestampToDateTimeStr(timestamp, DFLT_DATETIME_FORMAT); - } + /** + * Wait the given duration in millisecond + * + * @param duration + */ + public static void sleep(long duration) { + if (duration <= 0L) { + return; + } + try { + Thread.sleep(duration); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } - /** - * Convert date/time from long number to a string in the specified format - * - * @param timestamp - * a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) - * @param dateTimeFormatStr - * the format specified - * @return the time/date string in the specified format - */ - public static String timestampToDateTimeStr(long timestamp, String dateTimeFormatStr) - { - DateFormat dateFormat = new SimpleDateFormat(dateTimeFormatStr); - Date date = new Date(timestamp); - return dateFormat.format(date); - } + /** + * Convert date/time from long number (since an origin) to a string in the default format + * + * @param timestamp a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) + * @return the time/date string in the default format + */ + public static String timestampToDateTimeStr(long timestamp) { + return timestampToDateTimeStr(timestamp, DFLT_DATETIME_FORMAT); + } - /** - * Timestamp a message with the current DateTime - * - * @param msg - * @return the message decorated with current DateTime - */ - public static String timestamp(String msg) - { - return timestamp(msg, DFLT_DATETIME_FORMAT); - } + /** + * Convert date/time from long number to a string in the specified format + * + * @param timestamp a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) + * @param dateTimeFormatStr the format specified + * @return the time/date string in the specified format + */ + public static String timestampToDateTimeStr(long timestamp, String dateTimeFormatStr) { + DateFormat dateFormat = new SimpleDateFormat(dateTimeFormatStr); + Date date = new Date(timestamp); + return dateFormat.format(date); + } - /** - * Timestamp a message with the current DateTime - * - * @param msg - * @param dateTimeFormat - * @return the message decorated with current DateTime - */ - public static String timestamp(String msg, String dateTimeFormat) - { - DateFormat dateFormat = new SimpleDateFormat(dateTimeFormat); - Date date = new Date(); - String text = dateFormat.format(date) + " : " + msg; - return text; - } + /** + * Timestamp a message with the current DateTime + * + * @param msg + * @return the message decorated with current DateTime + */ + public static String timestamp(String msg) { + return timestamp(msg, DFLT_DATETIME_FORMAT); + } + /** + * Timestamp a message with the current DateTime + * + * @param msg + * @param dateTimeFormat + * @return the message decorated with current DateTime + */ + public static String timestamp(String msg, String dateTimeFormat) { + DateFormat dateFormat = new SimpleDateFormat(dateTimeFormat); + Date date = new Date(); + String text = dateFormat.format(date) + " : " + msg; + return text; + } } diff --git a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java index 1db1314..c60a93e 100644 --- a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java +++ b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java @@ -7,43 +7,35 @@ package enedis.tic.core; -public class ReadNextFrameSubscriber implements TICCoreSubscriber -{ - private TICCoreFrame frame; - private TICCoreError error; - - public ReadNextFrameSubscriber() - { - super(); - this.clear(); - } - - @Override - public void onData(TICCoreFrame frame) - { - this.frame = frame; - } - - @Override - public void onError(TICCoreError error) - { - this.error = error; - } - - public TICCoreFrame getData() - { - return this.frame; - } - - public TICCoreError getError() - { - return this.error; - } - - public void clear() - { - this.frame = null; - this.error = null; - } - +public class ReadNextFrameSubscriber implements TICCoreSubscriber { + private TICCoreFrame frame; + private TICCoreError error; + + public ReadNextFrameSubscriber() { + super(); + this.clear(); + } + + @Override + public void onData(TICCoreFrame frame) { + this.frame = frame; + } + + @Override + public void onError(TICCoreError error) { + this.error = error; + } + + public TICCoreFrame getData() { + return this.frame; + } + + public TICCoreError getError() { + return this.error; + } + + public void clear() { + this.frame = null; + this.error = null; + } } diff --git a/src/main/java/enedis/tic/core/TICCore.java b/src/main/java/enedis/tic/core/TICCore.java index b04d740..477776d 100644 --- a/src/main/java/enedis/tic/core/TICCore.java +++ b/src/main/java/enedis/tic/core/TICCore.java @@ -7,101 +7,84 @@ package enedis.tic.core; -import java.util.List; - import enedis.lab.io.tic.TICPortDescriptor; import enedis.lab.util.task.Task; +import java.util.List; -/** - * TICCore interface - */ -public interface TICCore extends Task -{ - /** - * Get available TICs - * - * @return The collection of TIC identifiers - */ - public List getAvailableTICs(); +/** TICCore interface */ +public interface TICCore extends Task { + /** + * Get available TICs + * + * @return The collection of TIC identifiers + */ + public List getAvailableTICs(); - /** - * Get modems informations - * - * @return The collection of TIC port descriptors - */ - public List getModemsInfo(); + /** + * Get modems informations + * + * @return The collection of TIC port descriptors + */ + public List getModemsInfo(); - /** - * Read next frame - * - * @param identifier - * the TIC identifier - * - * @return Frame read or null if timed out - * @throws TICCoreException - * if identifier not found or if any error occurs - */ - public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException; + /** + * Read next frame + * + * @param identifier the TIC identifier + * @return Frame read or null if timed out + * @throws TICCoreException if identifier not found or if any error occurs + */ + public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException; - /** - * Read next frame - * - * @param identifier - * the TIC identifier - * @param timeout - * the read timeout in milliseconds - * - * @return Frame read or null if timed out - * @throws TICCoreException - * if identifier not found or if any error occurs - */ - public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException; + /** + * Read next frame + * + * @param identifier the TIC identifier + * @param timeout the read timeout in milliseconds + * @return Frame read or null if timed out + * @throws TICCoreException if identifier not found or if any error occurs + */ + public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException; - /** - * Add a subscriber - * - * @param subscriber - */ - public void subscribe(TICCoreSubscriber subscriber); + /** + * Add a subscriber + * + * @param subscriber + */ + public void subscribe(TICCoreSubscriber subscriber); - /** - * Remove a subscriber - * - * @param subscriber - */ - public void unsubscribe(TICCoreSubscriber subscriber); + /** + * Remove a subscriber + * + * @param subscriber + */ + public void unsubscribe(TICCoreSubscriber subscriber); - /** - * Add a subscriber with identifier - * - * @param identifier - * the identifier used for subscription - * @param listener - * the subscriber reference - * @throws Exception - * on subscription failure - */ - public void subscribe(TICIdentifier identifier, TICCoreSubscriber listener) throws TICCoreException; + /** + * Add a subscriber with identifier + * + * @param identifier the identifier used for subscription + * @param listener the subscriber reference + * @throws Exception on subscription failure + */ + public void subscribe(TICIdentifier identifier, TICCoreSubscriber listener) + throws TICCoreException; - /** - * Remove a subscriber with identifier - * - * @param identifier - * the identifier used for subscription - * @param listener - * the subscriber reference - * @throws Exception - * if filter or listener not found - */ - public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber listener) throws TICCoreException; + /** + * Remove a subscriber with identifier + * + * @param identifier the identifier used for subscription + * @param listener the subscriber reference + * @throws Exception if filter or listener not found + */ + public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber listener) + throws TICCoreException; - /** - * Get TICs identifier associated with a subscriber - * - * @param listener - * the subscriber reference - * - * @return The collection of TIC identifiers for the subscriber - */ - public List getIndentifiers(TICCoreSubscriber listener); + /** + * Get TICs identifier associated with a subscriber + * + * @param listener the subscriber reference + * @return The collection of TIC identifiers for the subscriber + */ + public List getIndentifiers(TICCoreSubscriber listener); } diff --git a/src/main/java/enedis/tic/core/TICCoreBase.java b/src/main/java/enedis/tic/core/TICCoreBase.java index 82ff323..2072aed 100644 --- a/src/main/java/enedis/tic/core/TICCoreBase.java +++ b/src/main/java/enedis/tic/core/TICCoreBase.java @@ -7,18 +7,6 @@ package enedis.tic.core; -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.function.Predicate; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import enedis.lab.io.PlugSubscriber; import enedis.lab.io.tic.TICPortDescriptor; import enedis.lab.io.tic.TICPortFinder; @@ -27,479 +15,445 @@ import enedis.lab.protocol.tic.TICMode; import enedis.lab.types.DataArrayList; import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.time.Time; import enedis.lab.util.task.FilteredNotifier; import enedis.lab.util.task.FilteredNotifierBase; import enedis.lab.util.task.Task; import enedis.lab.util.task.TaskBase; +import enedis.lab.util.time.Time; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.function.Predicate; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; -/** - * TICCore implementation - */ -public class TICCoreBase implements TICCore, Task, TICCoreSubscriber, PlugSubscriber -{ - private static final int PLUG_NOTIFIER_POLLING_PERIOD = 100; - private static final int READ_NEXT_FRAME_TIMEOUT = 30000; - private static final int READ_NEXT_FRAME_POLLING_PERIOD = 100; - - private TICPortFinder portFinder; - private TICPortPlugNotifier plugNotifier; - private long plugNotifierPeriod; - private Constructor streamConstructor; - private TICMode streamMode; - private List nativePortNamesOnStart; - private Collection streamList; - private FilteredNotifier eventNotifier; - private static Logger logger = LogManager.getLogger(); - - public TICCoreBase() - { - this(TICMode.AUTO, null); - } - - public TICCoreBase(TICMode streamMode, List nativePortNamesStart) - { - this(TICPortFinderBase.getInstance(), PLUG_NOTIFIER_POLLING_PERIOD, TICCoreStreamBase.class, streamMode, nativePortNamesStart); - } - - public TICCoreBase(TICPortFinder ticPortFinder, long plugNotifierPeriod, Class streamClass, TICMode streamMode, List nativePortNamesOnStart) - { - super(); - this.portFinder = ticPortFinder; - this.plugNotifierPeriod = plugNotifierPeriod; - try - { - this.streamConstructor = streamClass.getConstructor(String.class, String.class, TICMode.class); - } - catch (NoSuchMethodException e) - { - throw new IllegalArgumentException("Class " + streamClass.getName() + " has no valid constructor : " + e.getMessage()); - } - if (streamMode == null) - { - throw new IllegalArgumentException("TICMode should be defined"); - } - this.streamMode = streamMode; - this.nativePortNamesOnStart = nativePortNamesOnStart; - this.streamList = Collections.synchronizedSet(new HashSet()); - this.eventNotifier = new FilteredNotifierBase(); - } - - @Override - public List getAvailableTICs() - { - List identifiers = new DataArrayList(); - - for (TICCoreStream stream : this.streamList) - { - identifiers.add(stream.getIdentifier()); - } - - return identifiers; - } - - @Override - public List getModemsInfo() - { - List descriptors = new DataArrayList(); - - for (TICCoreStream stream : this.streamList) - { - TICPortDescriptor descriptor = this.portFinder.findByPortName(stream.getIdentifier().getPortName()); - descriptors.add(descriptor); - } - - return descriptors; - } - - @Override - public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException - { - return this.readNextFrame(identifier, READ_NEXT_FRAME_TIMEOUT); - } - - @Override - public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException - { - TICCoreFrame frame = null; - TICPortDescriptor descriptor = null; - TICCoreStream stream = this.findStream(identifier); - - if (stream == null) - { - descriptor = this.portFinder.findNative(identifier.getPortName()); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - stream = this.startNewStream(descriptor); - } - ReadNextFrameSubscriber subscriber = new ReadNextFrameSubscriber(); - stream.subscribe(subscriber); - - long begin = System.nanoTime(); - long elapsed = 0; - while (elapsed < timeout) - { - if (subscriber.getError() != null) - { - TICCoreException exception = new TICCoreException(subscriber.getError().getErrorCode().intValue(), subscriber.getError().getErrorMessage()); - logger.error(exception.getMessage(), exception); - stream.unsubscribe(subscriber); - this.closeNativeStream(identifier, stream, subscriber); - throw exception; - } - if (subscriber.getData() != null) - { - frame = subscriber.getData(); - break; - } - Time.sleep(READ_NEXT_FRAME_POLLING_PERIOD); - elapsed = (System.nanoTime() - begin) / 1000000; - } - stream.unsubscribe(subscriber); - this.closeNativeStream(identifier, stream, subscriber); - if (elapsed >= timeout) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), "Stream " + identifier + " data read timeout !"); - logger.error(exception.getMessage(), exception); - throw exception; - } - return frame; - } - - @Override - public void subscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException - { - TICCoreStream stream = this.findStream(identifier); - - if (stream == null) - { - TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - this.startNewStream(descriptor); - } - try - { - this.eventNotifier.subscribe(identifier, subscriber); - } - catch (Exception e) - { - logger.error(e.getMessage(), e); - } - } - - @Override - public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException - { - TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); - if (descriptor != null) - { - if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) - { - Collection subscribers = this.findSubscribers(identifier, false); - if (subscribers.contains(subscriber) && subscribers.size() == 1) - { - this.stopStream(descriptor); - } - } - } - try - { - this.eventNotifier.unsubscribe(identifier, subscriber); - } - catch (Exception e) - { - logger.error(e.getMessage(), e); - } - } - - @Override - public void subscribe(TICCoreSubscriber subscriber) - { - this.eventNotifier.subscribe(subscriber); - } - - @Override - public void unsubscribe(TICCoreSubscriber subscriber) - { - this.eventNotifier.unsubscribe(subscriber); - } - - @Override - public List getIndentifiers(TICCoreSubscriber listener) - { - List identifiers = new ArrayList(); - - if (this.eventNotifier.getSubscribers(true, false).contains(listener)) - { - identifiers.addAll(this.getAvailableTICs()); - } - else if (this.eventNotifier.getSubscribers(false, true).contains(listener)) - { - identifiers.addAll(this.eventNotifier.getFilters(listener)); - } - - return identifiers; - } - - @Override - public void onData(TICCoreFrame frame) - { - logger.trace("TICCore frame:\n" + frame.toString(2)); - Collection subscriberList = this.findSubscribers(frame.getIdentifier(), true); - Task task = new TaskBase() - { - @Override - public void process() - { - TICCoreBase.this.notifyOnData(frame, subscriberList); - } - }; - task.start(); - } - - @Override - public void onError(TICCoreError error) - { - logger.error("TICCore error:\n" + error.toString(2)); - Collection subscriberList = this.findSubscribers(error.getIdentifier(), true); - Task task = new TaskBase() - { - @Override - public void process() - { - TICCoreBase.this.notifyOnError(error, subscriberList); - } - }; - task.start(); - } - - @Override - public void onPlugged(TICPortDescriptor descriptor) - { - logger.info("TICCore modem plugged:\n" + descriptor.toString(2)); - this.startNewStream(descriptor); - } - - @Override - public void onUnplugged(TICPortDescriptor descriptor) - { - logger.info("TICCore modem unplugged:\n" + descriptor.toString(2)); - TICIdentifier identifier = this.stopStream(descriptor); - Task task = new TaskBase() - { - @Override - public void process() - { - TICCoreBase.this.notifyOnUnpluggedAndUnsubscribe(identifier); - } - }; - task.start(); - } - - @Override - public void start() - { - if (!this.isRunning()) - { - logger.info("Starting TICCore"); - logger.debug("Starting TIC port plug notifier"); - this.plugNotifier = new TICPortPlugNotifier(this.plugNotifierPeriod, this.portFinder); - this.plugNotifier.subscribe(this); - this.plugNotifier.start(); - logger.debug("TIC port plug notifier started"); - if (this.nativePortNamesOnStart != null && this.nativePortNamesOnStart.size() > 0) - { - logger.debug("Starting natives TIC port: " + Arrays.toString(this.nativePortNamesOnStart.toArray())); - for (String portName : this.nativePortNamesOnStart) - { - TICPortDescriptor descriptor = this.portFinder.findNative(portName); - logger.debug("Starting native TIC port " + portName + " : " + descriptor); - if (descriptor != null && this.findStream(descriptor) == null) - { - this.startNewStream(descriptor); - } - } - } - } - } - - @Override - public void stop() - { - logger.info("Stopping TICCore"); - logger.debug("Stopping TIC port plug notifier"); - this.plugNotifier.unsubscribe(this); - this.plugNotifier.stop(); - logger.debug("Stopping all streams"); - for (TICCoreStream stream : this.streamList) - { - stream.unsubscribe(this); - stream.stop(); - } - this.streamList.clear(); - logger.debug("Removing all subscribers"); - this.unsubscribe(this.eventNotifier.getSubscribers()); - } - - @Override - public boolean isRunning() - { - return (this.plugNotifier != null) ? this.plugNotifier.isRunning() : false; - } - - private TICCoreStream findStream(TICIdentifier identifier) - { - for (TICCoreStream stream : this.streamList) - { - if (stream.getIdentifier().matches(identifier)) - { - return stream; - } - } - - return null; - } - - private TICCoreStream findStream(TICPortDescriptor descriptor) - { - for (TICCoreStream stream : this.streamList) - { - TICIdentifier identifier = stream.getIdentifier(); - if (descriptor.getPortId() != null && identifier.getPortId() != null) - { - if (descriptor.getPortId().equals(identifier.getPortId())) - { - return stream; - } - } - if (descriptor.getPortName() != null && identifier.getPortName() != null) - { - if (descriptor.getPortName().equals(identifier.getPortName())) - { - return stream; - } - } - } - - return null; - } - - private TICCoreStream startNewStream(TICPortDescriptor descriptor) - { - TICCoreStream stream = null; - logger.debug("TICCore starting new stream : " + descriptor.toString()); - try - { - stream = this.streamConstructor.newInstance(descriptor.getPortId(), descriptor.getPortName(), this.streamMode); - - stream.subscribe(this); - - stream.start(); - this.streamList.add(stream); - logger.debug("TICCore started new stream : " + descriptor.toString()); - } - catch (Exception e) - { - logger.error(e.getMessage(), e); - } - return stream; - } - - private TICIdentifier stopStream(TICPortDescriptor descriptor) - { - TICIdentifier identifier = null; - TICCoreStream stream = this.findStream(descriptor); - logger.debug("TICCore stopping new stream : " + descriptor.toString()); - if (stream != null) - { - identifier = stream.getIdentifier(); - stream.unsubscribe(this); - stream.stop(); - this.streamList.remove(stream); - logger.debug("TICCore stopped new stream : " + descriptor.toString()); - } - - return identifier; - } - - private void closeNativeStream(TICIdentifier identifier, TICCoreStream stream, ReadNextFrameSubscriber subscriber) - { - TICPortDescriptor nativeDescriptor = this.portFinder.findNative(identifier.getPortName()); - if (nativeDescriptor != null) - { - if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) - { - Collection streamSubscribers = stream.getSubscribers(); - Collection ticCoreSubscribers = this.findSubscribers(identifier, false); - if (!streamSubscribers.contains(subscriber) && streamSubscribers.contains(this) && streamSubscribers.size() == 1 && ticCoreSubscribers.size() == 0) - { - this.stopStream(nativeDescriptor); - } - } - } - } - - private Collection findSubscribers(TICIdentifier sourceIdentifier, boolean globalSubscribers) - { - Predicate predicate = new Predicate() - { - @Override - public boolean test(TICIdentifier identifier) - { - return sourceIdentifier.matches(identifier); - } - }; - - return this.eventNotifier.getSubscribers(predicate, globalSubscribers); - } - - private void notifyOnUnpluggedAndUnsubscribe(TICIdentifier identifier) - { - try - { - TICCoreError error = new TICCoreError(identifier, TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), - "TICCore stream " + identifier + " has been unplugged (unsubscribe has been forced)"); - Collection subscriberList = this.findSubscribers(identifier, true); - this.notifyOnError(error, subscriberList); - subscriberList = this.findSubscribers(identifier, false); - this.unsubscribe(subscriberList); - } - catch (DataDictionaryException e) - { - logger.error(e.getMessage(), e); - } - } - - private void notifyOnData(TICCoreFrame frame, Collection subscriberList) - { - for (TICCoreSubscriber subscriber : subscriberList) - { - subscriber.onData(frame); - } - } - - private void notifyOnError(TICCoreError error, Collection subscriberList) - { - for (TICCoreSubscriber subscriber : subscriberList) - { - subscriber.onError(error); - } - } - - private void unsubscribe(Collection subscriberList) - { - for (TICCoreSubscriber subscriber : subscriberList) - { - this.unsubscribe(subscriber); - } - } +/** TICCore implementation */ +public class TICCoreBase + implements TICCore, Task, TICCoreSubscriber, PlugSubscriber { + private static final int PLUG_NOTIFIER_POLLING_PERIOD = 100; + private static final int READ_NEXT_FRAME_TIMEOUT = 30000; + private static final int READ_NEXT_FRAME_POLLING_PERIOD = 100; + + private TICPortFinder portFinder; + private TICPortPlugNotifier plugNotifier; + private long plugNotifierPeriod; + private Constructor streamConstructor; + private TICMode streamMode; + private List nativePortNamesOnStart; + private Collection streamList; + private FilteredNotifier eventNotifier; + private static Logger logger = LogManager.getLogger(); + + public TICCoreBase() { + this(TICMode.AUTO, null); + } + + public TICCoreBase(TICMode streamMode, List nativePortNamesStart) { + this( + TICPortFinderBase.getInstance(), + PLUG_NOTIFIER_POLLING_PERIOD, + TICCoreStreamBase.class, + streamMode, + nativePortNamesStart); + } + + public TICCoreBase( + TICPortFinder ticPortFinder, + long plugNotifierPeriod, + Class streamClass, + TICMode streamMode, + List nativePortNamesOnStart) { + super(); + this.portFinder = ticPortFinder; + this.plugNotifierPeriod = plugNotifierPeriod; + try { + this.streamConstructor = + streamClass.getConstructor(String.class, String.class, TICMode.class); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException( + "Class " + streamClass.getName() + " has no valid constructor : " + e.getMessage()); + } + if (streamMode == null) { + throw new IllegalArgumentException("TICMode should be defined"); + } + this.streamMode = streamMode; + this.nativePortNamesOnStart = nativePortNamesOnStart; + this.streamList = Collections.synchronizedSet(new HashSet()); + this.eventNotifier = new FilteredNotifierBase(); + } + + @Override + public List getAvailableTICs() { + List identifiers = new DataArrayList(); + + for (TICCoreStream stream : this.streamList) { + identifiers.add(stream.getIdentifier()); + } + + return identifiers; + } + + @Override + public List getModemsInfo() { + List descriptors = new DataArrayList(); + + for (TICCoreStream stream : this.streamList) { + TICPortDescriptor descriptor = + this.portFinder.findByPortName(stream.getIdentifier().getPortName()); + descriptors.add(descriptor); + } + + return descriptors; + } + + @Override + public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException { + return this.readNextFrame(identifier, READ_NEXT_FRAME_TIMEOUT); + } + + @Override + public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException { + TICCoreFrame frame = null; + TICPortDescriptor descriptor = null; + TICCoreStream stream = this.findStream(identifier); + + if (stream == null) { + descriptor = this.portFinder.findNative(identifier.getPortName()); + if (descriptor == null) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), + "Stream " + identifier + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + stream = this.startNewStream(descriptor); + } + ReadNextFrameSubscriber subscriber = new ReadNextFrameSubscriber(); + stream.subscribe(subscriber); + + long begin = System.nanoTime(); + long elapsed = 0; + while (elapsed < timeout) { + if (subscriber.getError() != null) { + TICCoreException exception = + new TICCoreException( + subscriber.getError().getErrorCode().intValue(), + subscriber.getError().getErrorMessage()); + logger.error(exception.getMessage(), exception); + stream.unsubscribe(subscriber); + this.closeNativeStream(identifier, stream, subscriber); + throw exception; + } + if (subscriber.getData() != null) { + frame = subscriber.getData(); + break; + } + Time.sleep(READ_NEXT_FRAME_POLLING_PERIOD); + elapsed = (System.nanoTime() - begin) / 1000000; + } + stream.unsubscribe(subscriber); + this.closeNativeStream(identifier, stream, subscriber); + if (elapsed >= timeout) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), + "Stream " + identifier + " data read timeout !"); + logger.error(exception.getMessage(), exception); + throw exception; + } + return frame; + } + + @Override + public void subscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) + throws TICCoreException { + TICCoreStream stream = this.findStream(identifier); + + if (stream == null) { + TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); + if (descriptor == null) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), + "Stream " + identifier + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + this.startNewStream(descriptor); + } + try { + this.eventNotifier.subscribe(identifier, subscriber); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + + @Override + public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) + throws TICCoreException { + TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); + if (descriptor != null) { + if (this.nativePortNamesOnStart == null + || !this.nativePortNamesOnStart.contains(identifier.getPortName())) { + Collection subscribers = this.findSubscribers(identifier, false); + if (subscribers.contains(subscriber) && subscribers.size() == 1) { + this.stopStream(descriptor); + } + } + } + try { + this.eventNotifier.unsubscribe(identifier, subscriber); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + + @Override + public void subscribe(TICCoreSubscriber subscriber) { + this.eventNotifier.subscribe(subscriber); + } + + @Override + public void unsubscribe(TICCoreSubscriber subscriber) { + this.eventNotifier.unsubscribe(subscriber); + } + + @Override + public List getIndentifiers(TICCoreSubscriber listener) { + List identifiers = new ArrayList(); + + if (this.eventNotifier.getSubscribers(true, false).contains(listener)) { + identifiers.addAll(this.getAvailableTICs()); + } else if (this.eventNotifier.getSubscribers(false, true).contains(listener)) { + identifiers.addAll(this.eventNotifier.getFilters(listener)); + } + + return identifiers; + } + + @Override + public void onData(TICCoreFrame frame) { + logger.trace("TICCore frame:\n" + frame.toString(2)); + Collection subscriberList = + this.findSubscribers(frame.getIdentifier(), true); + Task task = + new TaskBase() { + @Override + public void process() { + TICCoreBase.this.notifyOnData(frame, subscriberList); + } + }; + task.start(); + } + + @Override + public void onError(TICCoreError error) { + logger.error("TICCore error:\n" + error.toString(2)); + Collection subscriberList = + this.findSubscribers(error.getIdentifier(), true); + Task task = + new TaskBase() { + @Override + public void process() { + TICCoreBase.this.notifyOnError(error, subscriberList); + } + }; + task.start(); + } + + @Override + public void onPlugged(TICPortDescriptor descriptor) { + logger.info("TICCore modem plugged:\n" + descriptor.toString(2)); + this.startNewStream(descriptor); + } + + @Override + public void onUnplugged(TICPortDescriptor descriptor) { + logger.info("TICCore modem unplugged:\n" + descriptor.toString(2)); + TICIdentifier identifier = this.stopStream(descriptor); + Task task = + new TaskBase() { + @Override + public void process() { + TICCoreBase.this.notifyOnUnpluggedAndUnsubscribe(identifier); + } + }; + task.start(); + } + + @Override + public void start() { + if (!this.isRunning()) { + logger.info("Starting TICCore"); + logger.debug("Starting TIC port plug notifier"); + this.plugNotifier = new TICPortPlugNotifier(this.plugNotifierPeriod, this.portFinder); + this.plugNotifier.subscribe(this); + this.plugNotifier.start(); + logger.debug("TIC port plug notifier started"); + if (this.nativePortNamesOnStart != null && this.nativePortNamesOnStart.size() > 0) { + logger.debug( + "Starting natives TIC port: " + Arrays.toString(this.nativePortNamesOnStart.toArray())); + for (String portName : this.nativePortNamesOnStart) { + TICPortDescriptor descriptor = this.portFinder.findNative(portName); + logger.debug("Starting native TIC port " + portName + " : " + descriptor); + if (descriptor != null && this.findStream(descriptor) == null) { + this.startNewStream(descriptor); + } + } + } + } + } + + @Override + public void stop() { + logger.info("Stopping TICCore"); + logger.debug("Stopping TIC port plug notifier"); + this.plugNotifier.unsubscribe(this); + this.plugNotifier.stop(); + logger.debug("Stopping all streams"); + for (TICCoreStream stream : this.streamList) { + stream.unsubscribe(this); + stream.stop(); + } + this.streamList.clear(); + logger.debug("Removing all subscribers"); + this.unsubscribe(this.eventNotifier.getSubscribers()); + } + + @Override + public boolean isRunning() { + return (this.plugNotifier != null) ? this.plugNotifier.isRunning() : false; + } + + private TICCoreStream findStream(TICIdentifier identifier) { + for (TICCoreStream stream : this.streamList) { + if (stream.getIdentifier().matches(identifier)) { + return stream; + } + } + + return null; + } + + private TICCoreStream findStream(TICPortDescriptor descriptor) { + for (TICCoreStream stream : this.streamList) { + TICIdentifier identifier = stream.getIdentifier(); + if (descriptor.getPortId() != null && identifier.getPortId() != null) { + if (descriptor.getPortId().equals(identifier.getPortId())) { + return stream; + } + } + if (descriptor.getPortName() != null && identifier.getPortName() != null) { + if (descriptor.getPortName().equals(identifier.getPortName())) { + return stream; + } + } + } + + return null; + } + + private TICCoreStream startNewStream(TICPortDescriptor descriptor) { + TICCoreStream stream = null; + logger.debug("TICCore starting new stream : " + descriptor.toString()); + try { + stream = + this.streamConstructor.newInstance( + descriptor.getPortId(), descriptor.getPortName(), this.streamMode); + + stream.subscribe(this); + + stream.start(); + this.streamList.add(stream); + logger.debug("TICCore started new stream : " + descriptor.toString()); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return stream; + } + + private TICIdentifier stopStream(TICPortDescriptor descriptor) { + TICIdentifier identifier = null; + TICCoreStream stream = this.findStream(descriptor); + logger.debug("TICCore stopping new stream : " + descriptor.toString()); + if (stream != null) { + identifier = stream.getIdentifier(); + stream.unsubscribe(this); + stream.stop(); + this.streamList.remove(stream); + logger.debug("TICCore stopped new stream : " + descriptor.toString()); + } + + return identifier; + } + + private void closeNativeStream( + TICIdentifier identifier, TICCoreStream stream, ReadNextFrameSubscriber subscriber) { + TICPortDescriptor nativeDescriptor = this.portFinder.findNative(identifier.getPortName()); + if (nativeDescriptor != null) { + if (this.nativePortNamesOnStart == null + || !this.nativePortNamesOnStart.contains(identifier.getPortName())) { + Collection streamSubscribers = stream.getSubscribers(); + Collection ticCoreSubscribers = this.findSubscribers(identifier, false); + if (!streamSubscribers.contains(subscriber) + && streamSubscribers.contains(this) + && streamSubscribers.size() == 1 + && ticCoreSubscribers.size() == 0) { + this.stopStream(nativeDescriptor); + } + } + } + } + + private Collection findSubscribers( + TICIdentifier sourceIdentifier, boolean globalSubscribers) { + Predicate predicate = + new Predicate() { + @Override + public boolean test(TICIdentifier identifier) { + return sourceIdentifier.matches(identifier); + } + }; + + return this.eventNotifier.getSubscribers(predicate, globalSubscribers); + } + + private void notifyOnUnpluggedAndUnsubscribe(TICIdentifier identifier) { + try { + TICCoreError error = + new TICCoreError( + identifier, + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + "TICCore stream " + identifier + " has been unplugged (unsubscribe has been forced)"); + Collection subscriberList = this.findSubscribers(identifier, true); + this.notifyOnError(error, subscriberList); + subscriberList = this.findSubscribers(identifier, false); + this.unsubscribe(subscriberList); + } catch (DataDictionaryException e) { + logger.error(e.getMessage(), e); + } + } + + private void notifyOnData(TICCoreFrame frame, Collection subscriberList) { + for (TICCoreSubscriber subscriber : subscriberList) { + subscriber.onData(frame); + } + } + + private void notifyOnError(TICCoreError error, Collection subscriberList) { + for (TICCoreSubscriber subscriber : subscriberList) { + subscriber.onError(error); + } + } + + private void unsubscribe(Collection subscriberList) { + for (TICCoreSubscriber subscriber : subscriberList) { + this.unsubscribe(subscriber); + } + } } diff --git a/src/main/java/enedis/tic/core/TICCoreError.java b/src/main/java/enedis/tic/core/TICCoreError.java index 2aea957..2fa452b 100644 --- a/src/main/java/enedis/tic/core/TICCoreError.java +++ b/src/main/java/enedis/tic/core/TICCoreError.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,10 +7,6 @@ package enedis.tic.core; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; @@ -18,216 +14,202 @@ import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; import enedis.lab.types.datadictionary.KeyDescriptorNumber; import enedis.lab.types.datadictionary.KeyDescriptorString; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * TICCoreError class * - * Generated + *

    Generated */ -public class TICCoreError extends DataDictionaryBase -{ - protected static final String KEY_IDENTIFIER = "identifier"; - protected static final String KEY_ERROR_CODE = "errorCode"; - protected static final String KEY_ERROR_MESSAGE = "errorMessage"; - protected static final String KEY_DATA = "data"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kIdentifier; - protected KeyDescriptorNumber kErrorCode; - protected KeyDescriptorString kErrorMessage; - protected KeyDescriptorDataDictionary kData; - - protected TICCoreError() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICCoreError(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICCoreError(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param identifier - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public TICCoreError(TICIdentifier identifier, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(identifier, errorCode, errorMessage, null); - } - - /** - * Constructor setting parameters to specific values - * - * @param identifier - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public TICCoreError(TICIdentifier identifier, Number errorCode, String errorMessage, DataDictionary data) throws DataDictionaryException - { - this(); - - this.setIdentifier(identifier); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /** - * Get identifier - * - * @return the identifier - */ - public TICIdentifier getIdentifier() - { - return (TICIdentifier) this.data.get(KEY_IDENTIFIER); - } - - /** - * Get error code - * - * @return the error code - */ - public Number getErrorCode() - { - return (Number) this.data.get(KEY_ERROR_CODE); - } - - /** - * Get error message - * - * @return the error message - */ - public String getErrorMessage() - { - return (String) this.data.get(KEY_ERROR_MESSAGE); - } - - /** - * Get data - * - * @return the data - */ - public DataDictionaryBase getData() - { - return (DataDictionaryBase) this.data.get(KEY_DATA); - } - - /** - * Set identifier - * - * @param identifier - * @throws DataDictionaryException - */ - public void setIdentifier(TICIdentifier identifier) throws DataDictionaryException - { - this.setIdentifier((Object) identifier); - } - - /** - * Set error code - * - * @param errorCode - * @throws DataDictionaryException - */ - public void setErrorCode(Number errorCode) throws DataDictionaryException - { - this.setErrorCode((Object) errorCode); - } - - /** - * Set error message - * - * @param errorMessage - * @throws DataDictionaryException - */ - public void setErrorMessage(String errorMessage) throws DataDictionaryException - { - this.setErrorMessage((Object) errorMessage); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(DataDictionary data) throws DataDictionaryException - { - this.setData((Object) data); - } - - protected void setIdentifier(Object identifier) throws DataDictionaryException - { - this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); - } - - protected void setErrorCode(Object errorCode) throws DataDictionaryException - { - this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); - } - - protected void setErrorMessage(Object errorMessage) throws DataDictionaryException - { - this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() - { - try - { - this.kIdentifier = new KeyDescriptorDataDictionary(KEY_IDENTIFIER, true, TICIdentifier.class); - this.keys.add(this.kIdentifier); - - this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); - this.keys.add(this.kErrorCode); - - this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, true, false); - this.keys.add(this.kErrorMessage); - - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, DataDictionaryBase.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class TICCoreError extends DataDictionaryBase { + protected static final String KEY_IDENTIFIER = "identifier"; + protected static final String KEY_ERROR_CODE = "errorCode"; + protected static final String KEY_ERROR_MESSAGE = "errorMessage"; + protected static final String KEY_DATA = "data"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kIdentifier; + protected KeyDescriptorNumber kErrorCode; + protected KeyDescriptorString kErrorMessage; + protected KeyDescriptorDataDictionary kData; + + protected TICCoreError() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICCoreError(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICCoreError(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param identifier + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public TICCoreError(TICIdentifier identifier, Number errorCode, String errorMessage) + throws DataDictionaryException { + this(identifier, errorCode, errorMessage, null); + } + + /** + * Constructor setting parameters to specific values + * + * @param identifier + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public TICCoreError( + TICIdentifier identifier, Number errorCode, String errorMessage, DataDictionary data) + throws DataDictionaryException { + this(); + + this.setIdentifier(identifier); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + /** + * Get identifier + * + * @return the identifier + */ + public TICIdentifier getIdentifier() { + return (TICIdentifier) this.data.get(KEY_IDENTIFIER); + } + + /** + * Get error code + * + * @return the error code + */ + public Number getErrorCode() { + return (Number) this.data.get(KEY_ERROR_CODE); + } + + /** + * Get error message + * + * @return the error message + */ + public String getErrorMessage() { + return (String) this.data.get(KEY_ERROR_MESSAGE); + } + + /** + * Get data + * + * @return the data + */ + public DataDictionaryBase getData() { + return (DataDictionaryBase) this.data.get(KEY_DATA); + } + + /** + * Set identifier + * + * @param identifier + * @throws DataDictionaryException + */ + public void setIdentifier(TICIdentifier identifier) throws DataDictionaryException { + this.setIdentifier((Object) identifier); + } + + /** + * Set error code + * + * @param errorCode + * @throws DataDictionaryException + */ + public void setErrorCode(Number errorCode) throws DataDictionaryException { + this.setErrorCode((Object) errorCode); + } + + /** + * Set error message + * + * @param errorMessage + * @throws DataDictionaryException + */ + public void setErrorMessage(String errorMessage) throws DataDictionaryException { + this.setErrorMessage((Object) errorMessage); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(DataDictionary data) throws DataDictionaryException { + this.setData((Object) data); + } + + protected void setIdentifier(Object identifier) throws DataDictionaryException { + this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); + } + + protected void setErrorCode(Object errorCode) throws DataDictionaryException { + this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); + } + + protected void setErrorMessage(Object errorMessage) throws DataDictionaryException { + this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); + } + + protected void setData(Object data) throws DataDictionaryException { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() { + try { + this.kIdentifier = + new KeyDescriptorDataDictionary(KEY_IDENTIFIER, true, TICIdentifier.class); + this.keys.add(this.kIdentifier); + + this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); + this.keys.add(this.kErrorCode); + + this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, true, false); + this.keys.add(this.kErrorMessage); + + this.kData = + new KeyDescriptorDataDictionary( + KEY_DATA, false, DataDictionaryBase.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/core/TICCoreErrorCode.java b/src/main/java/enedis/tic/core/TICCoreErrorCode.java index 548a78f..b157360 100644 --- a/src/main/java/enedis/tic/core/TICCoreErrorCode.java +++ b/src/main/java/enedis/tic/core/TICCoreErrorCode.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,27 +7,24 @@ package enedis.tic.core; -public enum TICCoreErrorCode -{ - NO_ERROR(0), - STREAM_PORT_ID_NOT_FOUND(1), - STREAM_PORT_NAME_NOT_FOUND(2), - STREAM_PORT_DESCRIPTOR_EMPTY(3), - STREAM_MODE_NOT_DEFINED(4), - STREAM_IDENTIFIER_NOT_FOUND(5), - STREAM_UNPLUGGED(6), - DATA_READ_TIMEOUT(7), - OTHER_REASON(99); +public enum TICCoreErrorCode { + NO_ERROR(0), + STREAM_PORT_ID_NOT_FOUND(1), + STREAM_PORT_NAME_NOT_FOUND(2), + STREAM_PORT_DESCRIPTOR_EMPTY(3), + STREAM_MODE_NOT_DEFINED(4), + STREAM_IDENTIFIER_NOT_FOUND(5), + STREAM_UNPLUGGED(6), + DATA_READ_TIMEOUT(7), + OTHER_REASON(99); - private int code; + private int code; - private TICCoreErrorCode(int code) - { - this.code = code; - } + private TICCoreErrorCode(int code) { + this.code = code; + } - public int getCode() - { - return this.code; - } + public int getCode() { + return this.code; + } } diff --git a/src/main/java/enedis/tic/core/TICCoreException.java b/src/main/java/enedis/tic/core/TICCoreException.java index 45bd7d3..9f75b55 100644 --- a/src/main/java/enedis/tic/core/TICCoreException.java +++ b/src/main/java/enedis/tic/core/TICCoreException.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -9,13 +9,10 @@ import enedis.lab.types.ExceptionBase; -public class TICCoreException extends ExceptionBase -{ - private static final long serialVersionUID = -3285641164559292710L; - - public TICCoreException(int code, String info) - { - super(code, info); - } +public class TICCoreException extends ExceptionBase { + private static final long serialVersionUID = -3285641164559292710L; + public TICCoreException(int code, String info) { + super(code, info); + } } diff --git a/src/main/java/enedis/tic/core/TICCoreFrame.java b/src/main/java/enedis/tic/core/TICCoreFrame.java index 97442ea..6101bc8 100644 --- a/src/main/java/enedis/tic/core/TICCoreFrame.java +++ b/src/main/java/enedis/tic/core/TICCoreFrame.java @@ -7,11 +7,6 @@ package enedis.tic.core; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.protocol.tic.TICMode; import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; @@ -20,209 +15,198 @@ import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; import enedis.lab.types.datadictionary.KeyDescriptorEnum; import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * TICCoreFrame class * - * Generated + *

    Generated */ -public class TICCoreFrame extends DataDictionaryBase -{ - protected static final String KEY_IDENTIFIER = "identifier"; - protected static final String KEY_MODE = "mode"; - protected static final String KEY_CAPTURE_DATE_TIME = "captureDateTime"; - protected static final String KEY_CONTENT = "content"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kIdentifier; - protected KeyDescriptorEnum kMode; - protected KeyDescriptorLocalDateTime kCaptureDateTime; - protected KeyDescriptorDataDictionary kContent; - - protected TICCoreFrame() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICCoreFrame(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICCoreFrame(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param identifier - * @param mode - * @param captureDateTime - * @param content - * @throws DataDictionaryException - */ - public TICCoreFrame(TICIdentifier identifier, TICMode mode, LocalDateTime captureDateTime, DataDictionaryBase content) throws DataDictionaryException - { - this(); - - this.setIdentifier(identifier); - this.setMode(mode); - this.setCaptureDateTime(captureDateTime); - this.setContent(content); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /** - * Get identifier - * - * @return the identifier - */ - public TICIdentifier getIdentifier() - { - return (TICIdentifier) this.data.get(KEY_IDENTIFIER); - } - - /** - * Get mode - * - * @return the mode - */ - public TICMode getMode() - { - return (TICMode) this.data.get(KEY_MODE); - } - - /** - * Get capture date time - * - * @return the capture date time - */ - public LocalDateTime getCaptureDateTime() - { - return (LocalDateTime) this.data.get(KEY_CAPTURE_DATE_TIME); - } - - /** - * Get content - * - * @return the content - */ - public DataDictionaryBase getContent() - { - return (DataDictionaryBase) this.data.get(KEY_CONTENT); - } - - /** - * Set identifier - * - * @param identifier - * @throws DataDictionaryException - */ - public void setIdentifier(TICIdentifier identifier) throws DataDictionaryException - { - this.setIdentifier((Object) identifier); - } - - /** - * Set mode - * - * @param mode - * @throws DataDictionaryException - */ - public void setMode(TICMode mode) throws DataDictionaryException - { - this.setMode((Object) mode); - } - - /** - * Set capture date time - * - * @param captureDateTime - * @throws DataDictionaryException - */ - public void setCaptureDateTime(LocalDateTime captureDateTime) throws DataDictionaryException - { - this.setCaptureDateTime((Object) captureDateTime); - } - - /** - * Set content - * - * @param content - * @throws DataDictionaryException - */ - public void setContent(DataDictionaryBase content) throws DataDictionaryException - { - this.setContent((Object) content); - } - - protected void setIdentifier(Object identifier) throws DataDictionaryException - { - this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); - } - - protected void setMode(Object mode) throws DataDictionaryException - { - this.data.put(KEY_MODE, this.kMode.convert(mode)); - } - - protected void setCaptureDateTime(Object captureDateTime) throws DataDictionaryException - { - this.data.put(KEY_CAPTURE_DATE_TIME, this.kCaptureDateTime.convert(captureDateTime)); - } - - protected void setContent(Object content) throws DataDictionaryException - { - this.data.put(KEY_CONTENT, this.kContent.convert(content)); - } - - private void loadKeyDescriptors() - { - try - { - this.kIdentifier = new KeyDescriptorDataDictionary(KEY_IDENTIFIER, true, TICIdentifier.class); - this.keys.add(this.kIdentifier); - - this.kMode = new KeyDescriptorEnum(KEY_MODE, true, TICMode.class); - this.keys.add(this.kMode); - - this.kCaptureDateTime = new KeyDescriptorLocalDateTime(KEY_CAPTURE_DATE_TIME, true); - this.keys.add(this.kCaptureDateTime); - - this.kContent = new KeyDescriptorDataDictionary(KEY_CONTENT, true, DataDictionaryBase.class); - this.keys.add(this.kContent); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class TICCoreFrame extends DataDictionaryBase { + protected static final String KEY_IDENTIFIER = "identifier"; + protected static final String KEY_MODE = "mode"; + protected static final String KEY_CAPTURE_DATE_TIME = "captureDateTime"; + protected static final String KEY_CONTENT = "content"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kIdentifier; + protected KeyDescriptorEnum kMode; + protected KeyDescriptorLocalDateTime kCaptureDateTime; + protected KeyDescriptorDataDictionary kContent; + + protected TICCoreFrame() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICCoreFrame(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICCoreFrame(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param identifier + * @param mode + * @param captureDateTime + * @param content + * @throws DataDictionaryException + */ + public TICCoreFrame( + TICIdentifier identifier, + TICMode mode, + LocalDateTime captureDateTime, + DataDictionaryBase content) + throws DataDictionaryException { + this(); + + this.setIdentifier(identifier); + this.setMode(mode); + this.setCaptureDateTime(captureDateTime); + this.setContent(content); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + super.updateOptionalParameters(); + } + + /** + * Get identifier + * + * @return the identifier + */ + public TICIdentifier getIdentifier() { + return (TICIdentifier) this.data.get(KEY_IDENTIFIER); + } + + /** + * Get mode + * + * @return the mode + */ + public TICMode getMode() { + return (TICMode) this.data.get(KEY_MODE); + } + + /** + * Get capture date time + * + * @return the capture date time + */ + public LocalDateTime getCaptureDateTime() { + return (LocalDateTime) this.data.get(KEY_CAPTURE_DATE_TIME); + } + + /** + * Get content + * + * @return the content + */ + public DataDictionaryBase getContent() { + return (DataDictionaryBase) this.data.get(KEY_CONTENT); + } + + /** + * Set identifier + * + * @param identifier + * @throws DataDictionaryException + */ + public void setIdentifier(TICIdentifier identifier) throws DataDictionaryException { + this.setIdentifier((Object) identifier); + } + + /** + * Set mode + * + * @param mode + * @throws DataDictionaryException + */ + public void setMode(TICMode mode) throws DataDictionaryException { + this.setMode((Object) mode); + } + + /** + * Set capture date time + * + * @param captureDateTime + * @throws DataDictionaryException + */ + public void setCaptureDateTime(LocalDateTime captureDateTime) throws DataDictionaryException { + this.setCaptureDateTime((Object) captureDateTime); + } + + /** + * Set content + * + * @param content + * @throws DataDictionaryException + */ + public void setContent(DataDictionaryBase content) throws DataDictionaryException { + this.setContent((Object) content); + } + + protected void setIdentifier(Object identifier) throws DataDictionaryException { + this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); + } + + protected void setMode(Object mode) throws DataDictionaryException { + this.data.put(KEY_MODE, this.kMode.convert(mode)); + } + + protected void setCaptureDateTime(Object captureDateTime) throws DataDictionaryException { + this.data.put(KEY_CAPTURE_DATE_TIME, this.kCaptureDateTime.convert(captureDateTime)); + } + + protected void setContent(Object content) throws DataDictionaryException { + this.data.put(KEY_CONTENT, this.kContent.convert(content)); + } + + private void loadKeyDescriptors() { + try { + this.kIdentifier = + new KeyDescriptorDataDictionary(KEY_IDENTIFIER, true, TICIdentifier.class); + this.keys.add(this.kIdentifier); + + this.kMode = new KeyDescriptorEnum(KEY_MODE, true, TICMode.class); + this.keys.add(this.kMode); + + this.kCaptureDateTime = new KeyDescriptorLocalDateTime(KEY_CAPTURE_DATE_TIME, true); + this.keys.add(this.kCaptureDateTime); + + this.kContent = + new KeyDescriptorDataDictionary( + KEY_CONTENT, true, DataDictionaryBase.class); + this.keys.add(this.kContent); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/core/TICCoreStream.java b/src/main/java/enedis/tic/core/TICCoreStream.java index 8a72a48..33e509e 100644 --- a/src/main/java/enedis/tic/core/TICCoreStream.java +++ b/src/main/java/enedis/tic/core/TICCoreStream.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -10,15 +10,12 @@ import enedis.lab.util.task.Notifier; import enedis.lab.util.task.Task; -/** - * TICCore stream interface - */ -public interface TICCoreStream extends Notifier, Task -{ - /** - * Get identifier - * - * @return The TICIdentifier associated with the stream - */ - public TICIdentifier getIdentifier(); +/** TICCore stream interface */ +public interface TICCoreStream extends Notifier, Task { + /** + * Get identifier + * + * @return The TICIdentifier associated with the stream + */ + public TICIdentifier getIdentifier(); } diff --git a/src/main/java/enedis/tic/core/TICCoreStreamBase.java b/src/main/java/enedis/tic/core/TICCoreStreamBase.java index 8975280..f14d461 100644 --- a/src/main/java/enedis/tic/core/TICCoreStreamBase.java +++ b/src/main/java/enedis/tic/core/TICCoreStreamBase.java @@ -7,12 +7,6 @@ package enedis.tic.core; -import java.time.LocalDateTime; -import java.util.Collection; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import enedis.lab.io.channels.Channel; import enedis.lab.io.channels.ChannelException; import enedis.lab.io.datastreams.DataStreamBase; @@ -37,266 +31,234 @@ import enedis.lab.util.task.Notifier; import enedis.lab.util.task.NotifierBase; import enedis.lab.util.task.Task; +import java.time.LocalDateTime; +import java.util.Collection; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; -/** - * TICCore stream implementation - */ -public class TICCoreStreamBase implements TICCoreStream, Task, DataStreamListener -{ +/** TICCore stream implementation */ +public class TICCoreStreamBase implements TICCoreStream, Task, DataStreamListener { - private TICIdentifier identifier; - private DataStreamBase stream; - private Channel channel; - private Notifier notifier; - static private Logger logger = LogManager.getLogger(); + private TICIdentifier identifier; + private DataStreamBase stream; + private Channel channel; + private Notifier notifier; + private static Logger logger = LogManager.getLogger(); - public TICCoreStreamBase(String portId, String portName, TICMode ticMode) throws TICCoreException - { - TICPortDescriptor descriptor = null; + public TICCoreStreamBase(String portId, String portName, TICMode ticMode) + throws TICCoreException { + TICPortDescriptor descriptor = null; - if (portId != null) - { - descriptor = TICPortFinderBase.getInstance().findByPortId(portId); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_ID_NOT_FOUND.getCode(), "TICCore stream port id " + portId + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - } - else if (portName != null) - { - descriptor = TICPortFinderBase.getInstance().findByPortName(portName); - if (descriptor == null) - { - descriptor = TICPortFinderBase.getInstance().findNative(portName); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_NAME_NOT_FOUND.getCode(), - "TICCore stream port name " + portName + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - } - } - else - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_DESCRIPTOR_EMPTY.getCode(), "TICCore stream port descriptor empty!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - if (ticMode == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_MODE_NOT_DEFINED.getCode(), "TICCore stream mode not defined!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - try - { - this.identifier = new TICIdentifier(portId, portName, null); - this.channel = new ChannelTICSerialPort(new ChannelTICSerialPortConfiguration("Channel@" + descriptor.getPortName(), portName, ticMode)); - this.stream = new TICInputStream( - new TICStreamConfiguration("Stream@" + descriptor.getPortName(), DataStreamDirection.INPUT, "Channel@" + descriptor.getPortName(), ticMode)); - this.stream.setChannel(this.channel); - this.notifier = new NotifierBase(); - logger = LogManager.getLogger(); - } - catch (DataDictionaryException | ChannelException | DataStreamException e) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.OTHER_REASON.getCode(), "TICCore stream instanciation failed : " + e.getMessage()); - logger.error(exception.getMessage(), exception); - throw exception; - } - } + if (portId != null) { + descriptor = TICPortFinderBase.getInstance().findByPortId(portId); + if (descriptor == null) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_PORT_ID_NOT_FOUND.getCode(), + "TICCore stream port id " + portId + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + } else if (portName != null) { + descriptor = TICPortFinderBase.getInstance().findByPortName(portName); + if (descriptor == null) { + descriptor = TICPortFinderBase.getInstance().findNative(portName); + if (descriptor == null) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_PORT_NAME_NOT_FOUND.getCode(), + "TICCore stream port name " + portName + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + } + } else { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_PORT_DESCRIPTOR_EMPTY.getCode(), + "TICCore stream port descriptor empty!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + if (ticMode == null) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_MODE_NOT_DEFINED.getCode(), + "TICCore stream mode not defined!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + try { + this.identifier = new TICIdentifier(portId, portName, null); + this.channel = + new ChannelTICSerialPort( + new ChannelTICSerialPortConfiguration( + "Channel@" + descriptor.getPortName(), portName, ticMode)); + this.stream = + new TICInputStream( + new TICStreamConfiguration( + "Stream@" + descriptor.getPortName(), + DataStreamDirection.INPUT, + "Channel@" + descriptor.getPortName(), + ticMode)); + this.stream.setChannel(this.channel); + this.notifier = new NotifierBase(); + logger = LogManager.getLogger(); + } catch (DataDictionaryException | ChannelException | DataStreamException e) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.OTHER_REASON.getCode(), + "TICCore stream instanciation failed : " + e.getMessage()); + logger.error(exception.getMessage(), exception); + throw exception; + } + } - @Override - public TICIdentifier getIdentifier() - { - TICIdentifier identifier; + @Override + public TICIdentifier getIdentifier() { + TICIdentifier identifier; - synchronized (this.identifier) - { - identifier = (TICIdentifier) this.identifier.clone(); - } + synchronized (this.identifier) { + identifier = (TICIdentifier) this.identifier.clone(); + } - return identifier; - } + return identifier; + } - @Override - public Collection getSubscribers() - { - return this.notifier.getSubscribers(); - } + @Override + public Collection getSubscribers() { + return this.notifier.getSubscribers(); + } - @Override - public boolean hasSubscriber(TICCoreSubscriber subscriber) - { - return this.notifier.hasSubscriber(subscriber); - } + @Override + public boolean hasSubscriber(TICCoreSubscriber subscriber) { + return this.notifier.hasSubscriber(subscriber); + } - @Override - public void subscribe(TICCoreSubscriber subscriber) - { - this.notifier.subscribe(subscriber); - } + @Override + public void subscribe(TICCoreSubscriber subscriber) { + this.notifier.subscribe(subscriber); + } - @Override - public void unsubscribe(TICCoreSubscriber subscriber) - { - this.notifier.unsubscribe(subscriber); - } + @Override + public void unsubscribe(TICCoreSubscriber subscriber) { + this.notifier.unsubscribe(subscriber); + } - @Override - public void onDataReceived(String dataStreamName, DataDictionary data) - { - TICCoreFrame frame = this.createFrame(data); + @Override + public void onDataReceived(String dataStreamName, DataDictionary data) { + TICCoreFrame frame = this.createFrame(data); - if (frame != null) - { - this.notifyOnData(frame); - } - else - { - logger.debug("Frame creation skipped due to null TICFrame"); - } - } + if (frame != null) { + this.notifyOnData(frame); + } else { + logger.debug("Frame creation skipped due to null TICFrame"); + } + } - @Override - public void onDataSent(String dataStreamName, DataDictionary data) - { - } + @Override + public void onDataSent(String dataStreamName, DataDictionary data) {} - @Override - public void onStatusChanged(String dataStreamName, DataStreamStatus newStatus) - { - } + @Override + public void onStatusChanged(String dataStreamName, DataStreamStatus newStatus) {} - @Override - public void onErrorDetected(String dataStreamName, int errorCode, String errorMessage, DataDictionary data) - { - TICCoreError error = this.createError(errorCode, errorMessage, data); - this.notifyOnError(error); - } + @Override + public void onErrorDetected( + String dataStreamName, int errorCode, String errorMessage, DataDictionary data) { + TICCoreError error = this.createError(errorCode, errorMessage, data); + this.notifyOnError(error); + } - @Override - public void start() - { - this.channel.start(); - try - { - this.stream.open(); - this.stream.subscribe(this); - } - catch (DataStreamException e) - { - logger.error(e.getMessage()); - } - } + @Override + public void start() { + this.channel.start(); + try { + this.stream.open(); + this.stream.subscribe(this); + } catch (DataStreamException e) { + logger.error(e.getMessage()); + } + } - @Override - public void stop() - { - this.stream.unsubscribe(this); - try - { - this.stream.close(); - } - catch (DataStreamException e) - { - logger.error(e.getMessage()); - } - this.channel.stop(); - } + @Override + public void stop() { + this.stream.unsubscribe(this); + try { + this.stream.close(); + } catch (DataStreamException e) { + logger.error(e.getMessage()); + } + this.channel.stop(); + } - @Override - public boolean isRunning() - { - return this.channel.isRunning(); - } + @Override + public boolean isRunning() { + return this.channel.isRunning(); + } - private TICCoreFrame createFrame(DataDictionary data) - { - TICCoreFrame frame = new TICCoreFrame(); - try - { - frame.setCaptureDateTime(LocalDateTime.now()); - TICFrame ticFrame = (TICFrame) data.get(TICInputStream.KEY_DATA); + private TICCoreFrame createFrame(DataDictionary data) { + TICCoreFrame frame = new TICCoreFrame(); + try { + frame.setCaptureDateTime(LocalDateTime.now()); + TICFrame ticFrame = (TICFrame) data.get(TICInputStream.KEY_DATA); - if (ticFrame == null) - { - logger.warn("TICFrame is null. Skipping frame creation."); - return null; - } + if (ticFrame == null) { + logger.warn("TICFrame is null. Skipping frame creation."); + return null; + } - DataDictionaryBase content = new DataDictionaryBase(); - for (TICFrameDataSet frameDataSet : ticFrame.getDataSetList()) - { - String label = frameDataSet.getLabel(); - content.set(label, ticFrame.getData(label)); - } - frame.setContent(content); - String serialNumber = null; - if (ticFrame instanceof TICFrameStandard) - { - frame.setMode(TICMode.STANDARD); - serialNumber = (String) content.get("ADSC"); - } - else if (ticFrame instanceof TICFrameHistoric) - { - frame.setMode(TICMode.HISTORIC); - serialNumber = (String) content.get("ADCO"); - } - synchronized (this.identifier) - { - this.identifier.setSerialNumber(serialNumber); - frame.setIdentifier(this.identifier.clone()); - } - } - catch (DataDictionaryException e) - { - logger.error(e.getMessage()); - frame = null; - } + DataDictionaryBase content = new DataDictionaryBase(); + for (TICFrameDataSet frameDataSet : ticFrame.getDataSetList()) { + String label = frameDataSet.getLabel(); + content.set(label, ticFrame.getData(label)); + } + frame.setContent(content); + String serialNumber = null; + if (ticFrame instanceof TICFrameStandard) { + frame.setMode(TICMode.STANDARD); + serialNumber = (String) content.get("ADSC"); + } else if (ticFrame instanceof TICFrameHistoric) { + frame.setMode(TICMode.HISTORIC); + serialNumber = (String) content.get("ADCO"); + } + synchronized (this.identifier) { + this.identifier.setSerialNumber(serialNumber); + frame.setIdentifier(this.identifier.clone()); + } + } catch (DataDictionaryException e) { + logger.error(e.getMessage()); + frame = null; + } - return frame; - } + return frame; + } - private TICCoreError createError(int errorCode, String errorMessage, DataDictionary data) - { - TICCoreError error = null; - try - { - error = new TICCoreError(this.getIdentifier(), errorCode, errorMessage, data); - } - catch (DataDictionaryException e) - { - logger.error(e.getMessage()); - } + private TICCoreError createError(int errorCode, String errorMessage, DataDictionary data) { + TICCoreError error = null; + try { + error = new TICCoreError(this.getIdentifier(), errorCode, errorMessage, data); + } catch (DataDictionaryException e) { + logger.error(e.getMessage()); + } - return error; - } + return error; + } - private void notifyOnData(TICCoreFrame frame) - { - if (frame == null) - { - return; - } - for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) - { - subscriber.onData(frame); - } - } + private void notifyOnData(TICCoreFrame frame) { + if (frame == null) { + return; + } + for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) { + subscriber.onData(frame); + } + } - private void notifyOnError(TICCoreError error) - { - if (error == null) - { - return; - } - for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) - { - subscriber.onError(error); - } - } + private void notifyOnError(TICCoreError error) { + if (error == null) { + return; + } + for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) { + subscriber.onError(error); + } + } } diff --git a/src/main/java/enedis/tic/core/TICCoreSubscriber.java b/src/main/java/enedis/tic/core/TICCoreSubscriber.java index 4adc14e..3f550e4 100644 --- a/src/main/java/enedis/tic/core/TICCoreSubscriber.java +++ b/src/main/java/enedis/tic/core/TICCoreSubscriber.java @@ -9,24 +9,19 @@ import enedis.lab.util.task.Subscriber; -/** - * TICCore subscriber interface - */ -public interface TICCoreSubscriber extends Subscriber -{ - /** - * Notify when data - * - * @param frame - * the frame received - */ - public void onData(TICCoreFrame frame); +/** TICCore subscriber interface */ +public interface TICCoreSubscriber extends Subscriber { + /** + * Notify when data + * + * @param frame the frame received + */ + public void onData(TICCoreFrame frame); - /** - * Notify when error - * - * @param error - * the error detected - */ - public void onError(TICCoreError error); + /** + * Notify when error + * + * @param error the error detected + */ + public void onError(TICCoreError error); } diff --git a/src/main/java/enedis/tic/core/TICIdentifier.java b/src/main/java/enedis/tic/core/TICIdentifier.java index a6cde29..1e09916 100644 --- a/src/main/java/enedis/tic/core/TICIdentifier.java +++ b/src/main/java/enedis/tic/core/TICIdentifier.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,221 +7,195 @@ package enedis.tic.core; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorString; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * TICIdentifier class * - * Generated + *

    Generated */ -public class TICIdentifier extends DataDictionaryBase -{ - protected static final String KEY_PORT_ID = "portId"; - protected static final String KEY_PORT_NAME = "portName"; - protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kPortId; - protected KeyDescriptorString kPortName; - protected KeyDescriptorString kSerialNumber; - - protected TICIdentifier() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICIdentifier(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICIdentifier(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param portId - * @param portName - * @param serialNumber - * @throws DataDictionaryException - */ - public TICIdentifier(String portId, String portName, String serialNumber) throws DataDictionaryException - { - this(); - - this.setPortId((Object) portId); - this.setPortName((Object) portName); - this.setSerialNumber((Object) serialNumber); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (this.getPortId() == null && this.getPortName() == null && this.getSerialNumber() == null) - { - throw new DataDictionaryException("Empty TICIdentifier not allowed!"); - } - } - - public boolean matches(TICIdentifier identifier) - { - if (identifier != null) - { - if (this.getSerialNumber() != null && identifier.getSerialNumber() != null) - { - return this.getSerialNumber().equals(identifier.getSerialNumber()); - } - if (this.getPortId() != null && identifier.getPortId() != null) - { - return this.getPortId().equals(identifier.getPortId()); - } - if (this.getPortName() != null && identifier.getPortName() != null) - { - return this.getPortName().equals(identifier.getPortName()); - } - } - - return false; - } - - /** - * Get port id - * - * @return the port id - */ - public String getPortId() - { - return (String) this.data.get(KEY_PORT_ID); - } - - /** - * Get port name - * - * @return the port name - */ - public String getPortName() - { - return (String) this.data.get(KEY_PORT_NAME); - } - - /** - * Get serial number - * - * @return the serial number - */ - public String getSerialNumber() - { - return (String) this.data.get(KEY_SERIAL_NUMBER); - } - - /** - * Set port id - * - * @param portId - * @throws DataDictionaryException - */ - public void setPortId(String portId) throws DataDictionaryException - { - if (portId == null && this.getPortName() == null && this.getSerialNumber() == null) - { - throw new DataDictionaryException("Cannot set null portId because empty TICIdentifier not allowed!"); - } - this.setPortId((Object) portId); - } - - /** - * Set port name - * - * @param portName - * @throws DataDictionaryException - */ - public void setPortName(String portName) throws DataDictionaryException - { - if (this.getPortId() == null && portName == null && this.getSerialNumber() == null) - { - throw new DataDictionaryException("Cannot set null portName because empty TICIdentifier not allowed!"); - } - this.setPortName((Object) portName); - } - - /** - * Set serial number - * - * @param serialNumber - * @throws DataDictionaryException - */ - public void setSerialNumber(String serialNumber) throws DataDictionaryException - { - if (this.getPortId() == null && this.getPortName() == null && serialNumber == null) - { - throw new DataDictionaryException("Cannot set null serialNumber because empty TICIdentifier not allowed!"); - } - this.setSerialNumber((Object) serialNumber); - } - - protected void setPortId(Object portId) throws DataDictionaryException - { - this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); - } - - protected void setPortName(Object portName) throws DataDictionaryException - { - this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); - } - - protected void setSerialNumber(Object serialNumber) throws DataDictionaryException - { - this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); - } - - private void loadKeyDescriptors() - { - try - { - this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); - this.keys.add(this.kPortId); - - this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); - this.keys.add(this.kPortName); - - this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, false); - this.keys.add(this.kSerialNumber); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class TICIdentifier extends DataDictionaryBase { + protected static final String KEY_PORT_ID = "portId"; + protected static final String KEY_PORT_NAME = "portName"; + protected static final String KEY_SERIAL_NUMBER = "serialNumber"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorString kPortId; + protected KeyDescriptorString kPortName; + protected KeyDescriptorString kSerialNumber; + + protected TICIdentifier() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TICIdentifier(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TICIdentifier(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param portId + * @param portName + * @param serialNumber + * @throws DataDictionaryException + */ + public TICIdentifier(String portId, String portName, String serialNumber) + throws DataDictionaryException { + this(); + + this.setPortId((Object) portId); + this.setPortName((Object) portName); + this.setSerialNumber((Object) serialNumber); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (this.getPortId() == null && this.getPortName() == null && this.getSerialNumber() == null) { + throw new DataDictionaryException("Empty TICIdentifier not allowed!"); + } + } + + public boolean matches(TICIdentifier identifier) { + if (identifier != null) { + if (this.getSerialNumber() != null && identifier.getSerialNumber() != null) { + return this.getSerialNumber().equals(identifier.getSerialNumber()); + } + if (this.getPortId() != null && identifier.getPortId() != null) { + return this.getPortId().equals(identifier.getPortId()); + } + if (this.getPortName() != null && identifier.getPortName() != null) { + return this.getPortName().equals(identifier.getPortName()); + } + } + + return false; + } + + /** + * Get port id + * + * @return the port id + */ + public String getPortId() { + return (String) this.data.get(KEY_PORT_ID); + } + + /** + * Get port name + * + * @return the port name + */ + public String getPortName() { + return (String) this.data.get(KEY_PORT_NAME); + } + + /** + * Get serial number + * + * @return the serial number + */ + public String getSerialNumber() { + return (String) this.data.get(KEY_SERIAL_NUMBER); + } + + /** + * Set port id + * + * @param portId + * @throws DataDictionaryException + */ + public void setPortId(String portId) throws DataDictionaryException { + if (portId == null && this.getPortName() == null && this.getSerialNumber() == null) { + throw new DataDictionaryException( + "Cannot set null portId because empty TICIdentifier not allowed!"); + } + this.setPortId((Object) portId); + } + + /** + * Set port name + * + * @param portName + * @throws DataDictionaryException + */ + public void setPortName(String portName) throws DataDictionaryException { + if (this.getPortId() == null && portName == null && this.getSerialNumber() == null) { + throw new DataDictionaryException( + "Cannot set null portName because empty TICIdentifier not allowed!"); + } + this.setPortName((Object) portName); + } + + /** + * Set serial number + * + * @param serialNumber + * @throws DataDictionaryException + */ + public void setSerialNumber(String serialNumber) throws DataDictionaryException { + if (this.getPortId() == null && this.getPortName() == null && serialNumber == null) { + throw new DataDictionaryException( + "Cannot set null serialNumber because empty TICIdentifier not allowed!"); + } + this.setSerialNumber((Object) serialNumber); + } + + protected void setPortId(Object portId) throws DataDictionaryException { + this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); + } + + protected void setPortName(Object portName) throws DataDictionaryException { + this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); + } + + protected void setSerialNumber(Object serialNumber) throws DataDictionaryException { + this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); + } + + private void loadKeyDescriptors() { + try { + this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); + this.keys.add(this.kPortId); + + this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); + this.keys.add(this.kPortName); + + this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, false); + this.keys.add(this.kSerialNumber); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java b/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java index e815213..df68bd5 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java +++ b/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,15 +7,6 @@ package enedis.tic.service; -import java.io.File; -import java.io.InputStream; -import java.util.Properties; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.LoggerContext; - import enedis.lab.types.datadictionary.DataDictionaryBase; import enedis.tic.core.TICCore; import enedis.tic.core.TICCoreBase; @@ -25,345 +16,304 @@ import enedis.tic.service.netty.TIC2WebSocketServer; import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandlerBase; +import java.io.File; +import java.io.InputStream; +import java.util.Properties; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; import picocli.CommandLine; import picocli.CommandLine.IVersionProvider; import picocli.CommandLine.ParseResult; -/** - * Class used to handle the application - */ -public class TIC2WebSocketApplication -{ - /** - * Project properties (see file project.properties) - */ - public static final Properties PROJECT_PROPERTIES = new Properties(); - - static - { - try - { - InputStream stream = TIC2WebSocketApplication.class.getClassLoader().getResourceAsStream("TIC2WebSocket.properties"); - - if (stream == null) - { - System.err.println("Can't find projet property file!"); - } - else - { - TIC2WebSocketApplication.PROJECT_PROPERTIES.load(stream); - } - } - catch (Exception exception) - { - System.err.println("Can't load projet property file!"); - exception.printStackTrace(System.err); - } - } - - /** - * Project name ("project_name" from project.properties) - */ - public static final String NAME = PROJECT_PROPERTIES.getProperty("project_name", ""); - - /** - * Project version ("project_version" from project.properties) - */ - public static final String VERSION = PROJECT_PROPERTIES.getProperty("project_version", ""); - - /** - * Project description ("project_description" from project.properties) - */ - public static final String DESCRIPTION = PROJECT_PROPERTIES.getProperty("project_description", ""); - - /** - * Program entry point - * - * @param args - * Command line arguments - */ - public static void main(String[] args) - { - try - { - TIC2WebSocketApplication application = new TIC2WebSocketApplication(args); - int result = application.run(); - System.exit(result); - } - catch (Exception exception) - { - exception.printStackTrace(System.err); - System.exit(-1); - } - } - - private String[] commandLineArgs; - private CommandLine commandLineParser; - private TIC2WebSocketCommandLine commandLine; - private Logger logger; - private boolean started; - private TIC2WebSocketConfiguration configuration; - - private TIC2WebSocketClientPool clientPool; - private TIC2WebSocketServer server; - private TIC2WebSocketRequestHandler requestHandler; - private TICCore ticCore; - - /** - * Application constructor - * - * @param args - * Command line arguments - */ - public TIC2WebSocketApplication(String[] args) - { - this.commandLineArgs = args; - this.commandLine = new TIC2WebSocketCommandLine(); - this.commandLineParser = new CommandLine(this.commandLine); - this.commandLineParser.setCommandName(TIC2WebSocketApplication.NAME); - this.commandLineParser.getCommandSpec().versionProvider(new IVersionProvider() - { - @Override - public String[] getVersion() throws Exception - { - return new String[] { TIC2WebSocketApplication.NAME + " v" + TIC2WebSocketApplication.VERSION }; - } - }); - this.commandLineParser.registerConverter(Level.class, new TIC2WebSocketCommandLine.VerboseLevelConverter()); - this.updateLoggerConfiguration(Level.ERROR); - this.started = false; - } - - /** - * Application execution - * - * @return 0 if success, else an error code - * @throws InterruptedException - * If the application thread gets interrupted - * @see TIC2WebSocketApplicationErrorCode - */ - public int run() throws InterruptedException - { - int result = this.init(); - - if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) - { - return result; - } - result = this.start(); - if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) - { - return result; - } - - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() - { - @Override - public void run() - { - TIC2WebSocketApplication.this.stop(); - } - })); - - while (this.isStarted()) - { - Thread.sleep(1000); - } - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - /** - * Application initialization - * - * @return 0 if success, else an error code - * @see TIC2WebSocketApplicationErrorCode - */ - public int init() - { - if (this.isStarted()) - { - this.logger.error("Application already started!"); - return TIC2WebSocketApplicationErrorCode.APPLICATION_ALREADY_STARTED.code(); - } - int result = this.parseCommandLine(); - if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) - { - return result; - } - result = this.updateLoggerConfiguration(this.commandLine.verboseLevel); - if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) - { - return result; - } - - result = this.loadConfiguration(); - if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) - { - return result; - } - - this.logger.info(TIC2WebSocketApplication.NAME + " initialized"); - - this.ticCore = new TICCoreBase(this.configuration.getTicMode(), this.configuration.getTicPortNames()); - this.clientPool = new TIC2WebSocketClientPoolBase(); - this.requestHandler = new TIC2WebSocketRequestHandlerBase(this.ticCore); - - this.server = new TIC2WebSocketServer("localhost", this.configuration.getServerPort().intValue(), - this.clientPool, this.requestHandler); - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - /** - * Application start-up - * - * @return 0 if success, else an error code - */ - public int start() - { - if (this.commandLineParser.isUsageHelpRequested()) - { - this.commandLineParser.usage(System.out); - } - if (this.commandLineParser.isVersionHelpRequested()) - { - this.commandLineParser.printVersionHelp(System.out); - } - this.logger.info(TIC2WebSocketApplication.NAME + " starting"); - - try - { - this.ticCore.start(); - this.server.start(); - } - catch (Exception exception) - { - this.logger.error(exception.getMessage(), exception); - return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); - } - - this.logger.info(TIC2WebSocketApplication.NAME + " started"); - this.started = true; - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - /** - * Application start-up indicator - * - * @return true if the application is started, false otherwise - */ - public boolean isStarted() - { - return this.started; - } - - /** - * Application stop - * - * @return 0 if success, else an error code - * @see TIC2WebSocketApplicationErrorCode - */ - public int stop() - { - if (!this.isStarted()) - { - this.logger.error("Application not started!"); - return TIC2WebSocketApplicationErrorCode.APPLICATION_NOT_STARTED.code(); - } - - try - { - this.server.stop(); - this.ticCore.stop(); - } - catch (Exception exception) - { - this.logger.error(exception.getMessage(), exception); - return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); - } - - this.logger.info(TIC2WebSocketApplication.NAME + " stopped"); - this.started = false; - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - private int parseCommandLine() - { - try - { - ParseResult parseResult = this.commandLineParser.parseArgs(this.commandLineArgs); - if (parseResult.errors().size() > 0) - { - this.logger.error("Invalid command line!"); - for (int i = 0; i < parseResult.errors().size(); i++) - { - this.logger.error(parseResult.errors().get(i).getMessage()); - } - return TIC2WebSocketApplicationErrorCode.COMMAND_LINE_INVALID.code(); - } - } - catch (Exception exception) - { - this.logger.error("Invalid command line!"); - this.logger.error(exception.getMessage()); - return TIC2WebSocketApplicationErrorCode.COMMAND_LINE_INVALID.code(); - } - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - private int updateLoggerConfiguration(Level level) - { - try - { - System.setProperty("log4j.configurationFile", "log4j2-" + level.name().toLowerCase() + ".xml"); - LoggerContext context = (LoggerContext) LogManager.getContext(false); - context.reconfigure(); - this.logger = LogManager.getLogger(); - } - catch (Exception exception) - { - System.err.println("Log configuration update failure!"); - System.err.println(exception.getMessage()); - return TIC2WebSocketApplicationErrorCode.UPDATE_LOGGER_CONFIGURATION_FAILURE.code(); - } - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - private int loadConfiguration() - { - String configFile = null; - - if (this.commandLine.configFile != null) - { - configFile = this.commandLine.configFile; - } - else - { - configFile = System.getProperty("configFile", NAME + "Configuration.json"); - } - File configPath = new File(configFile); - if (!configPath.exists()) - { - this.logger.error("No configuration file path '" + configPath.getAbsolutePath() + "' not found"); - return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); - } - try - { - this.logger.info("Loading configuration file " + configPath); - this.configuration = (TIC2WebSocketConfiguration) DataDictionaryBase.fromFile(configPath, TIC2WebSocketConfiguration.class); - } - catch (Exception exception) - { - this.logger.error("Loading configuration file " + configPath + " failed", exception); - return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); - } - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - - } -} \ No newline at end of file +/** Class used to handle the application */ +public class TIC2WebSocketApplication { + /** Project properties (see file project.properties) */ + public static final Properties PROJECT_PROPERTIES = new Properties(); + + static { + try { + InputStream stream = + TIC2WebSocketApplication.class + .getClassLoader() + .getResourceAsStream("TIC2WebSocket.properties"); + + if (stream == null) { + System.err.println("Can't find projet property file!"); + } else { + TIC2WebSocketApplication.PROJECT_PROPERTIES.load(stream); + } + } catch (Exception exception) { + System.err.println("Can't load projet property file!"); + exception.printStackTrace(System.err); + } + } + + /** Project name ("project_name" from project.properties) */ + public static final String NAME = PROJECT_PROPERTIES.getProperty("project_name", ""); + + /** Project version ("project_version" from project.properties) */ + public static final String VERSION = PROJECT_PROPERTIES.getProperty("project_version", ""); + + /** Project description ("project_description" from project.properties) */ + public static final String DESCRIPTION = + PROJECT_PROPERTIES.getProperty("project_description", ""); + + /** + * Program entry point + * + * @param args Command line arguments + */ + public static void main(String[] args) { + try { + TIC2WebSocketApplication application = new TIC2WebSocketApplication(args); + int result = application.run(); + System.exit(result); + } catch (Exception exception) { + exception.printStackTrace(System.err); + System.exit(-1); + } + } + + private String[] commandLineArgs; + private CommandLine commandLineParser; + private TIC2WebSocketCommandLine commandLine; + private Logger logger; + private boolean started; + private TIC2WebSocketConfiguration configuration; + + private TIC2WebSocketClientPool clientPool; + private TIC2WebSocketServer server; + private TIC2WebSocketRequestHandler requestHandler; + private TICCore ticCore; + + /** + * Application constructor + * + * @param args Command line arguments + */ + public TIC2WebSocketApplication(String[] args) { + this.commandLineArgs = args; + this.commandLine = new TIC2WebSocketCommandLine(); + this.commandLineParser = new CommandLine(this.commandLine); + this.commandLineParser.setCommandName(TIC2WebSocketApplication.NAME); + this.commandLineParser + .getCommandSpec() + .versionProvider( + new IVersionProvider() { + @Override + public String[] getVersion() throws Exception { + return new String[] { + TIC2WebSocketApplication.NAME + " v" + TIC2WebSocketApplication.VERSION + }; + } + }); + this.commandLineParser.registerConverter( + Level.class, new TIC2WebSocketCommandLine.VerboseLevelConverter()); + this.updateLoggerConfiguration(Level.ERROR); + this.started = false; + } + + /** + * Application execution + * + * @return 0 if success, else an error code + * @throws InterruptedException If the application thread gets interrupted + * @see TIC2WebSocketApplicationErrorCode + */ + public int run() throws InterruptedException { + int result = this.init(); + + if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) { + return result; + } + result = this.start(); + if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) { + return result; + } + + Runtime.getRuntime() + .addShutdownHook( + new Thread( + new Runnable() { + @Override + public void run() { + TIC2WebSocketApplication.this.stop(); + } + })); + + while (this.isStarted()) { + Thread.sleep(1000); + } + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + /** + * Application initialization + * + * @return 0 if success, else an error code + * @see TIC2WebSocketApplicationErrorCode + */ + public int init() { + if (this.isStarted()) { + this.logger.error("Application already started!"); + return TIC2WebSocketApplicationErrorCode.APPLICATION_ALREADY_STARTED.code(); + } + int result = this.parseCommandLine(); + if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) { + return result; + } + result = this.updateLoggerConfiguration(this.commandLine.verboseLevel); + if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) { + return result; + } + + result = this.loadConfiguration(); + if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) { + return result; + } + + this.logger.info(TIC2WebSocketApplication.NAME + " initialized"); + + this.ticCore = + new TICCoreBase(this.configuration.getTicMode(), this.configuration.getTicPortNames()); + this.clientPool = new TIC2WebSocketClientPoolBase(); + this.requestHandler = new TIC2WebSocketRequestHandlerBase(this.ticCore); + + this.server = + new TIC2WebSocketServer( + "localhost", + this.configuration.getServerPort().intValue(), + this.clientPool, + this.requestHandler); + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + /** + * Application start-up + * + * @return 0 if success, else an error code + */ + public int start() { + if (this.commandLineParser.isUsageHelpRequested()) { + this.commandLineParser.usage(System.out); + } + if (this.commandLineParser.isVersionHelpRequested()) { + this.commandLineParser.printVersionHelp(System.out); + } + this.logger.info(TIC2WebSocketApplication.NAME + " starting"); + + try { + this.ticCore.start(); + this.server.start(); + } catch (Exception exception) { + this.logger.error(exception.getMessage(), exception); + return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); + } + + this.logger.info(TIC2WebSocketApplication.NAME + " started"); + this.started = true; + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + /** + * Application start-up indicator + * + * @return true if the application is started, false otherwise + */ + public boolean isStarted() { + return this.started; + } + + /** + * Application stop + * + * @return 0 if success, else an error code + * @see TIC2WebSocketApplicationErrorCode + */ + public int stop() { + if (!this.isStarted()) { + this.logger.error("Application not started!"); + return TIC2WebSocketApplicationErrorCode.APPLICATION_NOT_STARTED.code(); + } + + try { + this.server.stop(); + this.ticCore.stop(); + } catch (Exception exception) { + this.logger.error(exception.getMessage(), exception); + return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); + } + + this.logger.info(TIC2WebSocketApplication.NAME + " stopped"); + this.started = false; + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + private int parseCommandLine() { + try { + ParseResult parseResult = this.commandLineParser.parseArgs(this.commandLineArgs); + if (parseResult.errors().size() > 0) { + this.logger.error("Invalid command line!"); + for (int i = 0; i < parseResult.errors().size(); i++) { + this.logger.error(parseResult.errors().get(i).getMessage()); + } + return TIC2WebSocketApplicationErrorCode.COMMAND_LINE_INVALID.code(); + } + } catch (Exception exception) { + this.logger.error("Invalid command line!"); + this.logger.error(exception.getMessage()); + return TIC2WebSocketApplicationErrorCode.COMMAND_LINE_INVALID.code(); + } + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + private int updateLoggerConfiguration(Level level) { + try { + System.setProperty( + "log4j.configurationFile", "log4j2-" + level.name().toLowerCase() + ".xml"); + LoggerContext context = (LoggerContext) LogManager.getContext(false); + context.reconfigure(); + this.logger = LogManager.getLogger(); + } catch (Exception exception) { + System.err.println("Log configuration update failure!"); + System.err.println(exception.getMessage()); + return TIC2WebSocketApplicationErrorCode.UPDATE_LOGGER_CONFIGURATION_FAILURE.code(); + } + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } + + private int loadConfiguration() { + String configFile = null; + + if (this.commandLine.configFile != null) { + configFile = this.commandLine.configFile; + } else { + configFile = System.getProperty("configFile", NAME + "Configuration.json"); + } + File configPath = new File(configFile); + if (!configPath.exists()) { + this.logger.error( + "No configuration file path '" + configPath.getAbsolutePath() + "' not found"); + return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); + } + try { + this.logger.info("Loading configuration file " + configPath); + this.configuration = + (TIC2WebSocketConfiguration) + DataDictionaryBase.fromFile(configPath, TIC2WebSocketConfiguration.class); + } catch (Exception exception) { + this.logger.error("Loading configuration file " + configPath + " failed", exception); + return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); + } + + return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); + } +} diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java b/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java index c2b970c..a93b70d 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java +++ b/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java @@ -7,39 +7,33 @@ package enedis.tic.service; -/** - * Application error codes list - */ -public enum TIC2WebSocketApplicationErrorCode -{ - /** Success code */ - NO_ERROR(0), - /** Command line invalid code */ - COMMAND_LINE_INVALID(1), - /** Update logger configuration code */ - UPDATE_LOGGER_CONFIGURATION_FAILURE(2), - /** Application already started code */ - APPLICATION_ALREADY_STARTED(3), - /** Application not started code */ - APPLICATION_NOT_STARTED(4), - /** Load configuration failure code */ - LOAD_CONFIGURATION_FAILURE(5); +/** Application error codes list */ +public enum TIC2WebSocketApplicationErrorCode { + /** Success code */ + NO_ERROR(0), + /** Command line invalid code */ + COMMAND_LINE_INVALID(1), + /** Update logger configuration code */ + UPDATE_LOGGER_CONFIGURATION_FAILURE(2), + /** Application already started code */ + APPLICATION_ALREADY_STARTED(3), + /** Application not started code */ + APPLICATION_NOT_STARTED(4), + /** Load configuration failure code */ + LOAD_CONFIGURATION_FAILURE(5); - private int code; + private int code; - private TIC2WebSocketApplicationErrorCode(int code) - { - this.code = code; - } - - /** - * Return error code - * - * @return code - */ - public int code() - { - return this.code; - } + private TIC2WebSocketApplicationErrorCode(int code) { + this.code = code; + } + /** + * Return error code + * + * @return code + */ + public int code() { + return this.code; + } } diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java b/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java index 8de4f88..66a8cfe 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java +++ b/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java @@ -8,78 +8,80 @@ package enedis.tic.service; import org.apache.logging.log4j.Level; - import picocli.CommandLine.Command; import picocli.CommandLine.ITypeConverter; import picocli.CommandLine.Option; import picocli.CommandLine.TypeConversionException; -/** - * Class used for command line - */ +/** Class used for command line */ @Command() -public class TIC2WebSocketCommandLine -{ - static class VerboseLevelConverter implements ITypeConverter - { - @Override - public Level convert(String value) throws Exception - { - try - { - switch (Integer.parseInt(value)) - { - case 0: - return Level.OFF; - case 1: - return Level.FATAL; - case 2: - return Level.ERROR; - case 3: - return Level.WARN; - case 4: - return Level.INFO; - case 5: - return Level.DEBUG; - case 6: - return Level.TRACE; - default: - throw new TypeConversionException(value); - } - } - catch (NumberFormatException exception) - { - Level level = Level.getLevel(value); - if (level == null) - { - throw new TypeConversionException(value); - } - return level; - } - } - } +public class TIC2WebSocketCommandLine { + static class VerboseLevelConverter implements ITypeConverter { + @Override + public Level convert(String value) throws Exception { + try { + switch (Integer.parseInt(value)) { + case 0: + return Level.OFF; + case 1: + return Level.FATAL; + case 2: + return Level.ERROR; + case 3: + return Level.WARN; + case 4: + return Level.INFO; + case 5: + return Level.DEBUG; + case 6: + return Level.TRACE; + default: + throw new TypeConversionException(value); + } + } catch (NumberFormatException exception) { + Level level = Level.getLevel(value); + if (level == null) { + throw new TypeConversionException(value); + } + return level; + } + } + } - @Option(names = { "-h", "--help" }, usageHelp = true, description = "Display help") - boolean helpRequested = false; + @Option( + names = {"-h", "--help"}, + usageHelp = true, + description = "Display help") + boolean helpRequested = false; - @Option(names = { "--version" }, versionHelp = true, description = "Display version") - boolean versionRequested = false; + @Option( + names = {"--version"}, + versionHelp = true, + description = "Display version") + boolean versionRequested = false; - //@formatter:off - @Option(names = { "-v", - "--verbose" }, defaultValue = "2", paramLabel = "LEVEL", - description = "Define verbosity level:\n" + - "0 ou OFF = muted\n" + - "1 ou FATAL = critical errors\n" + - "2 ou ERROR = error (default)\n" + - "3 ou WARN = warnings\n" + - "4 ou INFO = informations\n" + - "5 ou DEBUG = debugging\n" + - "6 ou TRACE = traces\n") - Level verboseLevel; - //@formatter:on + // @formatter:off + @Option( + names = {"-v", "--verbose"}, + defaultValue = "2", + paramLabel = "LEVEL", + description = + "Define verbosity level:\n" + + "0 ou OFF = muted\n" + + "1 ou FATAL = critical errors\n" + + "2 ou ERROR = error (default)\n" + + "3 ou WARN = warnings\n" + + "4 ou INFO = informations\n" + + "5 ou DEBUG = debugging\n" + + "6 ou TRACE = traces\n") + Level verboseLevel; - @Option(names = { "--configFile" }, paramLabel = "PATH", description = "Set configuration file") - String configFile = null; + // @formatter:on -}; + @Option( + names = {"--configFile"}, + paramLabel = "PATH", + description = "Set configuration file") + String configFile = null; +} +; diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java index 8cd94ca..e61724d 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,13 +7,6 @@ package enedis.tic.service.client; -import java.time.LocalDateTime; - -import io.netty.channel.Channel; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import enedis.lab.types.DataDictionaryException; import enedis.lab.util.message.Event; import enedis.tic.core.TICCoreError; @@ -22,65 +15,56 @@ import enedis.tic.service.endpoint.EventSender; import enedis.tic.service.message.EventOnError; import enedis.tic.service.message.EventOnTICData; +import io.netty.channel.Channel; +import java.time.LocalDateTime; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; -/** - * TIC2WebSocket client - */ -public class TIC2WebSocketClient implements TICCoreSubscriber -{ - private Logger logger; - private Channel channel; - private EventSender eventSender; - - /** - * Constructor - * - * @param channel - * @param eventSender - */ - public TIC2WebSocketClient(Channel channel, EventSender eventSender) { - super(); - this.logger = LogManager.getLogger(this.getClass()); - this.channel = channel; - this.eventSender = eventSender; - } +/** TIC2WebSocket client */ +public class TIC2WebSocketClient implements TICCoreSubscriber { + private Logger logger; + private Channel channel; + private EventSender eventSender; - @Override - public void onData(TICCoreFrame frame) - { - try - { - Event event = new EventOnTICData(LocalDateTime.now(), frame); - this.eventSender.sendEvent(this.channel, event); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } + /** + * Constructor + * + * @param channel + * @param eventSender + */ + public TIC2WebSocketClient(Channel channel, EventSender eventSender) { + super(); + this.logger = LogManager.getLogger(this.getClass()); + this.channel = channel; + this.eventSender = eventSender; + } - @Override - public void onError(TICCoreError error) - { - try - { - Event event = new EventOnError(LocalDateTime.now(), error); - this.eventSender.sendEvent(this.channel, event); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } + @Override + public void onData(TICCoreFrame frame) { + try { + Event event = new EventOnTICData(LocalDateTime.now(), frame); + this.eventSender.sendEvent(this.channel, event); + } catch (DataDictionaryException e) { + this.logger.error(e.getMessage(), e); + } + } - /** - * Get client websocket channel - * - * @return websocket channel - */ - public Channel getChannel() - { - return this.channel; - } + @Override + public void onError(TICCoreError error) { + try { + Event event = new EventOnError(LocalDateTime.now(), error); + this.eventSender.sendEvent(this.channel, event); + } catch (DataDictionaryException e) { + this.logger.error(e.getMessage(), e); + } + } + /** + * Get client websocket channel + * + * @return websocket channel + */ + public Channel getChannel() { + return this.channel; + } } diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java index 1518ae4..15f2895 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java @@ -7,46 +7,41 @@ package enedis.tic.service.client; -import java.util.Optional; - -import io.netty.channel.Channel; - import enedis.tic.service.endpoint.EventSender; +import io.netty.channel.Channel; +import java.util.Optional; -/** - * TIC2WebSocket client pool interface - */ -public interface TIC2WebSocketClientPool -{ - /** - * Get client from channel id - * - * @param channelId - * @return client - */ - public Optional getClient(String channelId); - - /** - * Check if a client with the given channel id exists - * - * @param channelId - * @return true if a client with the given channel id exists - */ - public boolean exists(String channelId); - - /** - * Create a new client - * - * @param channel - * @param sender - * @return new client - */ - public TIC2WebSocketClient createClient(Channel channel, EventSender sender); - - /** - * Remove client with the given channel id - * - * @param channelId - */ - public void remove(String channelId); +/** TIC2WebSocket client pool interface */ +public interface TIC2WebSocketClientPool { + /** + * Get client from channel id + * + * @param channelId + * @return client + */ + public Optional getClient(String channelId); + + /** + * Check if a client with the given channel id exists + * + * @param channelId + * @return true if a client with the given channel id exists + */ + public boolean exists(String channelId); + + /** + * Create a new client + * + * @param channel + * @param sender + * @return new client + */ + public TIC2WebSocketClient createClient(Channel channel, EventSender sender); + + /** + * Remove client with the given channel id + * + * @param channelId + */ + public void remove(String channelId); } diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java index d777c35..efcb142 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java @@ -7,79 +7,63 @@ package enedis.tic.service.client; +import enedis.tic.service.endpoint.EventSender; +import io.netty.channel.Channel; import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; -import io.netty.channel.Channel; - -import enedis.tic.service.endpoint.EventSender; - -/** - * Client pool - */ -public class TIC2WebSocketClientPoolBase implements TIC2WebSocketClientPool -{ - private Set clients; +/** Client pool */ +public class TIC2WebSocketClientPoolBase implements TIC2WebSocketClientPool { + private Set clients; - /** - * Default constructor - */ - public TIC2WebSocketClientPoolBase() - { - super(); - this.clients = new CopyOnWriteArraySet(); - } + /** Default constructor */ + public TIC2WebSocketClientPoolBase() { + super(); + this.clients = new CopyOnWriteArraySet(); + } - @Override - public Optional getClient(String channelId) - { - // @formatter:off - return this.clients.stream() - .filter(c -> c.getChannel().id().asLongText().equals(channelId)) - .findAny(); - // @formatter:on - } + @Override + public Optional getClient(String channelId) { + // @formatter:off + return this.clients.stream() + .filter(c -> c.getChannel().id().asLongText().equals(channelId)) + .findAny(); + // @formatter:on + } - @Override - public boolean exists(String channelId) - { - return this.getClient(channelId).isPresent(); - } + @Override + public boolean exists(String channelId) { + return this.getClient(channelId).isPresent(); + } - @Override - public TIC2WebSocketClient createClient(Channel channel, EventSender sender) - { - this.checkArguments(channel, sender); + @Override + public TIC2WebSocketClient createClient(Channel channel, EventSender sender) { + this.checkArguments(channel, sender); - String channelId = channel.id().asLongText(); - Optional client = this.getClient(channelId); - if (!client.isPresent()) - { - TIC2WebSocketClient newClient = new TIC2WebSocketClient(channel, sender); - this.clients.add(newClient); - return newClient; - } - else - { - return client.get(); - } - } + String channelId = channel.id().asLongText(); + Optional client = this.getClient(channelId); + if (!client.isPresent()) { + TIC2WebSocketClient newClient = new TIC2WebSocketClient(channel, sender); + this.clients.add(newClient); + return newClient; + } else { + return client.get(); + } + } - @Override - public void remove(String channelId) - { - Optional client = this.getClient(channelId); - if (client.isPresent()) - { - this.clients.remove(client.get()); - } - } + @Override + public void remove(String channelId) { + Optional client = this.getClient(channelId); + if (client.isPresent()) { + this.clients.remove(client.get()); + } + } - private void checkArguments(Channel channel, EventSender sender) - { - if (channel == null || sender == null) { - throw new IllegalArgumentException("Cannot create client with a null Channel or null EventSender"); - } - } + private void checkArguments(Channel channel, EventSender sender) { + if (channel == null || sender == null) { + throw new IllegalArgumentException( + "Cannot create client with a null Channel or null EventSender"); + } + } } diff --git a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java index a8e530e..9d7a13c 100644 --- a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java +++ b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,11 +7,6 @@ package enedis.tic.service.config; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.protocol.tic.TICMode; import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; @@ -20,208 +15,209 @@ import enedis.lab.types.datadictionary.KeyDescriptorEnum; import enedis.lab.types.datadictionary.KeyDescriptorListMinMaxSize; import enedis.lab.types.datadictionary.KeyDescriptorNumberMinMax; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * TIC2WebSocketConfiguration class * - * Generated + *

    Generated */ -public class TIC2WebSocketConfiguration extends ConfigurationBase -{ - protected static final String KEY_SERVER_PORT = "serverPort"; - protected static final String KEY_TIC_MODE = "ticMode"; - protected static final String KEY_TIC_PORT_NAMES = "ticPortNames"; - - private static final Number SERVER_PORT_MIN = 1; - private static final Number SERVER_PORT_MAX = 65535; - private static final int TIC_PORT_NAMES_MIN_SIZE = 1; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorNumberMinMax kServerPort; - protected KeyDescriptorEnum kTicMode; - protected KeyDescriptorListMinMaxSize kTicPortNames; - - protected TIC2WebSocketConfiguration() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TIC2WebSocketConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TIC2WebSocketConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public TIC2WebSocketConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param serverPort - * @param ticMode - * @param ticPortNames - * @throws DataDictionaryException - */ - public TIC2WebSocketConfiguration(Number serverPort, TICMode ticMode, List ticPortNames) throws DataDictionaryException - { - this(); - - this.setServerPort(serverPort); - this.setTicMode(ticMode); - this.setTicPortNames(ticPortNames); - - this.checkAndUpdate(); - } - - /** - * Get server port - * - * @return the server port - */ - public Number getServerPort() - { - return (Number) this.data.get(KEY_SERVER_PORT); - } - - /** - * Get tic mode - * - * @return the tic mode - */ - public TICMode getTicMode() - { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Get tic port names - * - * @return the tic port names - */ - @SuppressWarnings("unchecked") - public List getTicPortNames() - { - return (List) this.data.get(KEY_TIC_PORT_NAMES); - } - - /** - * Set server port - * - * @param serverPort - * @throws DataDictionaryException - */ - public void setServerPort(Number serverPort) throws DataDictionaryException - { - this.setServerPort((Object) serverPort); - } - - /** - * Set tic mode - * - * @param ticMode - * @throws DataDictionaryException - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException - { - this.setTicMode((Object) ticMode); - } - - /** - * Set tic port names - * - * @param ticPortNames - * @throws DataDictionaryException - */ - public void setTicPortNames(List ticPortNames) throws DataDictionaryException - { - this.setTicPortNames((Object) ticPortNames); - } - - protected void setServerPort(Object serverPort) throws DataDictionaryException - { - this.data.put(KEY_SERVER_PORT, this.kServerPort.convert(serverPort)); - } - - protected void setTicMode(Object ticMode) throws DataDictionaryException - { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - } - - protected void setTicPortNames(Object ticPortNames) throws DataDictionaryException - { - List portNames = this.kTicPortNames.convert(ticPortNames); - for (int i = 0; i < portNames.size(); i++) - { - String portName = portNames.get(i); - if (portName == null) - { - throw new DataDictionaryException("Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + "(" + portName + ") cannot be null"); - } - else if (portName.isEmpty()) - { - throw new DataDictionaryException("Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + "(" + portName + ") cannot be empty"); - } - else if (i != portNames.lastIndexOf(portName)) - { - throw new DataDictionaryException( - "Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + " and " + portNames.lastIndexOf(portName) + "(" + portName + ") are identical"); - } - } - this.data.put(KEY_TIC_PORT_NAMES, portNames); - } - - private void loadKeyDescriptors() - { - try - { - this.kServerPort = new KeyDescriptorNumberMinMax(KEY_SERVER_PORT, true, SERVER_PORT_MIN, SERVER_PORT_MAX); - this.keys.add(this.kServerPort); - - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, false, TICMode.class); - this.keys.add(this.kTicMode); - - this.kTicPortNames = new KeyDescriptorListMinMaxSize(KEY_TIC_PORT_NAMES, false, String.class, TIC_PORT_NAMES_MIN_SIZE, null); - this.keys.add(this.kTicPortNames); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class TIC2WebSocketConfiguration extends ConfigurationBase { + protected static final String KEY_SERVER_PORT = "serverPort"; + protected static final String KEY_TIC_MODE = "ticMode"; + protected static final String KEY_TIC_PORT_NAMES = "ticPortNames"; + + private static final Number SERVER_PORT_MIN = 1; + private static final Number SERVER_PORT_MAX = 65535; + private static final int TIC_PORT_NAMES_MIN_SIZE = 1; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorNumberMinMax kServerPort; + protected KeyDescriptorEnum kTicMode; + protected KeyDescriptorListMinMaxSize kTicPortNames; + + protected TIC2WebSocketConfiguration() { + super(); + this.loadKeyDescriptors(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public TIC2WebSocketConfiguration(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public TIC2WebSocketConfiguration(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting configuration name/file and parameters to default values + * + * @param name the configuration name + * @param file the configuration file + */ + public TIC2WebSocketConfiguration(String name, File file) { + this(); + this.init(name, file); + } + + /** + * Constructor setting parameters to specific values + * + * @param serverPort + * @param ticMode + * @param ticPortNames + * @throws DataDictionaryException + */ + public TIC2WebSocketConfiguration(Number serverPort, TICMode ticMode, List ticPortNames) + throws DataDictionaryException { + this(); + + this.setServerPort(serverPort); + this.setTicMode(ticMode); + this.setTicPortNames(ticPortNames); + + this.checkAndUpdate(); + } + + /** + * Get server port + * + * @return the server port + */ + public Number getServerPort() { + return (Number) this.data.get(KEY_SERVER_PORT); + } + + /** + * Get tic mode + * + * @return the tic mode + */ + public TICMode getTicMode() { + return (TICMode) this.data.get(KEY_TIC_MODE); + } + + /** + * Get tic port names + * + * @return the tic port names + */ + @SuppressWarnings("unchecked") + public List getTicPortNames() { + return (List) this.data.get(KEY_TIC_PORT_NAMES); + } + + /** + * Set server port + * + * @param serverPort + * @throws DataDictionaryException + */ + public void setServerPort(Number serverPort) throws DataDictionaryException { + this.setServerPort((Object) serverPort); + } + + /** + * Set tic mode + * + * @param ticMode + * @throws DataDictionaryException + */ + public void setTicMode(TICMode ticMode) throws DataDictionaryException { + this.setTicMode((Object) ticMode); + } + + /** + * Set tic port names + * + * @param ticPortNames + * @throws DataDictionaryException + */ + public void setTicPortNames(List ticPortNames) throws DataDictionaryException { + this.setTicPortNames((Object) ticPortNames); + } + + protected void setServerPort(Object serverPort) throws DataDictionaryException { + this.data.put(KEY_SERVER_PORT, this.kServerPort.convert(serverPort)); + } + + protected void setTicMode(Object ticMode) throws DataDictionaryException { + this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); + } + + protected void setTicPortNames(Object ticPortNames) throws DataDictionaryException { + List portNames = this.kTicPortNames.convert(ticPortNames); + for (int i = 0; i < portNames.size(); i++) { + String portName = portNames.get(i); + if (portName == null) { + throw new DataDictionaryException( + "Key " + + KEY_TIC_PORT_NAMES + + ": value at index " + + i + + "(" + + portName + + ") cannot be null"); + } else if (portName.isEmpty()) { + throw new DataDictionaryException( + "Key " + + KEY_TIC_PORT_NAMES + + ": value at index " + + i + + "(" + + portName + + ") cannot be empty"); + } else if (i != portNames.lastIndexOf(portName)) { + throw new DataDictionaryException( + "Key " + + KEY_TIC_PORT_NAMES + + ": value at index " + + i + + " and " + + portNames.lastIndexOf(portName) + + "(" + + portName + + ") are identical"); + } + } + this.data.put(KEY_TIC_PORT_NAMES, portNames); + } + + private void loadKeyDescriptors() { + try { + this.kServerPort = + new KeyDescriptorNumberMinMax(KEY_SERVER_PORT, true, SERVER_PORT_MIN, SERVER_PORT_MAX); + this.keys.add(this.kServerPort); + + this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, false, TICMode.class); + this.keys.add(this.kTicMode); + + this.kTicPortNames = + new KeyDescriptorListMinMaxSize( + KEY_TIC_PORT_NAMES, false, String.class, TIC_PORT_NAMES_MIN_SIZE, null); + this.keys.add(this.kTicPortNames); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/endpoint/EventSender.java b/src/main/java/enedis/tic/service/endpoint/EventSender.java index 2437707..37b90d7 100644 --- a/src/main/java/enedis/tic/service/endpoint/EventSender.java +++ b/src/main/java/enedis/tic/service/endpoint/EventSender.java @@ -7,20 +7,16 @@ package enedis.tic.service.endpoint; -import io.netty.channel.Channel; - import enedis.lab.util.message.Event; +import io.netty.channel.Channel; -/** - * Event sender interface - */ -public interface EventSender -{ - /** - * Send event - * - * @param channel - * @param event - */ - public void sendEvent(Channel channel, Event event); +/** Event sender interface */ +public interface EventSender { + /** + * Send event + * + * @param channel + * @param event + */ + public void sendEvent(Channel channel, Event event); } diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java index 5eca674..f6bc8f9 100644 --- a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java +++ b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java @@ -7,51 +7,45 @@ package enedis.tic.service.endpoint; -/** - * End point error codes list - */ -public enum TIC2WebSocketEndPointErrorCode -{ - /** Success code */ - NO_ERROR(0), - /** Unvalid message */ - INVALID_MESSAGE_FORMAT(-1), - /** Message type missing */ - MESSAGE_TYPE_MISSING(-2), - /** Message name missing */ - MESSAGE_NAME_MISSING(-3), - /** Message type invalid */ - MESSAGE_TYPE_INVALID(-4), - /** Unsupported message */ - UNSUPPORTED_MESSAGE(-5), - /** Invalid messge content */ - INVALID_MESSAGE_CONTENT(-6), - /** Internal error */ - INTERNAL_ERROR(-7), - /** Subscription fail */ - SUBSCRIPTION_FAIL(-10), - /** Unsubscription fail */ - UNSUBSCRIPTION_FAIL(-11), - /** TIC identifier not found */ - IDENTIFIER_NOT_FOUND(-12), - /** Read TIC timeout */ - READ_TIMEOUT(-13); +/** End point error codes list */ +public enum TIC2WebSocketEndPointErrorCode { + /** Success code */ + NO_ERROR(0), + /** Unvalid message */ + INVALID_MESSAGE_FORMAT(-1), + /** Message type missing */ + MESSAGE_TYPE_MISSING(-2), + /** Message name missing */ + MESSAGE_NAME_MISSING(-3), + /** Message type invalid */ + MESSAGE_TYPE_INVALID(-4), + /** Unsupported message */ + UNSUPPORTED_MESSAGE(-5), + /** Invalid messge content */ + INVALID_MESSAGE_CONTENT(-6), + /** Internal error */ + INTERNAL_ERROR(-7), + /** Subscription fail */ + SUBSCRIPTION_FAIL(-10), + /** Unsubscription fail */ + UNSUBSCRIPTION_FAIL(-11), + /** TIC identifier not found */ + IDENTIFIER_NOT_FOUND(-12), + /** Read TIC timeout */ + READ_TIMEOUT(-13); - private int value; + private int value; - private TIC2WebSocketEndPointErrorCode(int value) - { - this.value = value; - } - - /** - * Return error code - * - * @return code - */ - public int value() - { - return this.value; - } + private TIC2WebSocketEndPointErrorCode(int value) { + this.value = value; + } + /** + * Return error code + * + * @return code + */ + public int value() { + return this.value; + } } diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java index e114dd3..442fe10 100644 --- a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java +++ b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -9,77 +9,65 @@ /** * @author Antoine - * - */ -/** - * TIC2WebSocket end point Exception */ -public class TIC2WebSocketEndPointException extends Exception -{ - - private static final long serialVersionUID = -2263755971102386572L; +/** TIC2WebSocket end point Exception */ +public class TIC2WebSocketEndPointException extends Exception { - private TIC2WebSocketEndPointErrorCode code; + private static final long serialVersionUID = -2263755971102386572L; - /** - * Default constructor - * - * @param code - */ - public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code) - { - super(); - this.code = code; - } + private TIC2WebSocketEndPointErrorCode code; - /** - * Constructor using message and cause - * - * @param code - * - * @param message - * @param cause - */ - public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, String message, Throwable cause) - { - super(message, cause); - this.code = code; - } + /** + * Default constructor + * + * @param code + */ + public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code) { + super(); + this.code = code; + } - /** - * Constructor using message - * - * @param code - * - * @param message - */ - public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, String message) - { - super(message); - this.code = code; - } + /** + * Constructor using message and cause + * + * @param code + * @param message + * @param cause + */ + public TIC2WebSocketEndPointException( + TIC2WebSocketEndPointErrorCode code, String message, Throwable cause) { + super(message, cause); + this.code = code; + } - /** - * Constructor using cause - * - * @param code - * - * @param cause - */ - public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, Throwable cause) - { - super(cause); - this.code = code; - } + /** + * Constructor using message + * + * @param code + * @param message + */ + public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, String message) { + super(message); + this.code = code; + } - /** - * Get error code - * - * @return error code - */ - public TIC2WebSocketEndPointErrorCode getCode() - { - return this.code; - } + /** + * Constructor using cause + * + * @param code + * @param cause + */ + public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, Throwable cause) { + super(cause); + this.code = code; + } + /** + * Get error code + * + * @return error code + */ + public TIC2WebSocketEndPointErrorCode getCode() { + return this.code; + } } diff --git a/src/main/java/enedis/tic/service/message/EventOnError.java b/src/main/java/enedis/tic/service/message/EventOnError.java index 0c43f6f..7c97d2e 100644 --- a/src/main/java/enedis/tic/service/message/EventOnError.java +++ b/src/main/java/enedis/tic/service/message/EventOnError.java @@ -7,131 +7,117 @@ package enedis.tic.service.message; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; import enedis.lab.util.message.Event; import enedis.tic.core.TICCoreError; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * EventOnError class - * - * Generated + * + *

    Generated */ -public class EventOnError extends Event -{ - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "OnError"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - protected EventOnError() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public EventOnError(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public EventOnError(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param data - * @throws DataDictionaryException - */ - public EventOnError(LocalDateTime dateTime, TICCoreError data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setData(data); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Get data - * - * @return the data - */ - public TICCoreError getData() - { - return (TICCoreError) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICCoreError data) throws DataDictionaryException - { - this.setData((Object) data); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreError.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class EventOnError extends Event { + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "OnError"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + protected EventOnError() { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public EventOnError(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public EventOnError(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param data + * @throws DataDictionaryException + */ + public EventOnError(LocalDateTime dateTime, TICCoreError data) throws DataDictionaryException { + this(); + + this.setDateTime(dateTime); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + public TICCoreError getData() { + return (TICCoreError) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICCoreError data) throws DataDictionaryException { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() { + try { + this.kData = + new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreError.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/message/EventOnTICData.java b/src/main/java/enedis/tic/service/message/EventOnTICData.java index c51e38e..52ed6e1 100644 --- a/src/main/java/enedis/tic/service/message/EventOnTICData.java +++ b/src/main/java/enedis/tic/service/message/EventOnTICData.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,131 +7,117 @@ package enedis.tic.service.message; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; import enedis.lab.util.message.Event; import enedis.tic.core.TICCoreFrame; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * EventOnTICData class - * - * Generated + * + *

    Generated */ -public class EventOnTICData extends Event -{ - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "OnTICData"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - protected EventOnTICData() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public EventOnTICData(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public EventOnTICData(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param data - * @throws DataDictionaryException - */ - public EventOnTICData(LocalDateTime dateTime, TICCoreFrame data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setData(data); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Get data - * - * @return the data - */ - public TICCoreFrame getData() - { - return (TICCoreFrame) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICCoreFrame data) throws DataDictionaryException - { - this.setData((Object) data); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, true, TICCoreFrame.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class EventOnTICData extends Event { + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "OnTICData"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + protected EventOnTICData() { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public EventOnTICData(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public EventOnTICData(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param data + * @throws DataDictionaryException + */ + public EventOnTICData(LocalDateTime dateTime, TICCoreFrame data) throws DataDictionaryException { + this(); + + this.setDateTime(dateTime); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + public TICCoreFrame getData() { + return (TICCoreFrame) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICCoreFrame data) throws DataDictionaryException { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() { + try { + this.kData = + new KeyDescriptorDataDictionary(KEY_DATA, true, TICCoreFrame.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java index 2e342ed..31e2754 100644 --- a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java +++ b/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,68 +7,60 @@ package enedis.tic.service.message; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.util.message.Request; +import java.util.Map; /** * RequestGetAvailableTICs class - * - * Generated + * + *

    Generated */ -public class RequestGetAvailableTICs extends Request -{ - /** Message name */ - public static final String NAME = "GetAvailableTICs"; - - /** - * Default constructor - * - * @throws DataDictionaryException - */ - public RequestGetAvailableTICs() throws DataDictionaryException - { - super(); +public class RequestGetAvailableTICs extends Request { + /** Message name */ + public static final String NAME = "GetAvailableTICs"; - this.kName.setAcceptedValues(NAME); + /** + * Default constructor + * + * @throws DataDictionaryException + */ + public RequestGetAvailableTICs() throws DataDictionaryException { + super(); - this.checkAndUpdate(); - } + this.kName.setAcceptedValues(NAME); - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestGetAvailableTICs(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } + this.checkAndUpdate(); + } - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestGetAvailableTICs(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestGetAvailableTICs(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestGetAvailableTICs(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } } diff --git a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java b/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java index d3a2357..18a4b26 100644 --- a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java +++ b/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java @@ -7,63 +7,55 @@ package enedis.tic.service.message; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.util.message.Request; +import java.util.Map; /** * RequestGetModemsInfo class * - * Generated + *

    Generated */ -public class RequestGetModemsInfo extends Request -{ - /** Message name */ - public static final String NAME = "GetModemsInfo"; - - public RequestGetModemsInfo() throws DataDictionaryException - { - super(); - - this.kName.setAcceptedValues(NAME); - - this.checkAndUpdate(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestGetModemsInfo(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestGetModemsInfo(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - +public class RequestGetModemsInfo extends Request { + /** Message name */ + public static final String NAME = "GetModemsInfo"; + + public RequestGetModemsInfo() throws DataDictionaryException { + super(); + + this.kName.setAcceptedValues(NAME); + + this.checkAndUpdate(); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestGetModemsInfo(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestGetModemsInfo(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } } diff --git a/src/main/java/enedis/tic/service/message/RequestReadTIC.java b/src/main/java/enedis/tic/service/message/RequestReadTIC.java index 0a42a12..45d2d00 100644 --- a/src/main/java/enedis/tic/service/message/RequestReadTIC.java +++ b/src/main/java/enedis/tic/service/message/RequestReadTIC.java @@ -7,128 +7,114 @@ package enedis.tic.service.message; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; import enedis.lab.util.message.Request; import enedis.tic.core.TICIdentifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * RequestReadTIC class - * - * Generated + * + *

    Generated */ -public class RequestReadTIC extends Request -{ - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "ReadTIC"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - protected RequestReadTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestReadTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestReadTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param data - * @throws DataDictionaryException - */ - public RequestReadTIC(TICIdentifier data) throws DataDictionaryException - { - this(); - - this.setData(data); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Get data - * - * @return the data - */ - public TICIdentifier getData() - { - return (TICIdentifier) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICIdentifier data) throws DataDictionaryException - { - this.setData((Object) data); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, true, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class RequestReadTIC extends Request { + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "ReadTIC"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + protected RequestReadTIC() { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestReadTIC(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestReadTIC(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param data + * @throws DataDictionaryException + */ + public RequestReadTIC(TICIdentifier data) throws DataDictionaryException { + this(); + + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + public TICIdentifier getData() { + return (TICIdentifier) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICIdentifier data) throws DataDictionaryException { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() { + try { + this.kData = + new KeyDescriptorDataDictionary(KEY_DATA, true, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java index 0e91d0c..e5a5e08 100644 --- a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,129 +7,114 @@ package enedis.tic.service.message; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorList; import enedis.lab.util.message.Request; import enedis.tic.core.TICIdentifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * RequestSubscribeTIC class - * - * Generated + * + *

    Generated */ -public class RequestSubscribeTIC extends Request -{ - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "SubscribeTIC"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - protected RequestSubscribeTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestSubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestSubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param data - * @throws DataDictionaryException - */ - public RequestSubscribeTIC(List data) throws DataDictionaryException - { - this(); - - this.setData(data); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class RequestSubscribeTIC extends Request { + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "SubscribeTIC"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + protected RequestSubscribeTIC() { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestSubscribeTIC(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestSubscribeTIC(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param data + * @throws DataDictionaryException + */ + public RequestSubscribeTIC(List data) throws DataDictionaryException { + this(); + + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() { + try { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java index c0a9f60..e15be86 100644 --- a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,129 +7,114 @@ package enedis.tic.service.message; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorList; import enedis.lab.util.message.Request; import enedis.tic.core.TICIdentifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * RequestUnsubscribeTIC class - * - * Generated + * + *

    Generated */ -public class RequestUnsubscribeTIC extends Request -{ - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "UnsubscribeTIC"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - protected RequestUnsubscribeTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestUnsubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestUnsubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param data - * @throws DataDictionaryException - */ - public RequestUnsubscribeTIC(List data) throws DataDictionaryException - { - this(); - - this.setData(data); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class RequestUnsubscribeTIC extends Request { + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "UnsubscribeTIC"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + protected RequestUnsubscribeTIC() { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public RequestUnsubscribeTIC(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public RequestUnsubscribeTIC(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param data + * @throws DataDictionaryException + */ + public RequestUnsubscribeTIC(List data) throws DataDictionaryException { + this(); + + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() { + try { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java index 95ef2c3..963ca4a 100644 --- a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java +++ b/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java @@ -7,136 +7,123 @@ package enedis.tic.service.message; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorList; import enedis.lab.util.message.Response; import enedis.tic.core.TICIdentifier; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * ResponseGetAvailableTICs class - * - * Generated + * + *

    Generated */ -public class ResponseGetAvailableTICs extends Response -{ - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "GetAvailableTICs"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - protected ResponseGetAvailableTICs() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseGetAvailableTICs(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseGetAvailableTICs(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseGetAvailableTICs(LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class ResponseGetAvailableTICs extends Response { + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "GetAvailableTICs"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + protected ResponseGetAvailableTICs() { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseGetAvailableTICs(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseGetAvailableTICs(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseGetAvailableTICs( + LocalDateTime dateTime, Number errorCode, String errorMessage, List data) + throws DataDictionaryException { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() { + try { + this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java b/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java index fdb47d0..dea46f5 100644 --- a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java +++ b/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,136 +7,124 @@ package enedis.tic.service.message; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.io.tic.TICPortDescriptor; import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorList; import enedis.lab.util.message.Response; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * ResponseGetModemsInfo class - * - * Generated + * + *

    Generated */ -public class ResponseGetModemsInfo extends Response -{ - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "GetModemsInfo"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - protected ResponseGetModemsInfo() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseGetModemsInfo(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseGetModemsInfo(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseGetModemsInfo(LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICPortDescriptor.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class ResponseGetModemsInfo extends Response { + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "GetModemsInfo"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorList kData; + + protected ResponseGetModemsInfo() { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseGetModemsInfo(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseGetModemsInfo(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseGetModemsInfo( + LocalDateTime dateTime, Number errorCode, String errorMessage, List data) + throws DataDictionaryException { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + @SuppressWarnings("unchecked") + public List getData() { + return (List) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(List data) throws DataDictionaryException { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() { + try { + this.kData = + new KeyDescriptorList(KEY_DATA, false, TICPortDescriptor.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java b/src/main/java/enedis/tic/service/message/ResponseReadTIC.java index 1f3ed8f..326f75c 100644 --- a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java +++ b/src/main/java/enedis/tic/service/message/ResponseReadTIC.java @@ -7,135 +7,123 @@ package enedis.tic.service.message; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; import enedis.lab.util.message.Response; import enedis.tic.core.TICCoreFrame; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * ResponseReadTIC class - * - * Generated + * + *

    Generated */ -public class ResponseReadTIC extends Response -{ - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "ReadTIC"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - protected ResponseReadTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseReadTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseReadTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseReadTIC(LocalDateTime dateTime, Number errorCode, String errorMessage, TICCoreFrame data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Get data - * - * @return the data - */ - public TICCoreFrame getData() - { - return (TICCoreFrame) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICCoreFrame data) throws DataDictionaryException - { - this.setData((Object) data); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreFrame.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } +public class ResponseReadTIC extends Response { + protected static final String KEY_DATA = "data"; + + /** Message name */ + public static final String NAME = "ReadTIC"; + + private List> keys = new ArrayList>(); + + protected KeyDescriptorDataDictionary kData; + + protected ResponseReadTIC() { + super(); + this.loadKeyDescriptors(); + + this.kName.setAcceptedValues(NAME); + } + + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseReadTIC(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } + + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseReadTIC(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } + + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + * @throws DataDictionaryException + */ + public ResponseReadTIC( + LocalDateTime dateTime, Number errorCode, String errorMessage, TICCoreFrame data) + throws DataDictionaryException { + this(); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + this.setData(data); + + this.checkAndUpdate(); + } + + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } + + /** + * Get data + * + * @return the data + */ + public TICCoreFrame getData() { + return (TICCoreFrame) this.data.get(KEY_DATA); + } + + /** + * Set data + * + * @param data + * @throws DataDictionaryException + */ + public void setData(TICCoreFrame data) throws DataDictionaryException { + this.setData((Object) data); + } + + protected void setData(Object data) throws DataDictionaryException { + this.data.put(KEY_DATA, this.kData.convert(data)); + } + + private void loadKeyDescriptors() { + try { + this.kData = + new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreFrame.class); + this.keys.add(this.kData); + + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java index b68500b..93946ab 100644 --- a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,99 +7,88 @@ package enedis.tic.service.message; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.util.message.Response; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * ResponseSubscribeTIC class - * - * Generated + * + *

    Generated */ -public class ResponseSubscribeTIC extends Response -{ - /** Message name */ - public static final String NAME = "SubscribeTIC"; +public class ResponseSubscribeTIC extends Response { + /** Message name */ + public static final String NAME = "SubscribeTIC"; - private List> keys = new ArrayList>(); + private List> keys = new ArrayList>(); - protected ResponseSubscribeTIC() - { - super(); - this.loadKeyDescriptors(); + protected ResponseSubscribeTIC() { + super(); + this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } + this.kName.setAcceptedValues(NAME); + } - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseSubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseSubscribeTIC(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseSubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseSubscribeTIC(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public ResponseSubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(); + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public ResponseSubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) + throws DataDictionaryException { + this(); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); - this.checkAndUpdate(); - } + this.checkAndUpdate(); + } - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } - private void loadKeyDescriptors() - { - try - { + private void loadKeyDescriptors() { + try { - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java index 46eff64..9516f8a 100644 --- a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java @@ -7,99 +7,88 @@ package enedis.tic.service.message; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import enedis.lab.types.DataDictionary; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.KeyDescriptor; import enedis.lab.util.message.Response; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * ResponseUnsubscribeTIC class - * - * Generated + * + *

    Generated */ -public class ResponseUnsubscribeTIC extends Response -{ - /** Message name */ - public static final String NAME = "UnsubscribeTIC"; +public class ResponseUnsubscribeTIC extends Response { + /** Message name */ + public static final String NAME = "UnsubscribeTIC"; - private List> keys = new ArrayList>(); + private List> keys = new ArrayList>(); - protected ResponseUnsubscribeTIC() - { - super(); - this.loadKeyDescriptors(); + protected ResponseUnsubscribeTIC() { + super(); + this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } + this.kName.setAcceptedValues(NAME); + } - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseUnsubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } + /** + * Constructor using map + * + * @param map + * @throws DataDictionaryException + */ + public ResponseUnsubscribeTIC(Map map) throws DataDictionaryException { + this(); + this.copy(fromMap(map)); + } - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseUnsubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } + /** + * Constructor using datadictionary + * + * @param other + * @throws DataDictionaryException + */ + public ResponseUnsubscribeTIC(DataDictionary other) throws DataDictionaryException { + this(); + this.copy(other); + } - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public ResponseUnsubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(); + /** + * Constructor setting parameters to specific values + * + * @param dateTime + * @param errorCode + * @param errorMessage + * @throws DataDictionaryException + */ + public ResponseUnsubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) + throws DataDictionaryException { + this(); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); - this.checkAndUpdate(); - } + this.checkAndUpdate(); + } - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } + @Override + protected void updateOptionalParameters() throws DataDictionaryException { + if (!this.exists(KEY_NAME)) { + this.setName(NAME); + } + super.updateOptionalParameters(); + } - private void loadKeyDescriptors() - { - try - { + private void loadKeyDescriptors() { + try { - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } + this.addAllKeyDescriptor(this.keys); + } catch (DataDictionaryException e) { + throw new RuntimeException(e.getMessage(), e); + } + } } diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java index 834533b..75f460c 100644 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -11,18 +11,12 @@ import enedis.tic.service.message.EventOnError; import enedis.tic.service.message.EventOnTICData; -/** - * TIC2WebSocket request factory - */ -public class TIC2WebSocketEventFactory extends EventFactory -{ - /** - * Default constructor - */ - public TIC2WebSocketEventFactory() - { - super(); - this.addMessageClass(EventOnTICData.NAME, EventOnTICData.class); - this.addMessageClass(EventOnError.NAME, EventOnError.class); - } +/** TIC2WebSocket request factory */ +public class TIC2WebSocketEventFactory extends EventFactory { + /** Default constructor */ + public TIC2WebSocketEventFactory() { + super(); + this.addMessageClass(EventOnTICData.NAME, EventOnTICData.class); + this.addMessageClass(EventOnError.NAME, EventOnError.class); + } } diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java index e501d05..31c639f 100644 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -9,16 +9,13 @@ import enedis.lab.util.message.factory.MessageFactory; -/** - * TIC2WebSocket message factory - */ -public class TIC2WebSocketMessageFactory extends MessageFactory -{ - /** - * Default constructor - */ - public TIC2WebSocketMessageFactory() - { - super(new TIC2WebSocketRequestFactory(), new TIC2WebSocketResponseFactory(), new TIC2WebSocketEventFactory()); - } +/** TIC2WebSocket message factory */ +public class TIC2WebSocketMessageFactory extends MessageFactory { + /** Default constructor */ + public TIC2WebSocketMessageFactory() { + super( + new TIC2WebSocketRequestFactory(), + new TIC2WebSocketResponseFactory(), + new TIC2WebSocketEventFactory()); + } } diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java index e76db79..dc56b3f 100644 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java @@ -14,21 +14,15 @@ import enedis.tic.service.message.RequestSubscribeTIC; import enedis.tic.service.message.RequestUnsubscribeTIC; -/** - * TIC2WebSocket request factory - */ -public class TIC2WebSocketRequestFactory extends RequestFactory -{ - /** - * Default constructor - */ - public TIC2WebSocketRequestFactory() - { - super(); - this.addMessageClass(RequestGetAvailableTICs.NAME, RequestGetAvailableTICs.class); - this.addMessageClass(RequestGetModemsInfo.NAME, RequestGetModemsInfo.class); - this.addMessageClass(RequestReadTIC.NAME, RequestReadTIC.class); - this.addMessageClass(RequestSubscribeTIC.NAME, RequestSubscribeTIC.class); - this.addMessageClass(RequestUnsubscribeTIC.NAME, RequestUnsubscribeTIC.class); - } +/** TIC2WebSocket request factory */ +public class TIC2WebSocketRequestFactory extends RequestFactory { + /** Default constructor */ + public TIC2WebSocketRequestFactory() { + super(); + this.addMessageClass(RequestGetAvailableTICs.NAME, RequestGetAvailableTICs.class); + this.addMessageClass(RequestGetModemsInfo.NAME, RequestGetModemsInfo.class); + this.addMessageClass(RequestReadTIC.NAME, RequestReadTIC.class); + this.addMessageClass(RequestSubscribeTIC.NAME, RequestSubscribeTIC.class); + this.addMessageClass(RequestUnsubscribeTIC.NAME, RequestUnsubscribeTIC.class); + } } diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java index 13390e3..0ae7b41 100644 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java @@ -14,21 +14,15 @@ import enedis.tic.service.message.ResponseSubscribeTIC; import enedis.tic.service.message.ResponseUnsubscribeTIC; -/** - * TIC2WebSocket request factory - */ -public class TIC2WebSocketResponseFactory extends ResponseFactory -{ - /** - * Default constructor - */ - public TIC2WebSocketResponseFactory() - { - super(); - this.addMessageClass(ResponseGetAvailableTICs.NAME, ResponseGetAvailableTICs.class); - this.addMessageClass(ResponseGetModemsInfo.NAME, ResponseGetModemsInfo.class); - this.addMessageClass(ResponseReadTIC.NAME, ResponseReadTIC.class); - this.addMessageClass(ResponseSubscribeTIC.NAME, ResponseSubscribeTIC.class); - this.addMessageClass(ResponseUnsubscribeTIC.NAME, ResponseUnsubscribeTIC.class); - } -} \ No newline at end of file +/** TIC2WebSocket request factory */ +public class TIC2WebSocketResponseFactory extends ResponseFactory { + /** Default constructor */ + public TIC2WebSocketResponseFactory() { + super(); + this.addMessageClass(ResponseGetAvailableTICs.NAME, ResponseGetAvailableTICs.class); + this.addMessageClass(ResponseGetModemsInfo.NAME, ResponseGetModemsInfo.class); + this.addMessageClass(ResponseReadTIC.NAME, ResponseReadTIC.class); + this.addMessageClass(ResponseSubscribeTIC.NAME, ResponseSubscribeTIC.class); + this.addMessageClass(ResponseUnsubscribeTIC.NAME, ResponseUnsubscribeTIC.class); + } +} diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java index fa54692..98c28af 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,6 +7,8 @@ package enedis.tic.service.netty; +import enedis.tic.service.client.TIC2WebSocketClientPool; +import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; @@ -16,53 +18,45 @@ import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; import io.netty.handler.stream.ChunkedWriteHandler; -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; - -/** - * Channel initializer for TIC2WebSocket server - */ -public class TIC2WebSocketChannelInitializer extends ChannelInitializer -{ - private static final String WEBSOCKET_PATH = "/"; - - private final TIC2WebSocketClientPool clientPool; - private final TIC2WebSocketRequestHandler requestHandler; +/** Channel initializer for TIC2WebSocket server */ +public class TIC2WebSocketChannelInitializer extends ChannelInitializer { + private static final String WEBSOCKET_PATH = "/"; - /** - * Constructor - * - * @param clientPool the client pool - * @param requestHandler the request handler - */ - public TIC2WebSocketChannelInitializer(TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) - { - this.clientPool = clientPool; - this.requestHandler = requestHandler; - } + private final TIC2WebSocketClientPool clientPool; + private final TIC2WebSocketRequestHandler requestHandler; - @Override - protected void initChannel(SocketChannel ch) throws Exception - { - ChannelPipeline pipeline = ch.pipeline(); + /** + * Constructor + * + * @param clientPool the client pool + * @param requestHandler the request handler + */ + public TIC2WebSocketChannelInitializer( + TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) { + this.clientPool = clientPool; + this.requestHandler = requestHandler; + } - // HTTP codec - pipeline.addLast(new HttpServerCodec()); + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); - // HTTP object aggregator - pipeline.addLast(new HttpObjectAggregator(65536)); + // HTTP codec + pipeline.addLast(new HttpServerCodec()); - // Chunked write handler for large messages - pipeline.addLast(new ChunkedWriteHandler()); + // HTTP object aggregator + pipeline.addLast(new HttpObjectAggregator(65536)); - // WebSocket compression - pipeline.addLast(new WebSocketServerCompressionHandler()); + // Chunked write handler for large messages + pipeline.addLast(new ChunkedWriteHandler()); - // WebSocket protocol handler - pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true)); + // WebSocket compression + pipeline.addLast(new WebSocketServerCompressionHandler()); - // Custom TIC2WebSocket handler - pipeline.addLast(new TIC2WebSocketHandler(clientPool, requestHandler)); - } + // WebSocket protocol handler + pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true)); + // Custom TIC2WebSocket handler + pipeline.addLast(new TIC2WebSocketHandler(clientPool, requestHandler)); + } } diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java index 5d12ae6..7873292 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java @@ -7,18 +7,6 @@ package enedis.tic.service.netty; -import java.util.List; -import java.util.Optional; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; -import io.netty.handler.codec.http.websocketx.WebSocketFrame; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import enedis.lab.types.DataDictionaryException; import enedis.lab.util.message.Event; import enedis.lab.util.message.Message; @@ -38,230 +26,194 @@ import enedis.tic.service.message.RequestUnsubscribeTIC; import enedis.tic.service.message.factory.TIC2WebSocketMessageFactory; import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import java.util.List; +import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; -/** - * Netty WebSocket handler for TIC2WebSocket - */ -public class TIC2WebSocketHandler extends SimpleChannelInboundHandler implements EventSender -{ - private static final Logger logger = LogManager.getLogger(TIC2WebSocketHandler.class); - - private final TIC2WebSocketClientPool clientPool; - private final TIC2WebSocketRequestHandler requestHandler; - private final TIC2WebSocketMessageFactory factory; - - /** - * Constructor - * - * @param clientPool the client pool - * @param requestHandler the request handler - */ - public TIC2WebSocketHandler(TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) - { - this.clientPool = clientPool; - this.requestHandler = requestHandler; - this.factory = new TIC2WebSocketMessageFactory(); +/** Netty WebSocket handler for TIC2WebSocket */ +public class TIC2WebSocketHandler extends SimpleChannelInboundHandler + implements EventSender { + private static final Logger logger = LogManager.getLogger(TIC2WebSocketHandler.class); + + private final TIC2WebSocketClientPool clientPool; + private final TIC2WebSocketRequestHandler requestHandler; + private final TIC2WebSocketMessageFactory factory; + + /** + * Constructor + * + * @param clientPool the client pool + * @param requestHandler the request handler + */ + public TIC2WebSocketHandler( + TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) { + this.clientPool = clientPool; + this.requestHandler = requestHandler; + this.factory = new TIC2WebSocketMessageFactory(); + } + + @Override + public void sendEvent(Channel channel, Event event) { + this.sendMessage(channel, event); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + Channel channel = ctx.channel(); + String channelId = channel.id().asLongText(); + + logger.info("Open channel " + channelId); + + if (!clientPool.exists(channelId)) { + logger.info("Client create with channel id : " + channelId); + clientPool.createClient(channel, this); + } else { + logger.error("Client with channel id : " + channelId + " already exists ! "); } - @Override - public void sendEvent(Channel channel, Event event) - { - this.sendMessage(channel, event); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + Channel channel = ctx.channel(); + String channelId = channel.id().asLongText(); + + logger.info("Close channel " + channelId); + + if (clientPool.exists(channelId)) { + try { + logger.info("Generate unsubscribe request"); + Request request = new RequestUnsubscribeTIC((List) null); + this.handleRequest(clientPool.getClient(channelId).get(), request); + + logger.info("Remove client with channel id : " + channelId); + clientPool.remove(channelId); + } catch (DataDictionaryException e) { + logger.error("Error during channel close", e); + } + } else { + logger.error("Client with channel id : " + channelId + " doesn't exist ! "); } - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception - { - Channel channel = ctx.channel(); - String channelId = channel.id().asLongText(); - - logger.info("Open channel " + channelId); - - if (!clientPool.exists(channelId)) - { - logger.info("Client create with channel id : " + channelId); - clientPool.createClient(channel, this); - } - else - { - logger.error("Client with channel id : " + channelId + " already exists ! "); - } - - super.channelActive(ctx); - } + super.channelInactive(ctx); + } - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception - { - Channel channel = ctx.channel(); - String channelId = channel.id().asLongText(); - - logger.info("Close channel " + channelId); - - if (clientPool.exists(channelId)) - { - try - { - logger.info("Generate unsubscribe request"); - Request request = new RequestUnsubscribeTIC((List) null); - this.handleRequest(clientPool.getClient(channelId).get(), request); - - logger.info("Remove client with channel id : " + channelId); - clientPool.remove(channelId); - } - catch (DataDictionaryException e) - { - logger.error("Error during channel close", e); - } - } - else - { - logger.error("Client with channel id : " + channelId + " doesn't exist ! "); - } - - super.channelInactive(ctx); - } + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + Channel channel = ctx.channel(); + String channelId = channel.id().asLongText(); - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception - { - Channel channel = ctx.channel(); - String channelId = channel.id().asLongText(); + logger.error("Error on channel " + channelId, cause); - logger.error("Error on channel " + channelId, cause); + ctx.close(); + } - ctx.close(); - } + @Override + protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception { + if (frame instanceof TextWebSocketFrame) { + TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; + String text = textFrame.text(); + Channel channel = ctx.channel(); + String channelId = channel.id().asLongText(); + + logger.info("Message on channel " + channelId); - @Override - protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception - { - if (frame instanceof TextWebSocketFrame) - { - TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; - String text = textFrame.text(); - Channel channel = ctx.channel(); - String channelId = channel.id().asLongText(); - - logger.info("Message on channel " + channelId); - - TIC2WebSocketClient client = this.getClient(channel); - - Optional message = this.getMessage(channel, text); - if (!message.isPresent()) - { - return; - } - - Optional request = this.getRequest(channel, message.get()); - if (!request.isPresent()) - { - return; - } - - Response response = this.handleRequest(client, request.get()); - - this.sendMessage(channel, response); - } - else - { - // Handle other frame types if needed - logger.warn("Unsupported frame type: " + frame.getClass().getSimpleName()); - } + TIC2WebSocketClient client = this.getClient(channel); + + Optional message = this.getMessage(channel, text); + if (!message.isPresent()) { + return; + } + + Optional request = this.getRequest(channel, message.get()); + if (!request.isPresent()) { + return; + } + + Response response = this.handleRequest(client, request.get()); + + this.sendMessage(channel, response); + } else { + // Handle other frame types if needed + logger.warn("Unsupported frame type: " + frame.getClass().getSimpleName()); } + } - private TIC2WebSocketClient getClient(Channel channel) - { - String channelId = channel.id().asLongText(); - Optional clientOpt = clientPool.getClient(channelId); - if (!clientOpt.isPresent()) - { - return clientPool.createClient(channel, this); - } - return clientOpt.get(); + private TIC2WebSocketClient getClient(Channel channel) { + String channelId = channel.id().asLongText(); + Optional clientOpt = clientPool.getClient(channelId); + if (!clientOpt.isPresent()) { + return clientPool.createClient(channel, this); + } + return clientOpt.get(); + } + + private Optional getMessage(Channel channel, String text) { + Message message = null; + TIC2WebSocketEndPointErrorCode errorCode = TIC2WebSocketEndPointErrorCode.NO_ERROR; + String errorMessage = ""; + + try { + message = this.factory.getMessage(text); + } catch (MessageInvalidFormatException e) { + errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_FORMAT; + errorMessage = e.getMessage(); + } catch (MessageInvalidTypeException e) { + errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_INVALID; + errorMessage = e.getMessage(); + } catch (MessageKeyNameDoesntExistException e) { + errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_NAME_MISSING; + errorMessage = e.getMessage(); + } catch (MessageKeyTypeDoesntExistException e) { + errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_MISSING; + errorMessage = e.getMessage(); + } catch (MessageInvalidContentException e) { + errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_CONTENT; + errorMessage = e.getMessage(); + } catch (UnsupportedMessageException e) { + errorCode = TIC2WebSocketEndPointErrorCode.UNSUPPORTED_MESSAGE; + errorMessage = e.getMessage(); } - private Optional getMessage(Channel channel, String text) - { - Message message = null; - TIC2WebSocketEndPointErrorCode errorCode = TIC2WebSocketEndPointErrorCode.NO_ERROR; - String errorMessage = ""; - - try - { - message = this.factory.getMessage(text); - } - catch (MessageInvalidFormatException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_FORMAT; - errorMessage = e.getMessage(); - } - catch (MessageInvalidTypeException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_INVALID; - errorMessage = e.getMessage(); - } - catch (MessageKeyNameDoesntExistException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_NAME_MISSING; - errorMessage = e.getMessage(); - } - catch (MessageKeyTypeDoesntExistException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_MISSING; - errorMessage = e.getMessage(); - } - catch (MessageInvalidContentException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_CONTENT; - errorMessage = e.getMessage(); - } - catch (UnsupportedMessageException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.UNSUPPORTED_MESSAGE; - errorMessage = e.getMessage(); - } - - if (errorCode != TIC2WebSocketEndPointErrorCode.NO_ERROR) - { - logger.error("Error parsing message: " + errorMessage); - // TODO: Send error event - return Optional.empty(); - } - - return Optional.of(message); + if (errorCode != TIC2WebSocketEndPointErrorCode.NO_ERROR) { + logger.error("Error parsing message: " + errorMessage); + // TODO: Send error event + return Optional.empty(); } - private Optional getRequest(Channel channel, Message message) - { - if (!(message instanceof Request)) - { - logger.error("Message is not a request"); - // TODO: Send error event - return Optional.empty(); - } + return Optional.of(message); + } - return Optional.of((Request) message); + private Optional getRequest(Channel channel, Message message) { + if (!(message instanceof Request)) { + logger.error("Message is not a request"); + // TODO: Send error event + return Optional.empty(); } - private Response handleRequest(TIC2WebSocketClient client, Request request) - { - return requestHandler.handle(request, client); - } + return Optional.of((Request) message); + } + + private Response handleRequest(TIC2WebSocketClient client, Request request) { + return requestHandler.handle(request, client); + } + + private void sendMessage(Channel channel, Message message) { + try { + String json = message.toJSON().toString(); + TextWebSocketFrame frame = new TextWebSocketFrame(json); + channel.writeAndFlush(frame); - private void sendMessage(Channel channel, Message message) - { - try - { - String json = message.toJSON().toString(); - TextWebSocketFrame frame = new TextWebSocketFrame(json); - channel.writeAndFlush(frame); - - logger.debug("Sent message to channel {}: {}", channel.id().asLongText(), json); - } - catch (Exception e) - { - logger.error("Error sending message to channel " + channel.id().asLongText(), e); - } + logger.debug("Sent message to channel {}: {}", channel.id().asLongText(), json); + } catch (Exception e) { + logger.error("Error sending message to channel " + channel.id().asLongText(), e); } + } } diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java index 5a1919e..9bb4b0b 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,6 +7,8 @@ package enedis.tic.service.netty; +import enedis.tic.service.client.TIC2WebSocketClientPool; +import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; @@ -15,123 +17,102 @@ import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; - -/** - * TIC2WebSocket Netty WebSocket Server - */ -public class TIC2WebSocketServer -{ - private static final Logger logger = LogManager.getLogger(TIC2WebSocketServer.class); - - private final String host; - private final int port; - private final TIC2WebSocketClientPool clientPool; - private final TIC2WebSocketRequestHandler requestHandler; - - private EventLoopGroup bossGroup; - private EventLoopGroup workerGroup; - private Channel serverChannel; - - /** - * Constructor - * - * @param host the host to bind to - * @param port the port to bind to - * @param clientPool the client pool - * @param requestHandler the request handler - */ - public TIC2WebSocketServer(String host, int port, TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) - { - this.host = host; - this.port = port; - this.clientPool = clientPool; - this.requestHandler = requestHandler; +/** TIC2WebSocket Netty WebSocket Server */ +public class TIC2WebSocketServer { + private static final Logger logger = LogManager.getLogger(TIC2WebSocketServer.class); + + private final String host; + private final int port; + private final TIC2WebSocketClientPool clientPool; + private final TIC2WebSocketRequestHandler requestHandler; + + private EventLoopGroup bossGroup; + private EventLoopGroup workerGroup; + private Channel serverChannel; + + /** + * Constructor + * + * @param host the host to bind to + * @param port the port to bind to + * @param clientPool the client pool + * @param requestHandler the request handler + */ + public TIC2WebSocketServer( + String host, + int port, + TIC2WebSocketClientPool clientPool, + TIC2WebSocketRequestHandler requestHandler) { + this.host = host; + this.port = port; + this.clientPool = clientPool; + this.requestHandler = requestHandler; + } + + /** + * Start the WebSocket server + * + * @throws Exception if server startup fails + */ + public void start() throws Exception { + logger.info("Starting TIC2WebSocket Netty server on {}:{}", host, port); + + bossGroup = new NioEventLoopGroup(1); + workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap + .group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new TIC2WebSocketChannelInitializer(clientPool, requestHandler)); + + ChannelFuture future = bootstrap.bind(host, port).sync(); + serverChannel = future.channel(); + + logger.info("TIC2WebSocket Netty server started successfully on {}:{}", host, port); + } catch (Exception e) { + logger.error("Failed to start TIC2WebSocket Netty server", e); + stop(); + throw e; } - - /** - * Start the WebSocket server - * - * @throws Exception if server startup fails - */ - public void start() throws Exception - { - logger.info("Starting TIC2WebSocket Netty server on {}:{}", host, port); - - bossGroup = new NioEventLoopGroup(1); - workerGroup = new NioEventLoopGroup(); - - try - { - ServerBootstrap bootstrap = new ServerBootstrap(); - bootstrap.group(bossGroup, workerGroup) - .channel(NioServerSocketChannel.class) - .handler(new LoggingHandler(LogLevel.INFO)) - .childHandler(new TIC2WebSocketChannelInitializer(clientPool, requestHandler)); - - ChannelFuture future = bootstrap.bind(host, port).sync(); - serverChannel = future.channel(); - - logger.info("TIC2WebSocket Netty server started successfully on {}:{}", host, port); - } - catch (Exception e) - { - logger.error("Failed to start TIC2WebSocket Netty server", e); - stop(); - throw e; - } + } + + /** Stop the WebSocket server */ + public void stop() { + logger.info("Stopping TIC2WebSocket Netty server"); + + try { + if (serverChannel != null) { + serverChannel.close().sync(); + } + } catch (InterruptedException e) { + logger.warn("Interrupted while closing server channel", e); + Thread.currentThread().interrupt(); + } finally { + if (bossGroup != null) { + bossGroup.shutdownGracefully(); + } + if (workerGroup != null) { + workerGroup.shutdownGracefully(); + } } - /** - * Stop the WebSocket server - */ - public void stop() - { - logger.info("Stopping TIC2WebSocket Netty server"); - - try - { - if (serverChannel != null) - { - serverChannel.close().sync(); - } - } - catch (InterruptedException e) - { - logger.warn("Interrupted while closing server channel", e); - Thread.currentThread().interrupt(); - } - finally - { - if (bossGroup != null) - { - bossGroup.shutdownGracefully(); - } - if (workerGroup != null) - { - workerGroup.shutdownGracefully(); - } - } - - logger.info("TIC2WebSocket Netty server stopped"); + logger.info("TIC2WebSocket Netty server stopped"); + } + + /** + * Wait for the server to close + * + * @throws InterruptedException if interrupted while waiting + */ + public void waitForClose() throws InterruptedException { + if (serverChannel != null) { + serverChannel.closeFuture().sync(); } - - /** - * Wait for the server to close - * - * @throws InterruptedException if interrupted while waiting - */ - public void waitForClose() throws InterruptedException - { - if (serverChannel != null) - { - serverChannel.closeFuture().sync(); - } - } - + } } diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java index da0f444..8a1dbd1 100644 --- a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java +++ b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -11,17 +11,14 @@ import enedis.lab.util.message.Response; import enedis.tic.service.client.TIC2WebSocketClient; -/** - * TIC2WebSocket request handler interface - */ -public interface TIC2WebSocketRequestHandler -{ - /** - * Handle all TIC2WebSocket request - * - * @param request - * @param client - * @return request response - */ - public Response handle(Request request, TIC2WebSocketClient client); +/** TIC2WebSocket request handler interface */ +public interface TIC2WebSocketRequestHandler { + /** + * Handle all TIC2WebSocket request + * + * @param request + * @param client + * @return request response + */ + public Response handle(Request request, TIC2WebSocketClient client); } diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java index 65eb3b6..761c1cd 100644 --- a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java +++ b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java @@ -7,14 +7,6 @@ package enedis.tic.service.requesthandler; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import enedis.lab.io.tic.TICPortDescriptor; import enedis.lab.types.DataDictionaryException; import enedis.lab.util.message.Request; @@ -37,250 +29,243 @@ import enedis.tic.service.message.ResponseReadTIC; import enedis.tic.service.message.ResponseSubscribeTIC; import enedis.tic.service.message.ResponseUnsubscribeTIC; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; -/** - * TIC2WebSocket client pool interface - */ -public class TIC2WebSocketRequestHandlerBase implements TIC2WebSocketRequestHandler -{ - private Logger logger; - private TICCore ticCore; - - /** - * Default constructor - * - * @param ticCore - */ - public TIC2WebSocketRequestHandlerBase(TICCore ticCore) - { - super(); - this.logger = LogManager.getLogger(this.getClass()); - this.ticCore = ticCore; - } +/** TIC2WebSocket client pool interface */ +public class TIC2WebSocketRequestHandlerBase implements TIC2WebSocketRequestHandler { + private Logger logger; + private TICCore ticCore; - @Override - public Response handle(Request request, TIC2WebSocketClient client) - { - Response response = null; + /** + * Default constructor + * + * @param ticCore + */ + public TIC2WebSocketRequestHandlerBase(TICCore ticCore) { + super(); + this.logger = LogManager.getLogger(this.getClass()); + this.ticCore = ticCore; + } - switch (request.getName()) - { - case RequestGetAvailableTICs.NAME: - response = this.handleGetAvailableTICsRequest((RequestGetAvailableTICs) request); - break; - case RequestGetModemsInfo.NAME: - response = this.handleGetModemsInfoRequest((RequestGetModemsInfo) request); - break; - case RequestReadTIC.NAME: - response = this.handleReadTICRequest((RequestReadTIC) request); - break; - case RequestSubscribeTIC.NAME: - response = this.handleSubscribeTICRequest((RequestSubscribeTIC) request, client); - break; - case RequestUnsubscribeTIC.NAME: - response = this.handleUnsubscribeTICRequest((RequestUnsubscribeTIC) request, client); - break; - default: - this.logger.error("Request " + request.getName() + " not supported"); - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.UNSUPPORTED_MESSAGE, "Request " + request.getName() + " not supported"); - break; - } + @Override + public Response handle(Request request, TIC2WebSocketClient client) { + Response response = null; - return response; - } + switch (request.getName()) { + case RequestGetAvailableTICs.NAME: + response = this.handleGetAvailableTICsRequest((RequestGetAvailableTICs) request); + break; + case RequestGetModemsInfo.NAME: + response = this.handleGetModemsInfoRequest((RequestGetModemsInfo) request); + break; + case RequestReadTIC.NAME: + response = this.handleReadTICRequest((RequestReadTIC) request); + break; + case RequestSubscribeTIC.NAME: + response = this.handleSubscribeTICRequest((RequestSubscribeTIC) request, client); + break; + case RequestUnsubscribeTIC.NAME: + response = this.handleUnsubscribeTICRequest((RequestUnsubscribeTIC) request, client); + break; + default: + this.logger.error("Request " + request.getName() + " not supported"); + response = + this.createErrorResponse( + request.getName(), + TIC2WebSocketEndPointErrorCode.UNSUPPORTED_MESSAGE, + "Request " + request.getName() + " not supported"); + break; + } - private Response handleGetAvailableTICsRequest(RequestGetAvailableTICs request) - { - List ticIdentifiers = this.ticCore.getAvailableTICs(); + return response; + } - Response response = null; - try - { - response = new ResponseGetAvailableTICs(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, ticIdentifiers); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); - } + private Response handleGetAvailableTICsRequest(RequestGetAvailableTICs request) { + List ticIdentifiers = this.ticCore.getAvailableTICs(); - return response; - } + Response response = null; + try { + response = + new ResponseGetAvailableTICs( + LocalDateTime.now(), + TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), + null, + ticIdentifiers); + } catch (DataDictionaryException e) { + this.logger.error(e.getMessage(), e); + response = + this.createErrorResponse( + request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); + } - private Response handleGetModemsInfoRequest(RequestGetModemsInfo request) - { - List modemsInfo = this.ticCore.getModemsInfo(); + return response; + } - Response response = null; - try - { - response = new ResponseGetModemsInfo(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, modemsInfo); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); - } + private Response handleGetModemsInfoRequest(RequestGetModemsInfo request) { + List modemsInfo = this.ticCore.getModemsInfo(); - return response; - } + Response response = null; + try { + response = + new ResponseGetModemsInfo( + LocalDateTime.now(), + TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), + null, + modemsInfo); + } catch (DataDictionaryException e) { + this.logger.error(e.getMessage(), e); + response = + this.createErrorResponse( + request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); + } - private Response handleReadTICRequest(RequestReadTIC request) - { - Response response = null; - try - { - TICCoreFrame frame = this.ticCore.readNextFrame(request.getData()); - response = new ResponseReadTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, frame); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); - } - catch (TICCoreException e) - { - if(e.getErrorCode() == TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode()) - { - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.IDENTIFIER_NOT_FOUND, e.getMessage()); - } - else if(e.getErrorCode() == TICCoreErrorCode.DATA_READ_TIMEOUT.getCode()) - { - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.READ_TIMEOUT, e.getMessage()); - } - else - { - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); - } - } + return response; + } - return response; - } + private Response handleReadTICRequest(RequestReadTIC request) { + Response response = null; + try { + TICCoreFrame frame = this.ticCore.readNextFrame(request.getData()); + response = + new ResponseReadTIC( + LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, frame); + } catch (DataDictionaryException e) { + this.logger.error(e.getMessage(), e); + response = + this.createErrorResponse( + request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); + } catch (TICCoreException e) { + if (e.getErrorCode() == TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode()) { + response = + this.createErrorResponse( + request.getName(), + TIC2WebSocketEndPointErrorCode.IDENTIFIER_NOT_FOUND, + e.getMessage()); + } else if (e.getErrorCode() == TICCoreErrorCode.DATA_READ_TIMEOUT.getCode()) { + response = + this.createErrorResponse( + request.getName(), TIC2WebSocketEndPointErrorCode.READ_TIMEOUT, e.getMessage()); + } else { + response = + this.createErrorResponse( + request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } - private Response handleSubscribeTICRequest(RequestSubscribeTIC request, TIC2WebSocketClient client) - { - Response response = null; - Optional> ticIdentifiers = Optional.ofNullable(request.getData()); + return response; + } - if (ticIdentifiers.isPresent()) - { - List newSubscriptions = this.getNewSubcriptions(this.ticCore.getIndentifiers(client), ticIdentifiers.get()); - if(!newSubscriptions.isEmpty()) - { - for(TICIdentifier identifier : ticIdentifiers.get()) - { - try - { - this.ticCore.subscribe(identifier,client); - try - { - response = new ResponseSubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } - catch (TICCoreException e) - { - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.SUBSCRIPTION_FAIL, e.getMessage()); - } - } - } - } - else - { - this.ticCore.subscribe(client); - try - { - response = new ResponseSubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } + private Response handleSubscribeTICRequest( + RequestSubscribeTIC request, TIC2WebSocketClient client) { + Response response = null; + Optional> ticIdentifiers = Optional.ofNullable(request.getData()); - return response; - } + if (ticIdentifiers.isPresent()) { + List newSubscriptions = + this.getNewSubcriptions(this.ticCore.getIndentifiers(client), ticIdentifiers.get()); + if (!newSubscriptions.isEmpty()) { + for (TICIdentifier identifier : ticIdentifiers.get()) { + try { + this.ticCore.subscribe(identifier, client); + try { + response = + new ResponseSubscribeTIC( + LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); + } catch (DataDictionaryException e) { + this.logger.error(e.getMessage(), e); + } + } catch (TICCoreException e) { + response = + this.createErrorResponse( + request.getName(), + TIC2WebSocketEndPointErrorCode.SUBSCRIPTION_FAIL, + e.getMessage()); + } + } + } + } else { + this.ticCore.subscribe(client); + try { + response = + new ResponseSubscribeTIC( + LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); + } catch (DataDictionaryException e) { + this.logger.error(e.getMessage(), e); + } + } - private List getNewSubcriptions(List currentSubscriptions, List askedTicIdentifiers) - { - List newSubscriptions = new ArrayList(); + return response; + } - for (TICIdentifier askedTicIdentifier : askedTicIdentifiers) - { - boolean newSub = true; - for (TICIdentifier currentSubscription : currentSubscriptions) - { - if (askedTicIdentifier.matches(currentSubscription)) - { - newSub = false; - break; - } - } - if (newSub) - { - newSubscriptions.add(askedTicIdentifier); - } - } - return newSubscriptions; - } + private List getNewSubcriptions( + List currentSubscriptions, List askedTicIdentifiers) { + List newSubscriptions = new ArrayList(); - private Response handleUnsubscribeTICRequest(RequestUnsubscribeTIC request, TIC2WebSocketClient client) - { - Response response = null; - Optional> ticIdentifiers = Optional.ofNullable(request.getData()); + for (TICIdentifier askedTicIdentifier : askedTicIdentifiers) { + boolean newSub = true; + for (TICIdentifier currentSubscription : currentSubscriptions) { + if (askedTicIdentifier.matches(currentSubscription)) { + newSub = false; + break; + } + } + if (newSub) { + newSubscriptions.add(askedTicIdentifier); + } + } + return newSubscriptions; + } - if (ticIdentifiers.isPresent()) - { - for(TICIdentifier identifier : ticIdentifiers.get()) - { - try - { - this.ticCore.unsubscribe(identifier,client); - try - { - response = new ResponseUnsubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } - catch (TICCoreException e) - { - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.UNSUBSCRIPTION_FAIL, e.getMessage()); - } - } - } - else - { - this.ticCore.unsubscribe(client); - try - { - response = new ResponseUnsubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } + private Response handleUnsubscribeTICRequest( + RequestUnsubscribeTIC request, TIC2WebSocketClient client) { + Response response = null; + Optional> ticIdentifiers = Optional.ofNullable(request.getData()); - return response; - } + if (ticIdentifiers.isPresent()) { + for (TICIdentifier identifier : ticIdentifiers.get()) { + try { + this.ticCore.unsubscribe(identifier, client); + try { + response = + new ResponseUnsubscribeTIC( + LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); + } catch (DataDictionaryException e) { + this.logger.error(e.getMessage(), e); + } + } catch (TICCoreException e) { + response = + this.createErrorResponse( + request.getName(), + TIC2WebSocketEndPointErrorCode.UNSUBSCRIPTION_FAIL, + e.getMessage()); + } + } + } else { + this.ticCore.unsubscribe(client); + try { + response = + new ResponseUnsubscribeTIC( + LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); + } catch (DataDictionaryException e) { + this.logger.error(e.getMessage(), e); + } + } - private Response createErrorResponse(String name, TIC2WebSocketEndPointErrorCode code, String message) - { - try - { - return new ResponseBase(name, LocalDateTime.now(), code.value(), message, null); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - return null; - } - } + return response; + } + private Response createErrorResponse( + String name, TIC2WebSocketEndPointErrorCode code, String message) { + try { + return new ResponseBase(name, LocalDateTime.now(), code.value(), message, null); + } catch (DataDictionaryException e) { + this.logger.error(e.getMessage(), e); + return null; + } + } } diff --git a/src/test/java/enedis/lab/io/PortFinderMock.java b/src/test/java/enedis/lab/io/PortFinderMock.java index 4fb9c13..ce6e983 100644 --- a/src/test/java/enedis/lab/io/PortFinderMock.java +++ b/src/test/java/enedis/lab/io/PortFinderMock.java @@ -7,67 +7,55 @@ package enedis.lab.io; -import java.util.ArrayList; -import java.util.List; - import enedis.lab.mock.FunctionCall; import enedis.lab.types.DataArrayList; import enedis.lab.types.DataList; +import java.util.ArrayList; +import java.util.List; @SuppressWarnings("javadoc") -public class PortFinderMock implements PortFinder -{ - public List findAllCalls = new ArrayList(); - public DataList descriptorList = new DataArrayList(); - - public PortFinderMock() - { - this.descriptorList = new DataArrayList(); - } - - @Override - public DataList findAll() - { - this.findAllCalls.add(new FunctionCall()); - return this.getDescriptors(); - } - - public DataList getDescriptors() - { - DataList descriptors = new DataArrayList(); - - synchronized (this.descriptorList) - { - descriptors.addAll(this.descriptorList); - } - - return descriptors; - } - - public DataList setDescriptors(DataList descriptors) - { - synchronized (this.descriptorList) - { - this.descriptorList.clear(); - this.descriptorList.addAll(descriptors); - } - - return descriptors; - } - - public void addDescriptor(T descriptor) - { - synchronized (this.descriptorList) - { - this.descriptorList.add(descriptor); - } - } - - public void removeDescriptor(T descriptor) - { - synchronized (this.descriptorList) - { - this.descriptorList.remove(descriptor); - } - } +public class PortFinderMock implements PortFinder { + public List findAllCalls = new ArrayList(); + public DataList descriptorList = new DataArrayList(); + + public PortFinderMock() { + this.descriptorList = new DataArrayList(); + } + + @Override + public DataList findAll() { + this.findAllCalls.add(new FunctionCall()); + return this.getDescriptors(); + } + + public DataList getDescriptors() { + DataList descriptors = new DataArrayList(); + + synchronized (this.descriptorList) { + descriptors.addAll(this.descriptorList); + } + + return descriptors; + } + + public DataList setDescriptors(DataList descriptors) { + synchronized (this.descriptorList) { + this.descriptorList.clear(); + this.descriptorList.addAll(descriptors); + } + + return descriptors; + } + + public void addDescriptor(T descriptor) { + synchronized (this.descriptorList) { + this.descriptorList.add(descriptor); + } + } + + public void removeDescriptor(T descriptor) { + synchronized (this.descriptorList) { + this.descriptorList.remove(descriptor); + } + } } diff --git a/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java b/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java index 585cbf4..a74af07 100644 --- a/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java +++ b/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -12,36 +12,28 @@ import enedis.lab.types.DataList; @SuppressWarnings("javadoc") -public class TICPortFinderMock extends PortFinderMock implements TICPortFinder -{ - public DataList nativeDescriptorList = new DataArrayList(); - - public TICPortFinderMock() - { - super(); - } - - public TICPortFinderMock(TICPortDescriptor... descriptors) - { - this.setDescriptors(DataArrayList.asList(descriptors)); - } - - @Override - public TICPortDescriptor findNative(String portName) - { - for (TICPortDescriptor descriptor : this.nativeDescriptorList) - { - if (descriptor.getPortName() == null && portName == null) - { - return descriptor; - } - if (descriptor.getPortName().equals(portName)) - { - return descriptor; - } - } - - return null; - } - +public class TICPortFinderMock extends PortFinderMock implements TICPortFinder { + public DataList nativeDescriptorList = new DataArrayList(); + + public TICPortFinderMock() { + super(); + } + + public TICPortFinderMock(TICPortDescriptor... descriptors) { + this.setDescriptors(DataArrayList.asList(descriptors)); + } + + @Override + public TICPortDescriptor findNative(String portName) { + for (TICPortDescriptor descriptor : this.nativeDescriptorList) { + if (descriptor.getPortName() == null && portName == null) { + return descriptor; + } + if (descriptor.getPortName().equals(portName)) { + return descriptor; + } + } + + return null; + } } diff --git a/src/test/java/enedis/lab/mock/FunctionCall.java b/src/test/java/enedis/lab/mock/FunctionCall.java index 6a5ba81..13b4371 100644 --- a/src/test/java/enedis/lab/mock/FunctionCall.java +++ b/src/test/java/enedis/lab/mock/FunctionCall.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -8,19 +8,16 @@ package enedis.lab.mock; @SuppressWarnings("javadoc") -public class FunctionCall -{ - public long captureTime; +public class FunctionCall { + public long captureTime; - public FunctionCall() - { - super(); - this.captureTime = System.nanoTime(); - } + public FunctionCall() { + super(); + this.captureTime = System.nanoTime(); + } - public FunctionCall(long captureTime) - { - super(); - this.captureTime = captureTime; - } + public FunctionCall(long captureTime) { + super(); + this.captureTime = captureTime; + } } diff --git a/src/test/java/enedis/tic/core/TICCoreBaseTest.java b/src/test/java/enedis/tic/core/TICCoreBaseTest.java index f8d7fff..9d83c1a 100644 --- a/src/test/java/enedis/tic/core/TICCoreBaseTest.java +++ b/src/test/java/enedis/tic/core/TICCoreBaseTest.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,14 +7,6 @@ package enedis.tic.core; -import java.time.LocalDateTime; -import java.util.List; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - import enedis.lab.io.tic.TICModemType; import enedis.lab.io.tic.TICPortDescriptor; import enedis.lab.io.tic.TICPortFinderMock; @@ -22,477 +14,529 @@ import enedis.lab.protocol.tic.TICMode; import enedis.lab.types.DataDictionaryException; import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.util.time.Time; import enedis.lab.util.task.Task; +import enedis.lab.util.time.Time; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; @SuppressWarnings("javadoc") -public class TICCoreBaseTest -{ - private static final int NOTIFICATION_TIMEOUT = 1000; - - private TICPortFinderMock ticPortFinder; - private long plugNotifierPeriod; - private TICCoreBase ticCore; - - @Before - public void startTICCore() throws TICCoreException - { - this.ticPortFinder = new TICPortFinderMock(); - this.plugNotifierPeriod = 50; - this.ticCore = new TICCoreBase(this.ticPortFinder, this.plugNotifierPeriod, TICCoreStreamMock.class, TICMode.AUTO, null); - - this.ticCore.start(); - this.waitTaskRunning(this.ticCore); - } - - @After - public void stopTICCore() throws TICCoreException - { - this.ticCore.stop(); - TICCoreStreamMock.streams.clear(); - } - - @Test - public void test_getAvailableTICs() throws TICCoreException, DataDictionaryException - { - List availableTICs = this.ticCore.getAvailableTICs(); - Assert.assertNotNull(availableTICs); - Assert.assertEquals(0, availableTICs.size()); - - TICPortDescriptor descriptor1 = new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); - this.plugModem(descriptor1); - this.plugModem(descriptor2); - - availableTICs = this.ticCore.getAvailableTICs(); - Assert.assertNotNull(availableTICs); - Assert.assertEquals(2, availableTICs.size()); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("2", "COM4", null))); - - this.unplugModem(descriptor2); - - availableTICs = this.ticCore.getAvailableTICs(); - Assert.assertNotNull(availableTICs); - Assert.assertEquals(1, availableTICs.size()); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); - } - - @Test - public void test_getModemsInfo() throws TICCoreException, DataDictionaryException - { - List modemsInfo = this.ticCore.getModemsInfo(); - Assert.assertNotNull(modemsInfo); - Assert.assertEquals(0, modemsInfo.size()); - - TICPortDescriptor descriptor1 = new TICPortDescriptor(null, "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = new TICPortDescriptor(null, "COM4", null, null, null, null, TICModemType.TELEINFO); - this.plugModem(descriptor1); - this.plugModem(descriptor2); - - modemsInfo = this.ticCore.getModemsInfo(); - Assert.assertNotNull(modemsInfo); - Assert.assertEquals(2, modemsInfo.size()); - Assert.assertTrue(modemsInfo.contains(descriptor1)); - Assert.assertTrue(modemsInfo.contains(descriptor2)); - - this.unplugModem(descriptor2); - modemsInfo = this.ticCore.getModemsInfo(); - Assert.assertNotNull(modemsInfo); - Assert.assertEquals(1, modemsInfo.size()); - Assert.assertTrue(modemsInfo.contains(descriptor1)); - } - - @Test - public void test_readNextFrame_ok() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); - task.start(); - this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); - TICCoreFrame frame = this.createFrame(identifier, TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); - stream.notifyOnData(frame); - this.waitTaskTerminated(task); - Assert.assertNull(task.exception); - Assert.assertNotNull(task.frame); - Assert.assertTrue(task.frame == frame); - } - - @Test - public void test_readNextFrame_error_OTHER_REASON() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); - task.start(); - this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); - TICCoreError error = new TICCoreError(identifier, TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); - stream.notifyOnError(error); - this.waitTaskTerminated(task); - Assert.assertNull(task.frame); - Assert.assertNotNull(task.exception); - Assert.assertTrue(task.exception instanceof TICCoreException); - TICCoreException exception = (TICCoreException) task.exception; - Assert.assertEquals(error.getErrorCode(), exception.getErrorCode()); - Assert.assertEquals(error.getErrorMessage(), exception.getErrorInfo()); - } - - @Test - public void test_readNextFrame_error_STREAM_IDENTIFIER_NOT_FOUND() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, new TICIdentifier("2", "COM3", null)); - task.start(); - this.waitTaskRunning(task); - this.waitTaskTerminated(task); - Assert.assertNull(task.frame); - Assert.assertNotNull(task.exception); - Assert.assertTrue(task.exception instanceof TICCoreException); - TICCoreException exception = (TICCoreException) task.exception; - Assert.assertEquals(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); - } - - @Test - public void test_readNextFrame_timeout() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier, 200); - task.start(); - this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); - this.waitTaskTerminated(task); - Assert.assertNull(task.frame); - Assert.assertNotNull(task.exception); - Assert.assertTrue(task.exception instanceof TICCoreException); - TICCoreException exception = (TICCoreException) task.exception; - Assert.assertEquals(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), exception.getErrorCode()); - } - - @Test - public void test_subscribe_any() throws TICCoreException, DataDictionaryException - { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - } - - @Test - public void test_unsubscribe_any() throws TICCoreException, DataDictionaryException - { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - this.ticCore.unsubscribe(subscriber); - } - - @Test - public void test_subscribe_withIdentifier_ok() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - } - - @Test - public void test_unsubscribe_withIdentifier_ok() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - this.ticCore.unsubscribe(identifier, subscriber); - } - - @Test - public void test_subscribe_withIdentifier_notFound() throws TICCoreException, DataDictionaryException - { - this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - TICIdentifier identifier = new TICIdentifier(null, "COM4", null); - try - { - this.ticCore.subscribe(identifier, subscriber); - Assert.fail("Subscribe should have trhown an exception since stream identifier does not match !"); - } - catch (Exception e) - { - Assert.assertTrue(e instanceof TICCoreException); - TICCoreException exception = (TICCoreException) e; - Assert.assertEquals(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); - } - } - - @Test - public void test_onError_whenUnplugModem() throws TICCoreException, DataDictionaryException - { - TICPortDescriptor descriptor1 = new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); - TICIdentifier identifier1 = this.plugModem(descriptor1); - TICIdentifier identifier2 = this.plugModem(descriptor2); - - TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber4 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber5 = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber1); - this.ticCore.subscribe(identifier1, subscriber2); - this.ticCore.subscribe(identifier1, subscriber3); - this.ticCore.subscribe(identifier2, subscriber4); - this.ticCore.subscribe(identifier2, subscriber5); - - this.unplugModem(descriptor1); - this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); - Assert.assertEquals(1, subscriber1.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber1.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber2.onErrorCalls, 1); - Assert.assertEquals(1, subscriber2.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber2.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber3.onErrorCalls, 1); - Assert.assertEquals(1, subscriber3.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber3.onErrorCalls.get(0).error.getErrorCode()); - Assert.assertEquals(0, subscriber4.onErrorCalls.size()); - Assert.assertEquals(0, subscriber5.onErrorCalls.size()); - - this.unplugModem(descriptor2); - this.waitSubscriberNotification(subscriber4.onErrorCalls, 1); - Assert.assertEquals(1, subscriber4.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber4.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber5.onErrorCalls, 1); - Assert.assertEquals(1, subscriber5.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber5.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); - Assert.assertEquals(2, subscriber1.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber1.onErrorCalls.get(1).error.getErrorCode()); - Assert.assertEquals(1, subscriber2.onErrorCalls.size()); - Assert.assertEquals(1, subscriber3.onErrorCalls.size()); - } - - @Test - public void test_onData_any() throws TICCoreException, DataDictionaryException - { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - - this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreFrame frame1 = this.createFrame(stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); - stream1.notifyOnData(frame1); - this.waitSubscriberNotification(subscriber.onDataCalls, 1); - Assert.assertEquals(1, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - - this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreFrame frame2 = this.createFrame(stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); - stream2.notifyOnData(frame2); - this.waitSubscriberNotification(subscriber.onDataCalls, 2); - Assert.assertEquals(2, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - Assert.assertTrue(frame2 == subscriber.onDataCalls.get(1).frame); - } - - @Test - public void test_onData_withIdentifier() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreFrame frame1 = this.createFrame(stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); - stream1.notifyOnData(frame1); - this.waitSubscriberNotification(subscriber.onDataCalls, 1); - Assert.assertEquals(1, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - - this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.MICHAUD)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreFrame frame2 = this.createFrame(stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); - stream2.notifyOnData(frame2); - this.waitSubscriberNotification(subscriber.onDataCalls, 1); - Assert.assertEquals(1, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - } - - @Test - public void test_onError_any() throws TICCoreException, DataDictionaryException - { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - - this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreError error1 = new TICCoreError(stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); - stream1.notifyOnError(error1); - this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - - this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreError error2 = new TICCoreError(stream2.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Error reading stream COM4"); - stream2.notifyOnError(error2); - this.waitSubscriberNotification(subscriber.onErrorCalls, 2); - Assert.assertEquals(2, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - Assert.assertTrue(error2 == subscriber.onErrorCalls.get(1).error); - } - - @Test - public void test_onError_withIdentifier() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreError error1 = new TICCoreError(stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); - stream1.notifyOnError(error1); - this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - - this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreError error2 = new TICCoreError(stream2.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Error reading stream COM4"); - stream2.notifyOnError(error2); - this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - } - - @Test - public void test_getIdentifiers() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier1 = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - TICIdentifier identifier2 = this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - - TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); - - this.ticCore.subscribe(subscriber1); - List indentifierList1 = this.ticCore.getIndentifiers(subscriber1); - Assert.assertNotNull(indentifierList1); - Assert.assertEquals(2, indentifierList1.size()); - Assert.assertTrue(indentifierList1.contains(identifier1)); - Assert.assertTrue(indentifierList1.contains(identifier2)); - - this.ticCore.subscribe(identifier1, subscriber2); - List indentifierList2 = this.ticCore.getIndentifiers(subscriber2); - Assert.assertNotNull(indentifierList2); - Assert.assertEquals(1, indentifierList2.size()); - Assert.assertTrue(indentifierList2.contains(identifier1)); - - this.ticCore.subscribe(identifier2, subscriber3); - List indentifierList3 = this.ticCore.getIndentifiers(subscriber3); - Assert.assertNotNull(indentifierList3); - Assert.assertEquals(1, indentifierList3.size()); - Assert.assertTrue(indentifierList3.contains(identifier2)); - - this.ticCore.unsubscribe(subscriber1); - indentifierList1 = this.ticCore.getIndentifiers(subscriber1); - Assert.assertNotNull(indentifierList1); - Assert.assertEquals(0, indentifierList1.size()); - - this.ticCore.unsubscribe(identifier1, subscriber2); - indentifierList2 = this.ticCore.getIndentifiers(subscriber2); - Assert.assertNotNull(indentifierList2); - Assert.assertEquals(0, indentifierList2.size()); - - this.ticCore.unsubscribe(identifier2, subscriber3); - indentifierList3 = this.ticCore.getIndentifiers(subscriber3); - Assert.assertNotNull(indentifierList3); - Assert.assertEquals(0, indentifierList3.size()); - } - - private TICIdentifier plugModem(TICPortDescriptor descriptor) throws DataDictionaryException - { - this.ticPortFinder.addDescriptor(descriptor); - this.waitPlugNotifierUpdate(); - - return new TICIdentifier(descriptor.getPortId(), descriptor.getPortName(), null); - } - - private void unplugModem(TICPortDescriptor descriptor) - { - this.ticPortFinder.removeDescriptor(descriptor); - this.waitPlugNotifierUpdate(); - } - - private TICCoreFrame createFrame(TICIdentifier identifier, TICMode mode, LocalDateTime localDateTime) throws DataDictionaryException - { - DataDictionaryBase content = new DataDictionaryBase(); - if (mode == TICMode.STANDARD) - { - content.set("ADSC", identifier.getSerialNumber()); - } - else - { - content.set("ADCO", identifier.getSerialNumber()); - } - TICCoreFrame frame = new TICCoreFrame(identifier, mode, localDateTime, content); - - return frame; - } - - private void waitPlugNotifierUpdate() - { - Time.sleep(2 * this.plugNotifierPeriod); - } - - private void waitTaskRunning(Task task) - { - while (!task.isRunning()) - { - Time.sleep(50); - } - } - - private void waitTaskTerminated(Task task) - { - while (task.isRunning()) - { - Time.sleep(50); - } - } - - private void waitReadNextFrameSubsbription() - { - Time.sleep(100); - } - - private void waitSubscriberNotification(List onCalls, int expectedSize) - { - long begin = System.nanoTime(); - long elapsed = 0; - while (onCalls.size() < expectedSize && elapsed < NOTIFICATION_TIMEOUT) - { - Time.sleep(50); - elapsed = (System.nanoTime() - begin) / 1000000; - } - } +public class TICCoreBaseTest { + private static final int NOTIFICATION_TIMEOUT = 1000; + + private TICPortFinderMock ticPortFinder; + private long plugNotifierPeriod; + private TICCoreBase ticCore; + + @Before + public void startTICCore() throws TICCoreException { + this.ticPortFinder = new TICPortFinderMock(); + this.plugNotifierPeriod = 50; + this.ticCore = + new TICCoreBase( + this.ticPortFinder, + this.plugNotifierPeriod, + TICCoreStreamMock.class, + TICMode.AUTO, + null); + + this.ticCore.start(); + this.waitTaskRunning(this.ticCore); + } + + @After + public void stopTICCore() throws TICCoreException { + this.ticCore.stop(); + TICCoreStreamMock.streams.clear(); + } + + @Test + public void test_getAvailableTICs() throws TICCoreException, DataDictionaryException { + List availableTICs = this.ticCore.getAvailableTICs(); + Assert.assertNotNull(availableTICs); + Assert.assertEquals(0, availableTICs.size()); + + TICPortDescriptor descriptor1 = + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); + TICPortDescriptor descriptor2 = + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); + this.plugModem(descriptor1); + this.plugModem(descriptor2); + + availableTICs = this.ticCore.getAvailableTICs(); + Assert.assertNotNull(availableTICs); + Assert.assertEquals(2, availableTICs.size()); + Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); + Assert.assertTrue(availableTICs.contains(new TICIdentifier("2", "COM4", null))); + + this.unplugModem(descriptor2); + + availableTICs = this.ticCore.getAvailableTICs(); + Assert.assertNotNull(availableTICs); + Assert.assertEquals(1, availableTICs.size()); + Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); + } + + @Test + public void test_getModemsInfo() throws TICCoreException, DataDictionaryException { + List modemsInfo = this.ticCore.getModemsInfo(); + Assert.assertNotNull(modemsInfo); + Assert.assertEquals(0, modemsInfo.size()); + + TICPortDescriptor descriptor1 = + new TICPortDescriptor(null, "COM3", null, null, null, null, TICModemType.MICHAUD); + TICPortDescriptor descriptor2 = + new TICPortDescriptor(null, "COM4", null, null, null, null, TICModemType.TELEINFO); + this.plugModem(descriptor1); + this.plugModem(descriptor2); + + modemsInfo = this.ticCore.getModemsInfo(); + Assert.assertNotNull(modemsInfo); + Assert.assertEquals(2, modemsInfo.size()); + Assert.assertTrue(modemsInfo.contains(descriptor1)); + Assert.assertTrue(modemsInfo.contains(descriptor2)); + + this.unplugModem(descriptor2); + modemsInfo = this.ticCore.getModemsInfo(); + Assert.assertNotNull(modemsInfo); + Assert.assertEquals(1, modemsInfo.size()); + Assert.assertTrue(modemsInfo.contains(descriptor1)); + } + + @Test + public void test_readNextFrame_ok() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); + task.start(); + this.waitTaskRunning(task); + this.waitReadNextFrameSubsbription(); + TICCoreFrame frame = + this.createFrame(identifier, TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + stream.notifyOnData(frame); + this.waitTaskTerminated(task); + Assert.assertNull(task.exception); + Assert.assertNotNull(task.frame); + Assert.assertTrue(task.frame == frame); + } + + @Test + public void test_readNextFrame_error_OTHER_REASON() + throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); + task.start(); + this.waitTaskRunning(task); + this.waitReadNextFrameSubsbription(); + TICCoreError error = + new TICCoreError(identifier, TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + stream.notifyOnError(error); + this.waitTaskTerminated(task); + Assert.assertNull(task.frame); + Assert.assertNotNull(task.exception); + Assert.assertTrue(task.exception instanceof TICCoreException); + TICCoreException exception = (TICCoreException) task.exception; + Assert.assertEquals(error.getErrorCode(), exception.getErrorCode()); + Assert.assertEquals(error.getErrorMessage(), exception.getErrorInfo()); + } + + @Test + public void test_readNextFrame_error_STREAM_IDENTIFIER_NOT_FOUND() + throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = + new TICCoreReadNextFrameTask(this.ticCore, new TICIdentifier("2", "COM3", null)); + task.start(); + this.waitTaskRunning(task); + this.waitTaskTerminated(task); + Assert.assertNull(task.frame); + Assert.assertNotNull(task.exception); + Assert.assertTrue(task.exception instanceof TICCoreException); + TICCoreException exception = (TICCoreException) task.exception; + Assert.assertEquals( + TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); + } + + @Test + public void test_readNextFrame_timeout() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier, 200); + task.start(); + this.waitTaskRunning(task); + this.waitReadNextFrameSubsbription(); + this.waitTaskTerminated(task); + Assert.assertNull(task.frame); + Assert.assertNotNull(task.exception); + Assert.assertTrue(task.exception instanceof TICCoreException); + TICCoreException exception = (TICCoreException) task.exception; + Assert.assertEquals(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), exception.getErrorCode()); + } + + @Test + public void test_subscribe_any() throws TICCoreException, DataDictionaryException { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + } + + @Test + public void test_unsubscribe_any() throws TICCoreException, DataDictionaryException { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + this.ticCore.unsubscribe(subscriber); + } + + @Test + public void test_subscribe_withIdentifier_ok() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + } + + @Test + public void test_unsubscribe_withIdentifier_ok() + throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + this.ticCore.unsubscribe(identifier, subscriber); + } + + @Test + public void test_subscribe_withIdentifier_notFound() + throws TICCoreException, DataDictionaryException { + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + TICIdentifier identifier = new TICIdentifier(null, "COM4", null); + try { + this.ticCore.subscribe(identifier, subscriber); + Assert.fail( + "Subscribe should have trhown an exception since stream identifier does not match !"); + } catch (Exception e) { + Assert.assertTrue(e instanceof TICCoreException); + TICCoreException exception = (TICCoreException) e; + Assert.assertEquals( + TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); + } + } + + @Test + public void test_onError_whenUnplugModem() throws TICCoreException, DataDictionaryException { + TICPortDescriptor descriptor1 = + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); + TICPortDescriptor descriptor2 = + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); + TICIdentifier identifier1 = this.plugModem(descriptor1); + TICIdentifier identifier2 = this.plugModem(descriptor2); + + TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber4 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber5 = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber1); + this.ticCore.subscribe(identifier1, subscriber2); + this.ticCore.subscribe(identifier1, subscriber3); + this.ticCore.subscribe(identifier2, subscriber4); + this.ticCore.subscribe(identifier2, subscriber5); + + this.unplugModem(descriptor1); + this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); + Assert.assertEquals(1, subscriber1.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber1.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber2.onErrorCalls, 1); + Assert.assertEquals(1, subscriber2.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber2.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber3.onErrorCalls, 1); + Assert.assertEquals(1, subscriber3.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber3.onErrorCalls.get(0).error.getErrorCode()); + Assert.assertEquals(0, subscriber4.onErrorCalls.size()); + Assert.assertEquals(0, subscriber5.onErrorCalls.size()); + + this.unplugModem(descriptor2); + this.waitSubscriberNotification(subscriber4.onErrorCalls, 1); + Assert.assertEquals(1, subscriber4.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber4.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber5.onErrorCalls, 1); + Assert.assertEquals(1, subscriber5.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber5.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); + Assert.assertEquals(2, subscriber1.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber1.onErrorCalls.get(1).error.getErrorCode()); + Assert.assertEquals(1, subscriber2.onErrorCalls.size()); + Assert.assertEquals(1, subscriber3.onErrorCalls.size()); + } + + @Test + public void test_onData_any() throws TICCoreException, DataDictionaryException { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreFrame frame1 = + this.createFrame( + stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + stream1.notifyOnData(frame1); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); + Assert.assertEquals(1, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + + this.plugModem( + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreFrame frame2 = + this.createFrame( + stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); + stream2.notifyOnData(frame2); + this.waitSubscriberNotification(subscriber.onDataCalls, 2); + Assert.assertEquals(2, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + Assert.assertTrue(frame2 == subscriber.onDataCalls.get(1).frame); + } + + @Test + public void test_onData_withIdentifier() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreFrame frame1 = + this.createFrame( + stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + stream1.notifyOnData(frame1); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); + Assert.assertEquals(1, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + + this.plugModem( + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.MICHAUD)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreFrame frame2 = + this.createFrame( + stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); + stream2.notifyOnData(frame2); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); + Assert.assertEquals(1, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + } + + @Test + public void test_onError_any() throws TICCoreException, DataDictionaryException { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreError error1 = + new TICCoreError( + stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + stream1.notifyOnError(error1); + this.waitSubscriberNotification(subscriber.onErrorCalls, 1); + Assert.assertEquals(1, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + + this.plugModem( + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreError error2 = + new TICCoreError( + stream2.getIdentifier(), + TICCoreErrorCode.OTHER_REASON.getCode(), + "Error reading stream COM4"); + stream2.notifyOnError(error2); + this.waitSubscriberNotification(subscriber.onErrorCalls, 2); + Assert.assertEquals(2, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + Assert.assertTrue(error2 == subscriber.onErrorCalls.get(1).error); + } + + @Test + public void test_onError_withIdentifier() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreError error1 = + new TICCoreError( + stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + stream1.notifyOnError(error1); + this.waitSubscriberNotification(subscriber.onErrorCalls, 1); + Assert.assertEquals(1, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + + this.plugModem( + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreError error2 = + new TICCoreError( + stream2.getIdentifier(), + TICCoreErrorCode.OTHER_REASON.getCode(), + "Error reading stream COM4"); + stream2.notifyOnError(error2); + this.waitSubscriberNotification(subscriber.onErrorCalls, 1); + Assert.assertEquals(1, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + } + + @Test + public void test_getIdentifiers() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier1 = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + TICIdentifier identifier2 = + this.plugModem( + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + + TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); + + this.ticCore.subscribe(subscriber1); + List indentifierList1 = this.ticCore.getIndentifiers(subscriber1); + Assert.assertNotNull(indentifierList1); + Assert.assertEquals(2, indentifierList1.size()); + Assert.assertTrue(indentifierList1.contains(identifier1)); + Assert.assertTrue(indentifierList1.contains(identifier2)); + + this.ticCore.subscribe(identifier1, subscriber2); + List indentifierList2 = this.ticCore.getIndentifiers(subscriber2); + Assert.assertNotNull(indentifierList2); + Assert.assertEquals(1, indentifierList2.size()); + Assert.assertTrue(indentifierList2.contains(identifier1)); + + this.ticCore.subscribe(identifier2, subscriber3); + List indentifierList3 = this.ticCore.getIndentifiers(subscriber3); + Assert.assertNotNull(indentifierList3); + Assert.assertEquals(1, indentifierList3.size()); + Assert.assertTrue(indentifierList3.contains(identifier2)); + + this.ticCore.unsubscribe(subscriber1); + indentifierList1 = this.ticCore.getIndentifiers(subscriber1); + Assert.assertNotNull(indentifierList1); + Assert.assertEquals(0, indentifierList1.size()); + + this.ticCore.unsubscribe(identifier1, subscriber2); + indentifierList2 = this.ticCore.getIndentifiers(subscriber2); + Assert.assertNotNull(indentifierList2); + Assert.assertEquals(0, indentifierList2.size()); + + this.ticCore.unsubscribe(identifier2, subscriber3); + indentifierList3 = this.ticCore.getIndentifiers(subscriber3); + Assert.assertNotNull(indentifierList3); + Assert.assertEquals(0, indentifierList3.size()); + } + + private TICIdentifier plugModem(TICPortDescriptor descriptor) throws DataDictionaryException { + this.ticPortFinder.addDescriptor(descriptor); + this.waitPlugNotifierUpdate(); + + return new TICIdentifier(descriptor.getPortId(), descriptor.getPortName(), null); + } + + private void unplugModem(TICPortDescriptor descriptor) { + this.ticPortFinder.removeDescriptor(descriptor); + this.waitPlugNotifierUpdate(); + } + + private TICCoreFrame createFrame( + TICIdentifier identifier, TICMode mode, LocalDateTime localDateTime) + throws DataDictionaryException { + DataDictionaryBase content = new DataDictionaryBase(); + if (mode == TICMode.STANDARD) { + content.set("ADSC", identifier.getSerialNumber()); + } else { + content.set("ADCO", identifier.getSerialNumber()); + } + TICCoreFrame frame = new TICCoreFrame(identifier, mode, localDateTime, content); + + return frame; + } + + private void waitPlugNotifierUpdate() { + Time.sleep(2 * this.plugNotifierPeriod); + } + + private void waitTaskRunning(Task task) { + while (!task.isRunning()) { + Time.sleep(50); + } + } + + private void waitTaskTerminated(Task task) { + while (task.isRunning()) { + Time.sleep(50); + } + } + + private void waitReadNextFrameSubsbription() { + Time.sleep(100); + } + + private void waitSubscriberNotification(List onCalls, int expectedSize) { + long begin = System.nanoTime(); + long elapsed = 0; + while (onCalls.size() < expectedSize && elapsed < NOTIFICATION_TIMEOUT) { + Time.sleep(50); + elapsed = (System.nanoTime() - begin) / 1000000; + } + } } diff --git a/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java b/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java index 54744b7..4d49b45 100644 --- a/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java +++ b/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -9,46 +9,34 @@ import enedis.lab.util.task.TaskBase; -public class TICCoreReadNextFrameTask extends TaskBase -{ - public TICCore ticCore; - public TICIdentifier identifier; - public TICCoreFrame frame; - public Exception exception; - public int timeout; - - public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier) - { - this(ticCore, identifier, -1); - } - - public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier, int timeout) - { - super(); - this.ticCore = ticCore; - this.identifier = identifier; - this.timeout = timeout; - } - - @Override - protected void process() - { - try - { - if (this.timeout > 0) - { - this.frame = this.ticCore.readNextFrame(this.identifier, this.timeout); - } - else - { - this.frame = this.ticCore.readNextFrame(this.identifier); - } - } - catch (Exception exception) - { - this.exception = exception; - } - - } - +public class TICCoreReadNextFrameTask extends TaskBase { + public TICCore ticCore; + public TICIdentifier identifier; + public TICCoreFrame frame; + public Exception exception; + public int timeout; + + public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier) { + this(ticCore, identifier, -1); + } + + public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier, int timeout) { + super(); + this.ticCore = ticCore; + this.identifier = identifier; + this.timeout = timeout; + } + + @Override + protected void process() { + try { + if (this.timeout > 0) { + this.frame = this.ticCore.readNextFrame(this.identifier, this.timeout); + } else { + this.frame = this.ticCore.readNextFrame(this.identifier); + } + } catch (Exception exception) { + this.exception = exception; + } + } } diff --git a/src/test/java/enedis/tic/core/TICCoreStreamMock.java b/src/test/java/enedis/tic/core/TICCoreStreamMock.java index ce87b90..090aa2f 100644 --- a/src/test/java/enedis/tic/core/TICCoreStreamMock.java +++ b/src/test/java/enedis/tic/core/TICCoreStreamMock.java @@ -7,92 +7,77 @@ package enedis.tic.core; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionaryException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionaryException; - -public class TICCoreStreamMock implements TICCoreStream -{ - public static List streams = new ArrayList(); - public Collection subscribers; - public boolean running; - public TICIdentifier identifier; - - public TICCoreStreamMock(String portId, String portName, TICMode mode) throws DataDictionaryException - { - super(); - this.subscribers = new HashSet(); - this.running = false; - this.identifier = new TICIdentifier(portId, portName, null); - streams.add(this); - } - - @Override - public TICIdentifier getIdentifier() - { - return this.identifier; - } - - @Override - public boolean isRunning() - { - return this.running; - } - - @Override - public void start() - { - this.running = true; - } - - @Override - public void stop() - { - this.running = false; - } - - @Override - public Collection getSubscribers() - { - return this.subscribers; - } - - @Override - public boolean hasSubscriber(TICCoreSubscriber subscriber) - { - return this.subscribers.contains(subscriber); - } - - @Override - public void subscribe(TICCoreSubscriber subscriber) - { - this.subscribers.add(subscriber); - } - - @Override - public void unsubscribe(TICCoreSubscriber subscriber) - { - this.subscribers.remove(subscriber); - } - - public void notifyOnData(TICCoreFrame frame) - { - for (TICCoreSubscriber subscriber : this.subscribers) - { - subscriber.onData(frame); - } - } - - public void notifyOnError(TICCoreError error) - { - for (TICCoreSubscriber subscriber : this.subscribers) - { - subscriber.onError(error); - } - } - +public class TICCoreStreamMock implements TICCoreStream { + public static List streams = new ArrayList(); + public Collection subscribers; + public boolean running; + public TICIdentifier identifier; + + public TICCoreStreamMock(String portId, String portName, TICMode mode) + throws DataDictionaryException { + super(); + this.subscribers = new HashSet(); + this.running = false; + this.identifier = new TICIdentifier(portId, portName, null); + streams.add(this); + } + + @Override + public TICIdentifier getIdentifier() { + return this.identifier; + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public void start() { + this.running = true; + } + + @Override + public void stop() { + this.running = false; + } + + @Override + public Collection getSubscribers() { + return this.subscribers; + } + + @Override + public boolean hasSubscriber(TICCoreSubscriber subscriber) { + return this.subscribers.contains(subscriber); + } + + @Override + public void subscribe(TICCoreSubscriber subscriber) { + this.subscribers.add(subscriber); + } + + @Override + public void unsubscribe(TICCoreSubscriber subscriber) { + this.subscribers.remove(subscriber); + } + + public void notifyOnData(TICCoreFrame frame) { + for (TICCoreSubscriber subscriber : this.subscribers) { + subscriber.onData(frame); + } + } + + public void notifyOnError(TICCoreError error) { + for (TICCoreSubscriber subscriber : this.subscribers) { + subscriber.onError(error); + } + } } diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java b/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java index 6f4e0e9..6e5425d 100644 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java +++ b/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java @@ -10,21 +10,19 @@ import java.util.ArrayList; import java.util.List; -public class TICCoreSubscriberMock implements TICCoreSubscriber -{ - public List onDataCalls = new ArrayList(); - public List onErrorCalls = new ArrayList(); +public class TICCoreSubscriberMock implements TICCoreSubscriber { + public List onDataCalls = + new ArrayList(); + public List onErrorCalls = + new ArrayList(); - @Override - public void onData(TICCoreFrame frame) - { - this.onDataCalls.add(new TICCoreSubscriberOnDataCall(frame)); - } - - @Override - public void onError(TICCoreError error) - { - this.onErrorCalls.add(new TICCoreSubscriberOnErrorCall(error)); - } + @Override + public void onData(TICCoreFrame frame) { + this.onDataCalls.add(new TICCoreSubscriberOnDataCall(frame)); + } + @Override + public void onError(TICCoreError error) { + this.onErrorCalls.add(new TICCoreSubscriberOnErrorCall(error)); + } } diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java b/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java index 9341c46..aef62ab 100644 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java +++ b/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java @@ -10,14 +10,11 @@ import enedis.lab.mock.FunctionCall; @SuppressWarnings("javadoc") -public class TICCoreSubscriberOnDataCall extends FunctionCall -{ - public TICCoreFrame frame; - - public TICCoreSubscriberOnDataCall(TICCoreFrame frame) - { - super(); - this.frame = frame; - } +public class TICCoreSubscriberOnDataCall extends FunctionCall { + public TICCoreFrame frame; + public TICCoreSubscriberOnDataCall(TICCoreFrame frame) { + super(); + this.frame = frame; + } } diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java b/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java index 92111fc..8f8461f 100644 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java +++ b/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java @@ -10,14 +10,11 @@ import enedis.lab.mock.FunctionCall; @SuppressWarnings("javadoc") -public class TICCoreSubscriberOnErrorCall extends FunctionCall -{ - public TICCoreError error; - - public TICCoreSubscriberOnErrorCall(TICCoreError error) - { - super(); - this.error = error; - } +public class TICCoreSubscriberOnErrorCall extends FunctionCall { + public TICCoreError error; + public TICCoreSubscriberOnErrorCall(TICCoreError error) { + super(); + this.error = error; + } } diff --git a/src/test/java/enedis/tic/core/TICIdentifierTest.java b/src/test/java/enedis/tic/core/TICIdentifierTest.java index af522a6..6f7d699 100644 --- a/src/test/java/enedis/tic/core/TICIdentifierTest.java +++ b/src/test/java/enedis/tic/core/TICIdentifierTest.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,181 +7,161 @@ package enedis.tic.core; +import enedis.lab.types.DataDictionaryException; import org.junit.Assert; import org.junit.Test; -import enedis.lab.types.DataDictionaryException; - -public class TICIdentifierTest -{ - @Test - public void test_constructor_full() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier("{1-1}", "COM3", "021976551632"); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - Assert.assertEquals("COM3", identifier.getPortName()); - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlyPortId() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - Assert.assertNull(identifier.getPortName()); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlyPortName() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, "COM3", null); - - Assert.assertNull(identifier.getPortId()); - Assert.assertEquals("COM3", identifier.getPortName()); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlySerialNumber() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertNull(identifier.getPortId()); - Assert.assertNull(identifier.getPortName()); - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - } - - @Test(expected = DataDictionaryException.class) - public void test_constructor_empty() throws DataDictionaryException - { - new TICIdentifier(null, null, null); - } - - @Test - public void test_setPortId_null_notEmpty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, "021976551632"); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - identifier.setPortId(null); - Assert.assertNull(identifier.getPortId()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setPortId_null_empty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - identifier.setPortId(null); - } - - @Test - public void test_setPortName_null_notEmpty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); - - Assert.assertEquals("COM3", identifier.getPortName()); - identifier.setPortName(null); - Assert.assertNull(identifier.getPortName()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setPortName_null_empty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, "COM3", null); - - Assert.assertEquals("COM3", identifier.getPortName()); - identifier.setPortName(null); - } - - @Test - public void test_setSerialNumber_null_notEmpty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); - - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - identifier.setSerialNumber(null); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setSerialNumber_null_empty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - identifier.setSerialNumber(null); - } - - @Test - public void test_matches_null() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertFalse(identifier.matches(null)); - } - - @Test - public void test_matches_same_serialNumber() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM4", "021976551632"); - - Assert.assertTrue(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_serialNumber() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM3", "021976551638"); - - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_same_portId() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM4", "021976551633"); - TICIdentifier identifier3 = new TICIdentifier("{1-1}", "COM5", null); - - Assert.assertTrue(identifier1.matches(identifier3)); - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_portId() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", null, null); - TICIdentifier identifier2 = new TICIdentifier("{1-7}", null, null); - - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_same_portName() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM3", "021976551633"); - TICIdentifier identifier3 = new TICIdentifier("{1-3}", "COM3", null); - TICIdentifier identifier4 = new TICIdentifier(null, "COM3", null); - - Assert.assertTrue(identifier1.matches(identifier4)); - Assert.assertFalse(identifier1.matches(identifier3)); - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_portName() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier(null, "COM3", null); - TICIdentifier identifier2 = new TICIdentifier(null, "COM8", null); - - Assert.assertFalse(identifier1.matches(identifier2)); - } +public class TICIdentifierTest { + @Test + public void test_constructor_full() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier("{1-1}", "COM3", "021976551632"); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + Assert.assertEquals("COM3", identifier.getPortName()); + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlyPortId() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + Assert.assertNull(identifier.getPortName()); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlyPortName() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, "COM3", null); + + Assert.assertNull(identifier.getPortId()); + Assert.assertEquals("COM3", identifier.getPortName()); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlySerialNumber() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); + + Assert.assertNull(identifier.getPortId()); + Assert.assertNull(identifier.getPortName()); + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + } + + @Test(expected = DataDictionaryException.class) + public void test_constructor_empty() throws DataDictionaryException { + new TICIdentifier(null, null, null); + } + + @Test + public void test_setPortId_null_notEmpty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier("{1-1}", null, "021976551632"); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + identifier.setPortId(null); + Assert.assertNull(identifier.getPortId()); + } + + @Test(expected = DataDictionaryException.class) + public void test_setPortId_null_empty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + identifier.setPortId(null); + } + + @Test + public void test_setPortName_null_notEmpty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); + + Assert.assertEquals("COM3", identifier.getPortName()); + identifier.setPortName(null); + Assert.assertNull(identifier.getPortName()); + } + + @Test(expected = DataDictionaryException.class) + public void test_setPortName_null_empty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, "COM3", null); + + Assert.assertEquals("COM3", identifier.getPortName()); + identifier.setPortName(null); + } + + @Test + public void test_setSerialNumber_null_notEmpty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); + + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + identifier.setSerialNumber(null); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test(expected = DataDictionaryException.class) + public void test_setSerialNumber_null_empty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); + + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + identifier.setSerialNumber(null); + } + + @Test + public void test_matches_null() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); + + Assert.assertFalse(identifier.matches(null)); + } + + @Test + public void test_matches_same_serialNumber() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM4", "021976551632"); + + Assert.assertTrue(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_serialNumber() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM3", "021976551638"); + + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_same_portId() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM4", "021976551633"); + TICIdentifier identifier3 = new TICIdentifier("{1-1}", "COM5", null); + + Assert.assertTrue(identifier1.matches(identifier3)); + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_portId() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", null, null); + TICIdentifier identifier2 = new TICIdentifier("{1-7}", null, null); + + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_same_portName() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM3", "021976551633"); + TICIdentifier identifier3 = new TICIdentifier("{1-3}", "COM3", null); + TICIdentifier identifier4 = new TICIdentifier(null, "COM3", null); + + Assert.assertTrue(identifier1.matches(identifier4)); + Assert.assertFalse(identifier1.matches(identifier3)); + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_portName() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier(null, "COM3", null); + TICIdentifier identifier2 = new TICIdentifier(null, "COM8", null); + + Assert.assertFalse(identifier1.matches(identifier2)); + } } diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java index 2974e57..307cd1f 100644 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java +++ b/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java @@ -7,9 +7,6 @@ package enedis.tic.service.message; -import org.junit.Assert; -import org.junit.Test; - import enedis.lab.util.message.Message; import enedis.lab.util.message.MessageType; import enedis.lab.util.message.exception.MessageInvalidContentException; @@ -19,57 +16,74 @@ import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; import enedis.lab.util.message.exception.UnsupportedMessageException; import enedis.tic.service.message.factory.TIC2WebSocketEventFactory; +import org.junit.Assert; +import org.junit.Test; @SuppressWarnings("javadoc") -public class TIC2WebSocketEventFactoryTest -{ - @Test - public void testGetMessage_EventOnTICData() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketEventFactory factory = new TIC2WebSocketEventFactory(); - - String text = "{\"type\":\"" + MessageType.EVENT.name() + "\",\"name\":\"" + EventOnTICData.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"data\":{\"captureDateTime\":\"01/01/2000 00:00:00\",\"mode\":\"STANDARD\",\"identifier\":{\"serialNumber\":\"010203040506\"},\"content\":{\"URMS\":230}}}"; - - Message message = factory.getMessage(text, EventOnTICData.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof EventOnTICData); - - EventOnTICData event = (EventOnTICData) message; - - Assert.assertEquals(MessageType.EVENT, event.getType()); - Assert.assertEquals(EventOnTICData.NAME, event.getName()); - Assert.assertNotNull(event.getData()); - - Message messageDecoded = factory.getMessage(event.toString(), EventOnTICData.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(event)); - } - - @Test - public void testGetMessage_EventOnError() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketEventFactory factory = new TIC2WebSocketEventFactory(); - - String text = "{\"type\":\"" + MessageType.EVENT.name() + "\",\"name\":\"" + EventOnError.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"data\":{\"errorCode\":\"-1\",\"errorMessage\":\"une erreur\",\"identifier\":{\"serialNumber\":\"010203040506\"}}}"; - - Message message = factory.getMessage(text, EventOnError.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof EventOnError); - - EventOnError event = (EventOnError) message; - - Assert.assertEquals(MessageType.EVENT, event.getType()); - Assert.assertEquals(EventOnError.NAME, event.getName()); - Assert.assertNotNull(event.getData()); - - Message messageDecoded = factory.getMessage(event.toString(), EventOnError.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(event)); - } +public class TIC2WebSocketEventFactoryTest { + @Test + public void testGetMessage_EventOnTICData() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketEventFactory factory = new TIC2WebSocketEventFactory(); + + String text = + "{\"type\":\"" + + MessageType.EVENT.name() + + "\",\"name\":\"" + + EventOnTICData.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"data\":{\"captureDateTime\":\"01/01/2000 00:00:00\",\"mode\":\"STANDARD\",\"identifier\":{\"serialNumber\":\"010203040506\"},\"content\":{\"URMS\":230}}}"; + + Message message = factory.getMessage(text, EventOnTICData.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof EventOnTICData); + + EventOnTICData event = (EventOnTICData) message; + + Assert.assertEquals(MessageType.EVENT, event.getType()); + Assert.assertEquals(EventOnTICData.NAME, event.getName()); + Assert.assertNotNull(event.getData()); + + Message messageDecoded = factory.getMessage(event.toString(), EventOnTICData.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(event)); + } + + @Test + public void testGetMessage_EventOnError() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketEventFactory factory = new TIC2WebSocketEventFactory(); + + String text = + "{\"type\":\"" + + MessageType.EVENT.name() + + "\",\"name\":\"" + + EventOnError.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"data\":{\"errorCode\":\"-1\",\"errorMessage\":\"une erreur\",\"identifier\":{\"serialNumber\":\"010203040506\"}}}"; + + Message message = factory.getMessage(text, EventOnError.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof EventOnError); + + EventOnError event = (EventOnError) message; + + Assert.assertEquals(MessageType.EVENT, event.getType()); + Assert.assertEquals(EventOnError.NAME, event.getName()); + Assert.assertNotNull(event.getData()); + + Message messageDecoded = factory.getMessage(event.toString(), EventOnError.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(event)); + } } diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java index 0431191..cf27081 100644 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java +++ b/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,8 +7,6 @@ package enedis.tic.service.message; -import org.junit.Test; - import enedis.lab.util.message.MessageType; import enedis.lab.util.message.exception.MessageInvalidContentException; import enedis.lab.util.message.exception.MessageInvalidFormatException; @@ -17,18 +15,22 @@ import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; import enedis.lab.util.message.exception.UnsupportedMessageException; import enedis.tic.service.message.factory.TIC2WebSocketMessageFactory; +import org.junit.Test; @SuppressWarnings("javadoc") -public class TIC2WebSocketMessageFactoryTest -{ - @Test(expected = UnsupportedMessageException.class) - public void testGetMessage_UnsupportedRequest() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketMessageFactory factory = new TIC2WebSocketMessageFactory(); +public class TIC2WebSocketMessageFactoryTest { + @Test(expected = UnsupportedMessageException.class) + public void testGetMessage_UnsupportedRequest() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketMessageFactory factory = new TIC2WebSocketMessageFactory(); - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"coucou\"}"; + String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"coucou\"}"; - factory.getMessage(text); - } + factory.getMessage(text); + } } diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java index 2ddd38c..718653c 100644 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java +++ b/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,9 +7,6 @@ package enedis.tic.service.message; -import org.junit.Assert; -import org.junit.Test; - import enedis.lab.util.message.Message; import enedis.lab.util.message.MessageType; import enedis.lab.util.message.exception.MessageInvalidContentException; @@ -19,221 +16,301 @@ import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; import enedis.lab.util.message.exception.UnsupportedMessageException; import enedis.tic.service.message.factory.TIC2WebSocketRequestFactory; +import org.junit.Assert; +import org.junit.Test; @SuppressWarnings("javadoc") -public class TIC2WebSocketRequestFactoryTest -{ - @Test - public void testGetMessage_RequestGetAvailableTics() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestGetAvailableTICs.NAME + "\"}"; - - Message message = factory.getMessage(text, RequestGetAvailableTICs.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestGetAvailableTICs); - - RequestGetAvailableTICs request = (RequestGetAvailableTICs) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestGetAvailableTICs.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestGetAvailableTICs.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestGetModemsInfo() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestGetModemsInfo.NAME + "\"}"; - - Message message = factory.getMessage(text, RequestGetModemsInfo.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestGetModemsInfo); - - RequestGetModemsInfo request = (RequestGetModemsInfo) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestGetModemsInfo.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestGetModemsInfo.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestReadTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestReadTIC.NAME + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; - - Message message = factory.getMessage(text, RequestReadTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestReadTIC); - - RequestReadTIC request = (RequestReadTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestReadTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestReadTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestSubscribeTIC_allTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestSubscribeTIC.NAME + "\"}"; - - Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestSubscribeTIC); - - RequestSubscribeTIC request = (RequestSubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestSubscribeTIC_oneTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestSubscribeTIC.NAME + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; - - Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestSubscribeTIC); - - RequestSubscribeTIC request = (RequestSubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestSubscribeTIC_severalTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestSubscribeTIC.NAME - + "\",\"data\":[{\"serialNumber\":\"010203040506\"},{\"portId\":\"1-1\"}]}"; - - Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestSubscribeTIC); - - RequestSubscribeTIC request = (RequestSubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestUnsubscribeTIC_allTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestUnsubscribeTIC.NAME + "\"}"; - - Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestUnsubscribeTIC); - - RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestUnsubscribeTIC_oneTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestUnsubscribeTIC.NAME + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; - - Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestUnsubscribeTIC); - - RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestUnsubscribeTIC_severalTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestUnsubscribeTIC.NAME - + "\",\"data\":[{\"serialNumber\":\"010203040506\"},{\"portId\":\"1-1\"}]}"; - - Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestUnsubscribeTIC); - - RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } +public class TIC2WebSocketRequestFactoryTest { + @Test + public void testGetMessage_RequestGetAvailableTics() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = + "{\"type\":\"" + + MessageType.REQUEST.name() + + "\",\"name\":\"" + + RequestGetAvailableTICs.NAME + + "\"}"; + + Message message = factory.getMessage(text, RequestGetAvailableTICs.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestGetAvailableTICs); + + RequestGetAvailableTICs request = (RequestGetAvailableTICs) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestGetAvailableTICs.NAME, request.getName()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestGetAvailableTICs.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestGetModemsInfo() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = + "{\"type\":\"" + + MessageType.REQUEST.name() + + "\",\"name\":\"" + + RequestGetModemsInfo.NAME + + "\"}"; + + Message message = factory.getMessage(text, RequestGetModemsInfo.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestGetModemsInfo); + + RequestGetModemsInfo request = (RequestGetModemsInfo) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestGetModemsInfo.NAME, request.getName()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestGetModemsInfo.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestReadTIC() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = + "{\"type\":\"" + + MessageType.REQUEST.name() + + "\",\"name\":\"" + + RequestReadTIC.NAME + + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; + + Message message = factory.getMessage(text, RequestReadTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestReadTIC); + + RequestReadTIC request = (RequestReadTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestReadTIC.NAME, request.getName()); + Assert.assertNotNull(request.getData()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestReadTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestSubscribeTIC_allTIC() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = + "{\"type\":\"" + + MessageType.REQUEST.name() + + "\",\"name\":\"" + + RequestSubscribeTIC.NAME + + "\"}"; + + Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestSubscribeTIC); + + RequestSubscribeTIC request = (RequestSubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestSubscribeTIC_oneTIC() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = + "{\"type\":\"" + + MessageType.REQUEST.name() + + "\",\"name\":\"" + + RequestSubscribeTIC.NAME + + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; + + Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestSubscribeTIC); + + RequestSubscribeTIC request = (RequestSubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); + Assert.assertNotNull(request.getData()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestSubscribeTIC_severalTIC() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = + "{\"type\":\"" + + MessageType.REQUEST.name() + + "\",\"name\":\"" + + RequestSubscribeTIC.NAME + + "\",\"data\":[{\"serialNumber\":\"010203040506\"},{\"portId\":\"1-1\"}]}"; + + Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestSubscribeTIC); + + RequestSubscribeTIC request = (RequestSubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); + Assert.assertNotNull(request.getData()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestUnsubscribeTIC_allTIC() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = + "{\"type\":\"" + + MessageType.REQUEST.name() + + "\",\"name\":\"" + + RequestUnsubscribeTIC.NAME + + "\"}"; + + Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestUnsubscribeTIC); + + RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestUnsubscribeTIC_oneTIC() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = + "{\"type\":\"" + + MessageType.REQUEST.name() + + "\",\"name\":\"" + + RequestUnsubscribeTIC.NAME + + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; + + Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestUnsubscribeTIC); + + RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); + Assert.assertNotNull(request.getData()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } + + @Test + public void testGetMessage_RequestUnsubscribeTIC_severalTIC() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); + + String text = + "{\"type\":\"" + + MessageType.REQUEST.name() + + "\",\"name\":\"" + + RequestUnsubscribeTIC.NAME + + "\",\"data\":[{\"serialNumber\":\"010203040506\"},{\"portId\":\"1-1\"}]}"; + + Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof RequestUnsubscribeTIC); + + RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; + + Assert.assertEquals(MessageType.REQUEST, request.getType()); + Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); + Assert.assertNotNull(request.getData()); + + Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(request)); + } } diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java index dc8068e..f75c77e 100644 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java +++ b/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java @@ -7,9 +7,6 @@ package enedis.tic.service.message; -import org.junit.Assert; -import org.junit.Test; - import enedis.lab.util.message.Message; import enedis.lab.util.message.MessageType; import enedis.lab.util.message.exception.MessageInvalidContentException; @@ -19,143 +16,186 @@ import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; import enedis.lab.util.message.exception.UnsupportedMessageException; import enedis.tic.service.message.factory.TIC2WebSocketResponseFactory; +import org.junit.Assert; +import org.junit.Test; @SuppressWarnings("javadoc") -public class TIC2WebSocketResponseFactoryTest -{ - @Test - public void testGetMessage_ResponseGetAvailableTics() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseGetAvailableTICs.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"data\":[{\"serialNumber\":\"010203040506\"}]}"; - - Message message = factory.getMessage(text, ResponseGetAvailableTICs.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseGetAvailableTICs); - - ResponseGetAvailableTICs response = (ResponseGetAvailableTICs) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseGetAvailableTICs.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - Assert.assertNotNull(response.getData()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseGetAvailableTICs.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseGetModemsInfo() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseGetModemsInfo.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"errorMessage\":\"error\"}"; - - Message message = factory.getMessage(text, ResponseGetModemsInfo.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseGetModemsInfo); - - ResponseGetModemsInfo response = (ResponseGetModemsInfo) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseGetModemsInfo.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertEquals("error", response.getErrorMessage()); - Assert.assertNull(response.getData()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseGetModemsInfo.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseReadTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseReadTIC.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"data\":{\"captureDateTime\":\"01/01/2000 00:00:00\",\"mode\":\"STANDARD\",\"identifier\":{\"serialNumber\":\"010203040506\"},\"content\":{\"URMS\":230}}}"; - - Message message = factory.getMessage(text, ResponseReadTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseReadTIC); - - ResponseReadTIC response = (ResponseReadTIC) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseReadTIC.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - Assert.assertNotNull(response.getData()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseReadTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseSubscribeTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseSubscribeTIC.NAME + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0}"; - - Message message = factory.getMessage(text, ResponseSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseSubscribeTIC); - - ResponseSubscribeTIC response = (ResponseSubscribeTIC) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseSubscribeTIC.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseUnsubscribeTIC_allTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseUnsubscribeTIC.NAME + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0}"; - - Message message = factory.getMessage(text, ResponseUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseUnsubscribeTIC); - - ResponseUnsubscribeTIC response = (ResponseUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseUnsubscribeTIC.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } +public class TIC2WebSocketResponseFactoryTest { + @Test + public void testGetMessage_ResponseGetAvailableTics() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); + + String text = + "{\"type\":\"" + + MessageType.RESPONSE.name() + + "\",\"name\":\"" + + ResponseGetAvailableTICs.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"data\":[{\"serialNumber\":\"010203040506\"}]}"; + + Message message = factory.getMessage(text, ResponseGetAvailableTICs.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof ResponseGetAvailableTICs); + + ResponseGetAvailableTICs response = (ResponseGetAvailableTICs) message; + + Assert.assertEquals(MessageType.RESPONSE, response.getType()); + Assert.assertEquals(ResponseGetAvailableTICs.NAME, response.getName()); + Assert.assertNotNull(response.getDateTime()); + Assert.assertEquals(0, response.getErrorCode().intValue()); + Assert.assertNull(response.getErrorMessage()); + Assert.assertNotNull(response.getData()); + + Message messageDecoded = factory.getMessage(response.toString(), ResponseGetAvailableTICs.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(response)); + } + + @Test + public void testGetMessage_ResponseGetModemsInfo() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); + + String text = + "{\"type\":\"" + + MessageType.RESPONSE.name() + + "\",\"name\":\"" + + ResponseGetModemsInfo.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"errorMessage\":\"error\"}"; + + Message message = factory.getMessage(text, ResponseGetModemsInfo.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof ResponseGetModemsInfo); + + ResponseGetModemsInfo response = (ResponseGetModemsInfo) message; + + Assert.assertEquals(MessageType.RESPONSE, response.getType()); + Assert.assertEquals(ResponseGetModemsInfo.NAME, response.getName()); + Assert.assertNotNull(response.getDateTime()); + Assert.assertEquals(0, response.getErrorCode().intValue()); + Assert.assertEquals("error", response.getErrorMessage()); + Assert.assertNull(response.getData()); + + Message messageDecoded = factory.getMessage(response.toString(), ResponseGetModemsInfo.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(response)); + } + + @Test + public void testGetMessage_ResponseReadTIC() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); + + String text = + "{\"type\":\"" + + MessageType.RESPONSE.name() + + "\",\"name\":\"" + + ResponseReadTIC.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"data\":{\"captureDateTime\":\"01/01/2000 00:00:00\",\"mode\":\"STANDARD\",\"identifier\":{\"serialNumber\":\"010203040506\"},\"content\":{\"URMS\":230}}}"; + + Message message = factory.getMessage(text, ResponseReadTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof ResponseReadTIC); + + ResponseReadTIC response = (ResponseReadTIC) message; + + Assert.assertEquals(MessageType.RESPONSE, response.getType()); + Assert.assertEquals(ResponseReadTIC.NAME, response.getName()); + Assert.assertNotNull(response.getDateTime()); + Assert.assertEquals(0, response.getErrorCode().intValue()); + Assert.assertNull(response.getErrorMessage()); + Assert.assertNotNull(response.getData()); + + Message messageDecoded = factory.getMessage(response.toString(), ResponseReadTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(response)); + } + + @Test + public void testGetMessage_ResponseSubscribeTIC() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); + + String text = + "{\"type\":\"" + + MessageType.RESPONSE.name() + + "\",\"name\":\"" + + ResponseSubscribeTIC.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0}"; + + Message message = factory.getMessage(text, ResponseSubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof ResponseSubscribeTIC); + + ResponseSubscribeTIC response = (ResponseSubscribeTIC) message; + + Assert.assertEquals(MessageType.RESPONSE, response.getType()); + Assert.assertEquals(ResponseSubscribeTIC.NAME, response.getName()); + Assert.assertNotNull(response.getDateTime()); + Assert.assertEquals(0, response.getErrorCode().intValue()); + Assert.assertNull(response.getErrorMessage()); + + Message messageDecoded = factory.getMessage(response.toString(), ResponseSubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(response)); + } + + @Test + public void testGetMessage_ResponseUnsubscribeTIC_allTIC() + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException, + UnsupportedMessageException, + MessageInvalidContentException { + TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); + + String text = + "{\"type\":\"" + + MessageType.RESPONSE.name() + + "\",\"name\":\"" + + ResponseUnsubscribeTIC.NAME + + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0}"; + + Message message = factory.getMessage(text, ResponseUnsubscribeTIC.NAME); + + Assert.assertNotNull(message); + Assert.assertTrue(message instanceof ResponseUnsubscribeTIC); + + ResponseUnsubscribeTIC response = (ResponseUnsubscribeTIC) message; + + Assert.assertEquals(MessageType.RESPONSE, response.getType()); + Assert.assertEquals(ResponseUnsubscribeTIC.NAME, response.getName()); + Assert.assertNotNull(response.getDateTime()); + Assert.assertEquals(0, response.getErrorCode().intValue()); + Assert.assertNull(response.getErrorMessage()); + + Message messageDecoded = factory.getMessage(response.toString(), ResponseUnsubscribeTIC.NAME); + Assert.assertNotNull(message); + Assert.assertTrue(messageDecoded.equals(response)); + } } diff --git a/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java b/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java index 948283a..efdb4b4 100644 --- a/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java +++ b/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java @@ -1,5 +1,5 @@ // Copyright (C) 2025 Enedis Smarties team -// +// // SPDX-FileContributor: Jehan BOUSCH // SPDX-FileContributor: Mathieu SABARTHES // @@ -7,33 +7,31 @@ package enedis.tic.service.netty; -import org.junit.Test; import static org.junit.Assert.*; +import enedis.lab.protocol.tic.TICMode; +import enedis.tic.core.TICCoreBase; import enedis.tic.service.client.TIC2WebSocketClientPool; import enedis.tic.service.client.TIC2WebSocketClientPoolBase; import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandlerBase; -import enedis.tic.core.TICCoreBase; -import enedis.lab.protocol.tic.TICMode; import java.util.Arrays; +import org.junit.Test; -/** - * Test for TIC2WebSocketServer - */ -public class TIC2WebSocketServerTest -{ - @Test - public void testServerInstantiation() - { - // Create mock dependencies - TIC2WebSocketClientPool clientPool = new TIC2WebSocketClientPoolBase(); - TIC2WebSocketRequestHandler requestHandler = new TIC2WebSocketRequestHandlerBase( - new TICCoreBase(TICMode.HISTORIC, Arrays.asList("COM1"))); +/** Test for TIC2WebSocketServer */ +public class TIC2WebSocketServerTest { + @Test + public void testServerInstantiation() { + // Create mock dependencies + TIC2WebSocketClientPool clientPool = new TIC2WebSocketClientPoolBase(); + TIC2WebSocketRequestHandler requestHandler = + new TIC2WebSocketRequestHandlerBase( + new TICCoreBase(TICMode.HISTORIC, Arrays.asList("COM1"))); - // Test server creation - TIC2WebSocketServer server = new TIC2WebSocketServer("localhost", 8080, clientPool, requestHandler); + // Test server creation + TIC2WebSocketServer server = + new TIC2WebSocketServer("localhost", 8080, clientPool, requestHandler); - assertNotNull("Server should not be null", server); - } + assertNotNull("Server should not be null", server); + } } From 4d375d28b4ea03158bfa79c8d9ce5ab22b4f1adb Mon Sep 17 00:00:00 2001 From: MathieuSabarthes Date: Wed, 8 Oct 2025 17:40:43 +0200 Subject: [PATCH 27/59] chore: add javadoc --- .../java/enedis/lab/util/MinMaxChecker.java | 16 +++- .../java/enedis/lab/util/SystemError.java | 16 +++- src/main/java/enedis/lab/util/SystemLibC.java | 16 +++- .../java/enedis/lab/util/message/Event.java | 15 +++- .../java/enedis/lab/util/message/Message.java | 14 +++- .../enedis/lab/util/message/MessageType.java | 15 +++- .../java/enedis/lab/util/message/Request.java | 15 +++- .../enedis/lab/util/message/Response.java | 16 +++- .../enedis/lab/util/message/ResponseBase.java | 16 +++- .../message/exception/MessageException.java | 47 +++++++++--- .../MessageInvalidContentException.java | 47 +++++++++--- .../MessageInvalidFormatException.java | 47 +++++++++--- .../MessageInvalidTypeException.java | 47 +++++++++--- .../MessageKeyNameDoesntExistException.java | 47 +++++++++--- .../MessageKeyTypeDoesntExistException.java | 47 +++++++++--- .../UnsupportedMessageException.java | 47 +++++++++--- .../factory/AbstractMessageFactory.java | 51 +++++++++---- .../message/factory/BasicMessageFactory.java | 37 ++++++++-- .../util/message/factory/EventFactory.java | 26 ++++++- .../util/message/factory/MessageFactory.java | 73 +++++++++++++------ .../util/message/factory/RequestFactory.java | 23 +++++- .../util/message/factory/ResponseFactory.java | 26 ++++++- .../lab/util/task/FilteredNotifier.java | 17 ++++- .../lab/util/task/FilteredNotifierBase.java | 14 +++- .../java/enedis/lab/util/task/Notifier.java | 16 +++- .../enedis/lab/util/task/NotifierBase.java | 13 +++- .../java/enedis/lab/util/task/Subscriber.java | 16 +++- src/main/java/enedis/lab/util/task/Task.java | 16 +++- .../java/enedis/lab/util/task/TaskBase.java | 16 +++- .../enedis/lab/util/task/TaskPeriodic.java | 16 +++- .../task/TaskPeriodicWithSubscribers.java | 13 +++- src/main/java/enedis/lab/util/time/Time.java | 16 +++- .../tic/core/ReadNextFrameSubscriber.java | 15 ++++ src/main/java/enedis/tic/core/TICCore.java | 15 +++- .../java/enedis/tic/core/TICCoreBase.java | 16 +++- .../java/enedis/tic/core/TICCoreError.java | 14 +++- .../enedis/tic/core/TICCoreErrorCode.java | 24 ++++++ .../enedis/tic/core/TICCoreException.java | 8 ++ .../java/enedis/tic/core/TICCoreFrame.java | 14 +++- .../java/enedis/tic/core/TICCoreStream.java | 16 +++- .../enedis/tic/core/TICCoreStreamBase.java | 16 +++- .../enedis/tic/core/TICCoreSubscriber.java | 16 +++- .../java/enedis/tic/core/TICIdentifier.java | 14 +++- 43 files changed, 876 insertions(+), 149 deletions(-) diff --git a/src/main/java/enedis/lab/util/MinMaxChecker.java b/src/main/java/enedis/lab/util/MinMaxChecker.java index cb3281f..d945ae4 100644 --- a/src/main/java/enedis/lab/util/MinMaxChecker.java +++ b/src/main/java/enedis/lab/util/MinMaxChecker.java @@ -7,7 +7,21 @@ package enedis.lab.util; -/** Min max class */ +/** + * Utility class for checking if values are within a specified numeric range. + * + *

    This class provides methods to set minimum and maximum bounds and to check whether a given value + * falls within the defined range. It is designed for general-purpose range validation. + * + *

    Common use cases include: + *

      + *
    • Validating input values against numeric constraints
    • + *
    • Ensuring values remain within allowed limits
    • + *
    • Supporting configuration or parameter validation
    • + *
    + * + * @author Enedis Smarties team + */ public class MinMaxChecker { private Number min; private Number max; diff --git a/src/main/java/enedis/lab/util/SystemError.java b/src/main/java/enedis/lab/util/SystemError.java index 882afe1..ef4ca9c 100644 --- a/src/main/java/enedis/lab/util/SystemError.java +++ b/src/main/java/enedis/lab/util/SystemError.java @@ -11,7 +11,21 @@ import com.sun.jna.platform.win32.Kernel32Util; import org.apache.commons.lang3.SystemUtils; -/** Class for system error */ +/** + * Utility class for retrieving system error codes and messages. + * + *

    This class provides static methods to obtain the last system error code and its associated message, + * supporting multiple operating systems. It is designed for general-purpose error handling and diagnostics. + * + *

    Common use cases include: + *

      + *
    • Retrieving the last system error code
    • + *
    • Obtaining human-readable error messages
    • + *
    • Supporting cross-platform error diagnostics
    • + *
    + * + * @author Enedis Smarties team + */ public class SystemError { /** * Get system last error code diff --git a/src/main/java/enedis/lab/util/SystemLibC.java b/src/main/java/enedis/lab/util/SystemLibC.java index 13f97dd..5a3b769 100644 --- a/src/main/java/enedis/lab/util/SystemLibC.java +++ b/src/main/java/enedis/lab/util/SystemLibC.java @@ -10,7 +10,21 @@ import com.sun.jna.Library; import com.sun.jna.Native; -/** Interface for system of C library */ +/** + * Interface for accessing native C library functions via JNA. + * + *

    This interface provides access to system-level C library functions, such as retrieving error messages + * associated with error codes. It is designed for general-purpose native integration and diagnostics. + * + *

    Common use cases include: + *

      + *
    • Obtaining human-readable error messages from system error codes
    • + *
    • Integrating with native system libraries
    • + *
    • Supporting cross-platform diagnostics
    • + *
    + * + * @author Enedis Smarties team + */ public interface SystemLibC extends Library { /** Instance */ SystemLibC INSTANCE = Native.load("c", SystemLibC.class); diff --git a/src/main/java/enedis/lab/util/message/Event.java b/src/main/java/enedis/lab/util/message/Event.java index 0e7ed75..b6eec9f 100644 --- a/src/main/java/enedis/lab/util/message/Event.java +++ b/src/main/java/enedis/lab/util/message/Event.java @@ -17,9 +17,20 @@ import java.util.Map; /** - * Event class + * Abstract base class for event messages. * - *

    Generated + *

    This class represents event messages. It provides support for event-specific fields such as date/time and + * enforces the accepted message type. Subclasses can define additional event data and behavior. + * + *

    Common use cases include: + *

      + *
    • Representing system or application events
    • + *
    • Handling event notifications in the message pipeline
    • + *
    • Extending for custom event types
    • + *
    + * + * @author Enedis Smarties team + * @see Message */ public abstract class Event extends Message { protected static final String KEY_DATE_TIME = "dateTime"; diff --git a/src/main/java/enedis/lab/util/message/Message.java b/src/main/java/enedis/lab/util/message/Message.java index 64e6235..a5ca362 100644 --- a/src/main/java/enedis/lab/util/message/Message.java +++ b/src/main/java/enedis/lab/util/message/Message.java @@ -18,9 +18,19 @@ import java.util.Map; /** - * Message class + * Base class for all messages. * - *

    Generated + *

    This class provides the core structure for messages, including type and name fields. It supports construction from maps and data dictionaries, + * and can be extended for specific message types such as requests, responses, and events. + * + *

    Common use cases include: + *

      + *
    • Representing generic messages in the communication pipeline
    • + *
    • Providing a base for request, response, and event messages
    • + *
    • Supporting extensible message structures
    • + *
    + * + * @author Enedis Smarties team */ public class Message extends DataDictionaryBase { /** Key type */ diff --git a/src/main/java/enedis/lab/util/message/MessageType.java b/src/main/java/enedis/lab/util/message/MessageType.java index fdec4e4..cd13db7 100644 --- a/src/main/java/enedis/lab/util/message/MessageType.java +++ b/src/main/java/enedis/lab/util/message/MessageType.java @@ -7,7 +7,20 @@ package enedis.lab.util.message; -/** Message type */ +/** + * Enumeration of message types. + * + *

    This enum defines the supported message types, including events, requests, and responses. It is used to categorize + * messages and control their processing in the communication pipeline. + * + *

      + *
    • {@link #EVENT} - Event messages
    • + *
    • {@link #REQUEST} - Request messages
    • + *
    • {@link #RESPONSE} - Response messages
    • + *
    + * + * @author Enedis Smarties team + */ public enum MessageType { /** Event */ EVENT, diff --git a/src/main/java/enedis/lab/util/message/Request.java b/src/main/java/enedis/lab/util/message/Request.java index b7a7950..64a3156 100644 --- a/src/main/java/enedis/lab/util/message/Request.java +++ b/src/main/java/enedis/lab/util/message/Request.java @@ -15,9 +15,20 @@ import java.util.Map; /** - * Request class + * Abstract base class for request messages. * - *

    Generated + *

    This class represents request messages. It enforces the accepted message type and provides support for request-specific + * fields. Subclasses can define additional request data and behavior. + * + *

    Common use cases include: + *

      + *
    • Representing client requests for data or actions
    • + *
    • Handling request validation and processing
    • + *
    • Extending for custom request types
    • + *
    + * + * @author Enedis Smarties team + * @see Message */ public abstract class Request extends Message { private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.REQUEST; diff --git a/src/main/java/enedis/lab/util/message/Response.java b/src/main/java/enedis/lab/util/message/Response.java index 0c1cf55..1bb4596 100644 --- a/src/main/java/enedis/lab/util/message/Response.java +++ b/src/main/java/enedis/lab/util/message/Response.java @@ -19,9 +19,21 @@ import java.util.Map; /** - * Response class + * Abstract base class for response messages. * - *

    Generated + *

    This class represents response messages. It provides support for response-specific fields such as date/time, + * error code, and error message, and enforces the accepted message type. Subclasses can define + * additional response data and behavior. + * + *

    Common use cases include: + *

      + *
    • Representing server responses to client requests
    • + *
    • Handling error reporting and status information
    • + *
    • Extending for custom response types
    • + *
    + * + * @author Enedis Smarties team + * @see Message */ public abstract class Response extends Message { protected static final String KEY_DATE_TIME = "dateTime"; diff --git a/src/main/java/enedis/lab/util/message/ResponseBase.java b/src/main/java/enedis/lab/util/message/ResponseBase.java index 0ac4de4..f26b9d2 100644 --- a/src/main/java/enedis/lab/util/message/ResponseBase.java +++ b/src/main/java/enedis/lab/util/message/ResponseBase.java @@ -18,9 +18,21 @@ import java.util.Map; /** - * ResponseBase class + * Concrete base class for response messages with data payload. * - *

    Generated + *

    This class extends {@link Response} to provide support for a data payload field, enabling + * responses to carry additional structured data. It is used for responses that require more + * complex content beyond basic status and error reporting. + * + *

    Common use cases include: + *

      + *
    • Returning structured data in response to client requests
    • + *
    • Supporting extensible response formats
    • + *
    • Providing a base for custom response types with data
    • + *
    + * + * @author Enedis Smarties team + * @see Response */ public class ResponseBase extends Response { protected static final String KEY_DATA = "data"; diff --git a/src/main/java/enedis/lab/util/message/exception/MessageException.java b/src/main/java/enedis/lab/util/message/exception/MessageException.java index 9b06c40..d5e47a8 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageException.java @@ -7,39 +7,68 @@ package enedis.lab.util.message.exception; -/** Message exception */ +/** + * Exception for signaling errors during message processing in the TIC2WebSocket framework. + * + *

    This exception serves as the base class for all message-related errors encountered while parsing, + * validating, or handling messages. + * It enables robust error reporting and propagation throughout the message handling pipeline. + * + *

    Common use cases include: + *

      + *
    • Reporting invalid message formats
    • + *
    • Handling missing or unsupported message types
    • + *
    • Signaling errors in message content or structure
    • + *
    • Chaining underlying exceptions for debugging
    • + *
    + * + * @author Enedis Smarties team + */ public class MessageException extends Exception { private static final long serialVersionUID = -2263755971102386572L; - /** Default constructor */ + /** + * Creates a new MessageException with no detail message. + * + *

    This constructor is typically used when no specific error information is available. + */ public MessageException() { super(); } /** - * Constructor using message and cause + * Creates a new MessageException with a detail message and underlying cause. * - * @param message - * @param cause + *

    This constructor is used when both a descriptive error message and the original cause + * of the error are available, providing complete context for debugging. + * + * @param message the detail message explaining the error + * @param cause the underlying exception that caused this error */ public MessageException(String message, Throwable cause) { super(message, cause); } /** - * Constructor using message + * Creates a new MessageException with a detail message. + * + *

    This constructor is used when a descriptive error message is available but there is no + * underlying cause to chain. * - * @param message + * @param message the detail message explaining the error */ public MessageException(String message) { super(message); } /** - * Constructor using cause + * Creates a new MessageException with an underlying cause. + * + *

    This constructor is used when the original cause is known but no additional descriptive + * message is needed beyond what the cause provides. * - * @param cause + * @param cause the underlying exception that caused this error */ public MessageException(Throwable cause) { super(cause); diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java index f1fed8b..e4e34f2 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java @@ -7,39 +7,68 @@ package enedis.lab.util.message.exception; -/** Unvalid message format exception */ +/** + * Exception indicating that a message contains invalid or unexpected content. + * + *

    This exception is thrown when the content of a message does not conform to the expected structure, + * contains invalid values, or fails validation during parsing or processing. It is part of the message + * exception hierarchy used to signal specific errors in communication. + * + *

    Common use cases include: + *

      + *
    • Detecting missing or malformed fields in a message
    • + *
    • Reporting invalid data values or types
    • + *
    • Handling content validation failures
    • + *
    • Chaining underlying exceptions for debugging
    • + *
    + * + * @author Enedis Smarties team + */ public class MessageInvalidContentException extends MessageException { private static final long serialVersionUID = -2263755971102386572L; - /** Default constructor */ + /** + * Creates a new MessageInvalidContentException with no detail message. + * + *

    This constructor is typically used when no specific error information is available. + */ public MessageInvalidContentException() { super(); } /** - * Constructor using message and cause + * Creates a new MessageInvalidContentException with a detail message and underlying cause. * - * @param message - * @param cause + *

    This constructor is used when both a descriptive error message and the original cause + * of the error are available, providing complete context for debugging. + * + * @param message the detail message explaining the error + * @param cause the underlying exception that caused this error */ public MessageInvalidContentException(String message, Throwable cause) { super(message, cause); } /** - * Constructor using message + * Creates a new MessageInvalidContentException with a detail message. + * + *

    This constructor is used when a descriptive error message is available but there is no + * underlying cause to chain. * - * @param message + * @param message the detail message explaining the error */ public MessageInvalidContentException(String message) { super(message); } /** - * Constructor using cause + * Creates a new MessageInvalidContentException with an underlying cause. + * + *

    This constructor is used when the original cause is known but no additional descriptive + * message is needed beyond what the cause provides. * - * @param cause + * @param cause the underlying exception that caused this error */ public MessageInvalidContentException(Throwable cause) { super(cause); diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java index 87127fb..4ddef57 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java @@ -7,39 +7,68 @@ package enedis.lab.util.message.exception; -/** Unvalid message format exception */ +/** + * Exception indicating that a message has an invalid format. + * + *

    This exception is thrown when a message fails to conform to the expected format, such as + * incorrect JSON structure, missing required fields, or syntactic errors during parsing. It is part + * of the message exception hierarchy used to signal specific errors in communication. + * + *

    Common use cases include: + *

      + *
    • Detecting malformed or corrupted messages
    • + *
    • Reporting missing or unexpected fields
    • + *
    • Handling parsing failures due to format errors
    • + *
    • Chaining underlying exceptions for debugging
    • + *
    + * + * @author Enedis Smarties team + */ public class MessageInvalidFormatException extends MessageException { private static final long serialVersionUID = -2263755971102386572L; - /** Default constructor */ + /** + * Creates a new MessageInvalidFormatException with no detail message. + * + *

    This constructor is typically used when no specific error information is available. + */ public MessageInvalidFormatException() { super(); } /** - * Constructor using message and cause + * Creates a new MessageInvalidFormatException with a detail message and underlying cause. * - * @param message - * @param cause + *

    This constructor is used when both a descriptive error message and the original cause + * of the error are available, providing complete context for debugging. + * + * @param message the detail message explaining the error + * @param cause the underlying exception that caused this error */ public MessageInvalidFormatException(String message, Throwable cause) { super(message, cause); } /** - * Constructor using message + * Creates a new MessageInvalidFormatException with a detail message. + * + *

    This constructor is used when a descriptive error message is available but there is no + * underlying cause to chain. * - * @param message + * @param message the detail message explaining the error */ public MessageInvalidFormatException(String message) { super(message); } /** - * Constructor using cause + * Creates a new MessageInvalidFormatException with an underlying cause. + * + *

    This constructor is used when the original cause is known but no additional descriptive + * message is needed beyond what the cause provides. * - * @param cause + * @param cause the underlying exception that caused this error */ public MessageInvalidFormatException(Throwable cause) { super(cause); diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java index c5a6a2e..69b0147 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java @@ -7,39 +7,68 @@ package enedis.lab.util.message.exception; -/** Unvalid message format exception */ +/** + * Exception indicating that a message has an invalid or unsupported type. + * + *

    This exception is thrown when a message specifies a type that is not recognized, not supported, + * or does not match the expected types during parsing or processing. It is part of the message exception + * hierarchy used to signal specific errors in communication. + * + *

    Common use cases include: + *

      + *
    • Detecting unknown or unsupported message types
    • + *
    • Reporting type mismatches in message definitions
    • + *
    • Handling validation failures due to type errors
    • + *
    • Chaining underlying exceptions for debugging
    • + *
    + * + * @author Enedis Smarties team + */ public class MessageInvalidTypeException extends MessageException { private static final long serialVersionUID = -2263755971102386572L; - /** Default constructor */ + /** + * Creates a new MessageInvalidTypeException with no detail message. + * + *

    This constructor is typically used when no specific error information is available. + */ public MessageInvalidTypeException() { super(); } /** - * Constructor using message and cause + * Creates a new MessageInvalidTypeException with a detail message and underlying cause. * - * @param message - * @param cause + *

    This constructor is used when both a descriptive error message and the original cause + * of the error are available, providing complete context for debugging. + * + * @param message the detail message explaining the error + * @param cause the underlying exception that caused this error */ public MessageInvalidTypeException(String message, Throwable cause) { super(message, cause); } /** - * Constructor using message + * Creates a new MessageInvalidTypeException with a detail message. + * + *

    This constructor is used when a descriptive error message is available but there is no + * underlying cause to chain. * - * @param message + * @param message the detail message explaining the error */ public MessageInvalidTypeException(String message) { super(message); } /** - * Constructor using cause + * Creates a new MessageInvalidTypeException with an underlying cause. + * + *

    This constructor is used when the original cause is known but no additional descriptive + * message is needed beyond what the cause provides. * - * @param cause + * @param cause the underlying exception that caused this error */ public MessageInvalidTypeException(Throwable cause) { super(cause); diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java index 443ac5e..46e6788 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java @@ -7,39 +7,68 @@ package enedis.lab.util.message.exception; -/** Unvalid message format exception */ +/** + * Exception indicating that a required message key name does not exist. + * + *

    This exception is thrown when a message references a key name that is missing, undefined, + * or not recognized during parsing or processing. It is part of the message exception hierarchy + * used to signal specific errors in communication. + * + *

    Common use cases include: + *

      + *
    • Detecting missing key names in message definitions
    • + *
    • Reporting undefined or unrecognized keys
    • + *
    • Handling validation failures due to absent keys
    • + *
    • Chaining underlying exceptions for debugging
    • + *
    + * + * @author Enedis Smarties team + */ public class MessageKeyNameDoesntExistException extends MessageException { private static final long serialVersionUID = -2263755971102386572L; - /** Default constructor */ + /** + * Creates a new MessageKeyNameDoesntExistException with no detail message. + * + *

    This constructor is typically used when no specific error information is available. + */ public MessageKeyNameDoesntExistException() { super(); } /** - * Constructor using message and cause + * Creates a new MessageKeyNameDoesntExistException with a detail message and underlying cause. * - * @param message - * @param cause + *

    This constructor is used when both a descriptive error message and the original cause + * of the error are available, providing complete context for debugging. + * + * @param message the detail message explaining the error + * @param cause the underlying exception that caused this error */ public MessageKeyNameDoesntExistException(String message, Throwable cause) { super(message, cause); } /** - * Constructor using message + * Creates a new MessageKeyNameDoesntExistException with a detail message. + * + *

    This constructor is used when a descriptive error message is available but there is no + * underlying cause to chain. * - * @param message + * @param message the detail message explaining the error */ public MessageKeyNameDoesntExistException(String message) { super(message); } /** - * Constructor using cause + * Creates a new MessageKeyNameDoesntExistException with an underlying cause. + * + *

    This constructor is used when the original cause is known but no additional descriptive + * message is needed beyond what the cause provides. * - * @param cause + * @param cause the underlying exception that caused this error */ public MessageKeyNameDoesntExistException(Throwable cause) { super(cause); diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java index ee427c9..9b7c2b0 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java @@ -7,39 +7,68 @@ package enedis.lab.util.message.exception; -/** Unvalid message format exception */ +/** + * Exception indicating that a required message key type does not exist. + * + *

    This exception is thrown when a message references a key type that is missing, undefined, + * or not recognized during parsing or processing. It is part of the message exception hierarchy + * used to signal specific errors in communication. + * + *

    Common use cases include: + *

      + *
    • Detecting missing key types in message definitions
    • + *
    • Reporting undefined or unrecognized key types
    • + *
    • Handling validation failures due to absent key types
    • + *
    • Chaining underlying exceptions for debugging
    • + *
    + * + * @author Enedis Smarties team + */ public class MessageKeyTypeDoesntExistException extends MessageException { private static final long serialVersionUID = -2263755971102386572L; - /** Default constructor */ + /** + * Creates a new MessageKeyTypeDoesntExistException with no detail message. + * + *

    This constructor is typically used when no specific error information is available. + */ public MessageKeyTypeDoesntExistException() { super(); } /** - * Constructor using message and cause + * Creates a new MessageKeyTypeDoesntExistException with a detail message and underlying cause. * - * @param message - * @param cause + *

    This constructor is used when both a descriptive error message and the original cause + * of the error are available, providing complete context for debugging. + * + * @param message the detail message explaining the error + * @param cause the underlying exception that caused this error */ public MessageKeyTypeDoesntExistException(String message, Throwable cause) { super(message, cause); } /** - * Constructor using message + * Creates a new MessageKeyTypeDoesntExistException with a detail message. + * + *

    This constructor is used when a descriptive error message is available but there is no + * underlying cause to chain. * - * @param message + * @param message the detail message explaining the error */ public MessageKeyTypeDoesntExistException(String message) { super(message); } /** - * Constructor using cause + * Creates a new MessageKeyTypeDoesntExistException with an underlying cause. + * + *

    This constructor is used when the original cause is known but no additional descriptive + * message is needed beyond what the cause provides. * - * @param cause + * @param cause the underlying exception that caused this error */ public MessageKeyTypeDoesntExistException(Throwable cause) { super(cause); diff --git a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java index 6bafbce..e967f4d 100644 --- a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java +++ b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java @@ -7,39 +7,68 @@ package enedis.lab.util.message.exception; -/** Unvalid message format exception */ +/** + * Exception indicating that a message type or operation is unsupported. + * + *

    This exception is thrown when a message specifies a type, operation, or feature that is not + * supported by the current implementation. It is part of the message exception hierarchy used to + * signal specific errors in communication. + * + *

    Common use cases include: + *

      + *
    • Detecting unsupported message types or operations
    • + *
    • Reporting attempts to use unimplemented features
    • + *
    • Handling validation failures due to unsupported requests
    • + *
    • Chaining underlying exceptions for debugging
    • + *
    + * + * @author Enedis Smarties team + */ public class UnsupportedMessageException extends MessageException { private static final long serialVersionUID = -2263755971102386572L; - /** Default constructor */ + /** + * Creates a new UnsupportedMessageException with no detail message. + * + *

    This constructor is typically used when no specific error information is available. + */ public UnsupportedMessageException() { super(); } /** - * Constructor using message and cause + * Creates a new UnsupportedMessageException with a detail message and underlying cause. * - * @param message - * @param cause + *

    This constructor is used when both a descriptive error message and the original cause + * of the error are available, providing complete context for debugging. + * + * @param message the detail message explaining the error + * @param cause the underlying exception that caused this error */ public UnsupportedMessageException(String message, Throwable cause) { super(message, cause); } /** - * Constructor using message + * Creates a new UnsupportedMessageException with a detail message. + * + *

    This constructor is used when a descriptive error message is available but there is no + * underlying cause to chain. * - * @param message + * @param message the detail message explaining the error */ public UnsupportedMessageException(String message) { super(message); } /** - * Constructor using cause + * Creates a new UnsupportedMessageException with an underlying cause. + * + *

    This constructor is used when the original cause is known but no additional descriptive + * message is needed beyond what the cause provides. * - * @param cause + * @param cause the underlying exception that caused this error */ public UnsupportedMessageException(Throwable cause) { super(cause); diff --git a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java index 7451122..37e4070 100644 --- a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java @@ -18,18 +18,34 @@ import org.json.JSONException; /** - * Message factory + * Generic factory for creating and decoding message objects. * - * @param + *

    This class provides methods for registering message types and decoding messages from text + * representations, typically JSON. It supports extensible message handling by allowing new message + * classes to be registered and decoded dynamically based on their names. + * + *

    Common use cases include: + *

      + *
    • Decoding incoming JSON messages into typed objects
    • + *
    • Registering custom message types for extensibility
    • + *
    • Handling errors in message format or content
    • + *
    • Supporting generic message processing pipelines
    • + *
    + * + * @param the type of message handled by this factory + * @author Enedis Smarties team */ public class AbstractMessageFactory { private Class clazz; private Map> messageClasses; /** - * Default constructor + * Creates a new AbstractMessageFactory for the specified message type. + * + *

    This constructor initializes the factory for a given message class and prepares the + * internal registry for message type mappings. * - * @param clazz + * @param clazz the class of message objects handled by this factory */ public AbstractMessageFactory(Class clazz) { this.clazz = clazz; @@ -37,14 +53,18 @@ public AbstractMessageFactory(Class clazz) { } /** - * Get message from text + * Decodes a message from its text representation and type name. * - * @param text - * @param name - * @return message - * @throws UnsupportedMessageException - * @throws MessageInvalidFormatException - * @throws MessageInvalidContentException + *

    This method parses the provided text (typically JSON) and instantiates the corresponding + * message object based on the registered type name. It throws specific exceptions for unsupported + * message types, invalid formats, or content errors. + * + * @param text the text representation of the message (usually JSON) + * @param name the type name of the message to decode + * @return the decoded message object + * @throws UnsupportedMessageException if the message type is not registered or supported + * @throws MessageInvalidFormatException if the message format is invalid (e.g., malformed JSON) + * @throws MessageInvalidContentException if the message content fails validation */ public final T getMessage(String text, String name) throws UnsupportedMessageException, @@ -79,10 +99,13 @@ public final T getMessage(String text, String name) } /** - * Add a Message class to decode message with the given name + * Registers a message class for decoding messages with the specified type name. + * + *

    This method allows the factory to support new message types by associating a name with a + * message class. Registered types can then be decoded from text representations. * - * @param name - * @param messageClazz + * @param name the type name of the message + * @param messageClazz the class to use for decoding messages of this type */ public final void addMessageClass(String name, Class messageClazz) { if (name == null || messageClazz == null) { diff --git a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java index 3169c87..8ac208c 100644 --- a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java @@ -17,17 +17,38 @@ import org.json.JSONException; import org.json.JSONObject; -/** Message factory */ +/** + * Static factory for decoding basic message objects. + * + *

    This class provides static methods for parsing and validating messages from their text + * representations, typically JSON. It handles extraction of message type and name, and throws + * specific exceptions for format, type, or key errors. + * + *

    Common use cases include: + *

      + *
    • Decoding incoming JSON messages into basic message objects
    • + *
    • Validating message format and required keys
    • + *
    • Handling errors in message type or name extraction
    • + *
    • Supporting generic message processing pipelines
    • + *
    + * + * @author Enedis Smarties team + */ public abstract class BasicMessageFactory { + /** - * Get message from text + * Decodes a basic message from its text representation. * - * @param text - * @return message - * @throws MessageInvalidFormatException - * @throws MessageKeyTypeDoesntExistException - * @throws MessageKeyNameDoesntExistException - * @throws MessageInvalidTypeException + *

    This method parses the provided text (typically JSON), extracts the message type and name, + * and instantiates a basic message object. It throws specific exceptions for unsupported message + * types, invalid formats, or missing keys. + * + * @param text the text representation of the message (usually JSON) + * @return the decoded basic message object + * @throws MessageInvalidFormatException if the message format is invalid (e.g., malformed JSON) + * @throws MessageKeyTypeDoesntExistException if the message type key is missing + * @throws MessageKeyNameDoesntExistException if the message name key is missing + * @throws MessageInvalidTypeException if the message type is invalid or unsupported */ public static Message getMessage(String text) throws MessageInvalidFormatException, diff --git a/src/main/java/enedis/lab/util/message/factory/EventFactory.java b/src/main/java/enedis/lab/util/message/factory/EventFactory.java index 5613aca..0a85138 100644 --- a/src/main/java/enedis/lab/util/message/factory/EventFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/EventFactory.java @@ -9,9 +9,31 @@ import enedis.lab.util.message.Event; -/** Request factory */ +/** + * Factory for creating and decoding event message objects. + * + *

    This class extends {@link AbstractMessageFactory} to provide specialized support for event + * messages. It enables registration and decoding of event types from their text representations, + * typically JSON, and supports extensible event handling in the message processing pipeline. + * + *

    Common use cases include: + *

      + *
    • Decoding incoming event messages into typed objects
    • + *
    • Registering custom event types for extensibility
    • + *
    • Supporting generic event processing pipelines
    • + *
    + * + * @author Enedis Smarties team + * @see AbstractMessageFactory + * @see Event + */ public class EventFactory extends AbstractMessageFactory { - /** Default constructor */ + /** + * Creates a new EventFactory for event message objects. + * + *

    This constructor initializes the factory for the {@link Event} class and prepares the + * internal registry for event type mappings. + */ public EventFactory() { super(Event.class); } diff --git a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java index 2d647b8..af79a96 100644 --- a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java @@ -15,23 +15,50 @@ import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; import enedis.lab.util.message.exception.UnsupportedMessageException; -/** Message factory */ +/** + * Central factory for decoding and dispatching messages in the TIC2WebSocket framework. + * + *

    This class coordinates the decoding of messages by delegating to specialized sub-factories + * for requests, responses, and events. It parses the message type and name, then dispatches to + * the appropriate factory for further decoding and validation. + * + *

    Common use cases include: + *

      + *
    • Decoding incoming JSON messages into typed objects
    • + *
    • Coordinating request, response, and event message handling
    • + *
    • Supporting extensible message processing pipelines
    • + *
    • Managing sub-factory references for modularity
    • + *
    + * + * @author Enedis Smarties team + * @see BasicMessageFactory + * @see RequestFactory + * @see ResponseFactory + * @see EventFactory + */ public class MessageFactory { private RequestFactory requestFactory; private ResponseFactory responseFactory; private EventFactory eventFactory; - /** Default constructor */ + /** + * Creates a new MessageFactory with no sub-factories set. + * + *

    Sub-factories must be set before decoding messages. + */ public MessageFactory() { super(); } /** - * Constructor using field + * Creates a new MessageFactory with the specified sub-factories. * - * @param requestFactory - * @param responseFactory - * @param eventFactory + *

    This constructor initializes the factory with request, response, and event sub-factories + * for coordinated message decoding. + * + * @param requestFactory the factory for decoding request messages + * @param responseFactory the factory for decoding response messages + * @param eventFactory the factory for decoding event messages */ public MessageFactory( RequestFactory requestFactory, ResponseFactory responseFactory, EventFactory eventFactory) { @@ -42,16 +69,20 @@ public MessageFactory( } /** - * Get message from String + * Decodes and dispatches a message from its text representation. + * + *

    This method parses the provided text (typically JSON), determines the message type, and + * delegates decoding to the appropriate sub-factory. It throws specific exceptions for unsupported + * message types, invalid formats, or missing keys. * - * @param text - * @return message - * @throws MessageInvalidTypeException - * @throws MessageKeyNameDoesntExistException - * @throws MessageKeyTypeDoesntExistException - * @throws MessageInvalidFormatException - * @throws MessageInvalidContentException - * @throws UnsupportedMessageException + * @param text the text representation of the message (usually JSON) + * @return the decoded message object + * @throws MessageInvalidTypeException if the message type is invalid or unsupported + * @throws MessageKeyNameDoesntExistException if the message name key is missing + * @throws MessageKeyTypeDoesntExistException if the message type key is missing + * @throws MessageInvalidFormatException if the message format is invalid (e.g., malformed JSON) + * @throws MessageInvalidContentException if the message content fails validation + * @throws UnsupportedMessageException if the message type is not registered or supported */ public Message getMessage(String text) throws MessageInvalidFormatException, @@ -85,27 +116,27 @@ public Message getMessage(String text) } /** - * Set request factory + * Sets the request sub-factory for decoding request messages. * - * @param requestFactory + * @param requestFactory the factory for decoding request messages */ public void setRequestFactory(RequestFactory requestFactory) { this.requestFactory = requestFactory; } /** - * Set response factory + * Sets the response sub-factory for decoding response messages. * - * @param responseFactory + * @param responseFactory the factory for decoding response messages */ public void setResponseFactory(ResponseFactory responseFactory) { this.responseFactory = responseFactory; } /** - * Set event factory + * Sets the event sub-factory for decoding event messages. * - * @param eventFactory + * @param eventFactory the factory for decoding event messages */ public void setEventFactory(EventFactory eventFactory) { this.eventFactory = eventFactory; diff --git a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java index 974a7c3..d21022e 100644 --- a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java @@ -9,7 +9,28 @@ import enedis.lab.util.message.Request; -/** Request factory */ +/** + * Factory for decoding and instantiating request messages. + * + *

    This factory is responsible for creating {@link Request} objects from their serialized representations. + * It supports extensible registration of request message types, enabling dynamic decoding and validation + * of incoming request messages. + * + *

    Common use cases include: + *

      + *
    • Decoding incoming JSON request messages into typed {@link Request} objects
    • + *
    • Registering custom request message classes for extensibility
    • + *
    • Supporting modular request processing pipelines
    • + *
    • Managing request message type mappings for robust decoding
    • + *
    + * + *

    The factory pattern allows for decoupled message instantiation, enabling flexible handling of + * different request types and supporting future protocol extensions. + * + * @author Enedis Smarties team + * @see Request + * @see AbstractMessageFactory + */ public class RequestFactory extends AbstractMessageFactory { /** Default constructor */ public RequestFactory() { diff --git a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java index 4f7aaac..6d4a03a 100644 --- a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java @@ -9,9 +9,31 @@ import enedis.lab.util.message.Response; -/** Request factory */ +/** + * Factory for creating and decoding response message objects. + * + *

    This class extends {@link AbstractMessageFactory} to provide specialized support for response + * messages. It enables registration and decoding of response types from their text representations, + * typically JSON, and supports extensible response handling in the message processing pipeline. + * + *

    Common use cases include: + *

      + *
    • Decoding incoming response messages into typed objects
    • + *
    • Registering custom response types for extensibility
    • + *
    • Supporting generic response processing pipelines
    • + *
    + * + * @author Enedis Smarties team + * @see AbstractMessageFactory + * @see Response + */ public class ResponseFactory extends AbstractMessageFactory { - /** Default constructor */ + /** + * Creates a new ResponseFactory for response message objects. + * + *

    This constructor initializes the factory for the {@link Response} class and prepares the + * internal registry for response type mappings. + */ public ResponseFactory() { super(Response.class); } diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifier.java b/src/main/java/enedis/lab/util/task/FilteredNotifier.java index 9ca7ea5..3f8e6f6 100644 --- a/src/main/java/enedis/lab/util/task/FilteredNotifier.java +++ b/src/main/java/enedis/lab/util/task/FilteredNotifier.java @@ -11,10 +11,21 @@ import java.util.function.Predicate; /** - * Notifier interface with filter + * Interface for managing filtered notifications to subscribers. * - * @param the filter - * @param the subscriber + *

    This interface defines methods for subscribing, unsubscribing, and querying subscribers + * with associated filters. It supports flexible event delivery and observer patterns. + * + *

    Common use cases include: + *

      + *
    • Selective event delivery based on filters
    • + *
    • Managing filtered lists of subscribers
    • + *
    • Supporting custom notification logic
    • + *
    + * + * @param the filter type + * @param the subscriber type + * @author Enedis Smarties team */ public interface FilteredNotifier extends Notifier { /** diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java index 6a4d708..f09642f 100644 --- a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java +++ b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java @@ -17,10 +17,22 @@ import java.util.function.Predicate; /** - * Filtered notifier with subscribers basic implementation + * Generic base implementation for managing filtered notifications to subscribers. + * + *

    This class provides mechanisms for subscribing, unsubscribing, and querying subscribers + * with associated filters. It supports both filtered and unfiltered (global) subscriptions, + * enabling flexible event delivery and observer patterns. + * + *

    Common use cases include: + *

      + *
    • Delivering events to subscribers that match specific filters
    • + *
    • Supporting selective notification in observer patterns
    • + *
    • Extending for custom filtering logic
    • + *
    * * @param the filter type * @param the subscriber type + * @author Enedis Smarties team */ public class FilteredNotifierBase implements FilteredNotifier { protected Map> subscribersFiltered; diff --git a/src/main/java/enedis/lab/util/task/Notifier.java b/src/main/java/enedis/lab/util/task/Notifier.java index f764b14..3d2081f 100644 --- a/src/main/java/enedis/lab/util/task/Notifier.java +++ b/src/main/java/enedis/lab/util/task/Notifier.java @@ -10,9 +10,21 @@ import java.util.Collection; /** - * Notifier interface + * Interface for managing subscribers and sending notifications. * - * @param + *

    This interface defines methods for adding, removing, and querying subscribers, as well as + * retrieving the current set of subscribers. It is used as a generic contract for event-driven + * and observer patterns. + * + *

    Common use cases include: + *

      + *
    • Managing lists of listeners or observers
    • + *
    • Sending notifications to registered subscribers
    • + *
    • Supporting event-driven architectures
    • + *
    + * + * @param the subscriber type + * @author Enedis Smarties team */ public interface Notifier { /** diff --git a/src/main/java/enedis/lab/util/task/NotifierBase.java b/src/main/java/enedis/lab/util/task/NotifierBase.java index 6515f7e..7787f9b 100644 --- a/src/main/java/enedis/lab/util/task/NotifierBase.java +++ b/src/main/java/enedis/lab/util/task/NotifierBase.java @@ -12,9 +12,20 @@ import java.util.concurrent.CopyOnWriteArraySet; /** - * Notifier with subscribers basic implementation + * Generic base implementation for managing subscribers and sending notifications. + * + *

    This class provides mechanisms for adding, removing, and querying subscribers. It is used as a + * foundation for event-driven and observer patterns, supporting flexible notification delivery. + * + *

    Common use cases include: + *

      + *
    • Managing lists of listeners or observers
    • + *
    • Sending notifications to registered subscribers
    • + *
    • Extending for custom notification logic
    • + *
    * * @param the subscriber type + * @author Enedis Smarties team */ public class NotifierBase implements Notifier { protected Set subscribers; diff --git a/src/main/java/enedis/lab/util/task/Subscriber.java b/src/main/java/enedis/lab/util/task/Subscriber.java index 9e1dd91..6170467 100644 --- a/src/main/java/enedis/lab/util/task/Subscriber.java +++ b/src/main/java/enedis/lab/util/task/Subscriber.java @@ -7,5 +7,19 @@ package enedis.lab.util.task; -/** Subscriber interface */ +/** + * Interface representing a generic event subscriber. + * + *

    This interface is used as a contract for objects that can receive notifications or events + * from a notifier. It is commonly used in observer and event-driven patterns. + * + *

    Common use cases include: + *

      + *
    • Receiving notifications from event sources
    • + *
    • Implementing observer patterns
    • + *
    • Extending for custom event handling logic
    • + *
    + * + * @author Enedis Smarties team + */ public interface Subscriber {} diff --git a/src/main/java/enedis/lab/util/task/Task.java b/src/main/java/enedis/lab/util/task/Task.java index 85cd5b8..2f55843 100644 --- a/src/main/java/enedis/lab/util/task/Task.java +++ b/src/main/java/enedis/lab/util/task/Task.java @@ -7,7 +7,21 @@ package enedis.lab.util.task; -/** Task interface */ +/** + * Interface representing a generic asynchronous task. + * + *

    This interface defines the contract for executing tasks, typically in a concurrent or event-driven + * environment. Implementations may represent background jobs, scheduled actions, or event handlers. + * + *

    Common use cases include: + *

      + *
    • Running background operations
    • + *
    • Scheduling periodic or delayed actions
    • + *
    • Handling events or notifications
    • + *
    + * + * @author Enedis Smarties team + */ public interface Task { /** Start task */ public void start(); diff --git a/src/main/java/enedis/lab/util/task/TaskBase.java b/src/main/java/enedis/lab/util/task/TaskBase.java index fb8428f..58c762e 100644 --- a/src/main/java/enedis/lab/util/task/TaskBase.java +++ b/src/main/java/enedis/lab/util/task/TaskBase.java @@ -11,7 +11,21 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -/** Task basic implementation */ +/** + * Abstract base class representing a generic asynchronous task. + * + *

    This class provides a foundation for implementing tasks that can be executed + * asynchronously, typically in concurrent or event-driven environments. + * + *

    Common use cases include: + *

      + *
    • Running background operations
    • + *
    • Scheduling periodic or delayed actions
    • + *
    • Extending for custom task logic
    • + *
    + * + * @author Enedis Smarties team + */ public abstract class TaskBase implements Task, Runnable { private AtomicBoolean stopRequired; private Thread task; diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodic.java b/src/main/java/enedis/lab/util/task/TaskPeriodic.java index a9ba56f..fea7315 100644 --- a/src/main/java/enedis/lab/util/task/TaskPeriodic.java +++ b/src/main/java/enedis/lab/util/task/TaskPeriodic.java @@ -10,7 +10,21 @@ import enedis.lab.util.time.Time; import java.util.concurrent.atomic.AtomicLong; -/** Periodic task with configurable period */ +/** + * Abstract class representing a generic periodic asynchronous task. + * + *

    This class provides functionality for executing tasks at regular intervals, + * typically used for background operations or scheduled actions. + * + *

    Common use cases include: + *

      + *
    • Running periodic background jobs
    • + *
    • Scheduling recurring actions
    • + *
    • Extending for custom periodic logic
    • + *
    + * + * @author Enedis Smarties team + */ public abstract class TaskPeriodic extends TaskBase { /** Default period (in milliseconds) used to execute process */ public static final long DEFAULT_PERIOD = 1000; diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java index c469612..2ea9711 100644 --- a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java +++ b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java @@ -10,9 +10,20 @@ import java.util.Collection; /** - * Periodic task with subscribers basic implementation + * Abstract class representing a generic periodic asynchronous task with subscriber notification. + * + *

    This class provides functionality for executing tasks at regular intervals and notifying + * registered subscribers of events or results. + * + *

    Common use cases include: + *

      + *
    • Running periodic background jobs with notifications
    • + *
    • Scheduling recurring actions and informing listeners
    • + *
    • Extending for custom periodic and notification logic
    • + *
    * * @param the subscriber type + * @author Enedis Smarties team */ public abstract class TaskPeriodicWithSubscribers extends TaskPeriodic implements Notifier { diff --git a/src/main/java/enedis/lab/util/time/Time.java b/src/main/java/enedis/lab/util/time/Time.java index d3dcb4b..1a0feb4 100644 --- a/src/main/java/enedis/lab/util/time/Time.java +++ b/src/main/java/enedis/lab/util/time/Time.java @@ -11,7 +11,21 @@ import java.text.SimpleDateFormat; import java.util.Date; -/** Time implementation */ +/** + * Utility class providing time-related constants and helper methods. + * + *

    This class offers constants for time units and static methods for sleeping, formatting timestamps, + * and decorating messages with timestamps. It is designed for general-purpose time management and formatting. + * + *

    Common use cases include: + *

      + *
    • Sleeping for a specified duration
    • + *
    • Formatting timestamps as date/time strings
    • + *
    • Decorating log messages with current date/time
    • + *
    + * + * @author Enedis Smarties team + */ public abstract class Time { /** Millisecond */ public static final int MILLISECOND = 1; diff --git a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java index c60a93e..94e7518 100644 --- a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java +++ b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java @@ -7,6 +7,21 @@ package enedis.tic.core; +/** + * Subscriber implementation for receiving the next frame and error notifications. + * + *

    This class listens for frame and error events, storing the latest received frame or error. + * It provides methods to access the received data and to clear its state. + * + *

    Common use cases include: + *

      + *
    • Receiving and processing the next available frame
    • + *
    • Handling error notifications
    • + *
    • Resetting subscriber state for reuse
    • + *
    + * + * @author Enedis Smarties team + */ public class ReadNextFrameSubscriber implements TICCoreSubscriber { private TICCoreFrame frame; private TICCoreError error; diff --git a/src/main/java/enedis/tic/core/TICCore.java b/src/main/java/enedis/tic/core/TICCore.java index 477776d..538cb27 100644 --- a/src/main/java/enedis/tic/core/TICCore.java +++ b/src/main/java/enedis/tic/core/TICCore.java @@ -11,7 +11,20 @@ import enedis.lab.util.task.Task; import java.util.List; -/** TICCore interface */ +/** + * Core interface for managing frame reading and subscriber notifications. + * + *

    This interface defines methods for reading frames, managing subscribers, and retrieving modem information. + * + *

    Common use cases include: + *

      + *
    • Reading frames from available sources
    • + *
    • Managing and notifying subscribers
    • + *
    • Retrieving modem and identifier information
    • + *
    + * + * @author Enedis Smarties team + */ public interface TICCore extends Task { /** * Get available TICs diff --git a/src/main/java/enedis/tic/core/TICCoreBase.java b/src/main/java/enedis/tic/core/TICCoreBase.java index 2072aed..006a2a9 100644 --- a/src/main/java/enedis/tic/core/TICCoreBase.java +++ b/src/main/java/enedis/tic/core/TICCoreBase.java @@ -31,7 +31,21 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -/** TICCore implementation */ +/** + * Core implementation for managing frame reading and subscriber notifications. + * + *

    This class provides mechanisms for reading frames, managing streams, handling subscribers, + * and notifying events. It implements the core contract for frame acquisition and event delivery. + * + *

    Common use cases include: + *

      + *
    • Reading frames from available sources
    • + *
    • Managing and notifying subscribers
    • + *
    • Handling stream lifecycle and modem events
    • + *
    + * + * @author Enedis Smarties team + */ public class TICCoreBase implements TICCore, Task, TICCoreSubscriber, PlugSubscriber { private static final int PLUG_NOTIFIER_POLLING_PERIOD = 100; diff --git a/src/main/java/enedis/tic/core/TICCoreError.java b/src/main/java/enedis/tic/core/TICCoreError.java index 2fa452b..8466e50 100644 --- a/src/main/java/enedis/tic/core/TICCoreError.java +++ b/src/main/java/enedis/tic/core/TICCoreError.java @@ -19,9 +19,19 @@ import java.util.Map; /** - * TICCoreError class + * Class representing a core error with identifier, code, message, and optional data. * - *

    Generated + *

    This class provides mechanisms for constructing, accessing, and managing error information + * including error codes, messages, and associated data. It is designed for general-purpose error handling. + * + *

    Common use cases include: + *

      + *
    • Representing errors with structured data
    • + *
    • Managing error codes and messages
    • + *
    • Associating additional data with errors
    • + *
    + * + * @author Enedis Smarties team */ public class TICCoreError extends DataDictionaryBase { protected static final String KEY_IDENTIFIER = "identifier"; diff --git a/src/main/java/enedis/tic/core/TICCoreErrorCode.java b/src/main/java/enedis/tic/core/TICCoreErrorCode.java index b157360..13bd104 100644 --- a/src/main/java/enedis/tic/core/TICCoreErrorCode.java +++ b/src/main/java/enedis/tic/core/TICCoreErrorCode.java @@ -7,15 +7,39 @@ package enedis.tic.core; +/** + * Enumeration of core error codes for error handling and diagnostics. + * + *

    This enum defines error codes used to represent various error conditions in core operations. + * It provides a structured way to manage and interpret error states. + * + *

    Common use cases include: + *

      + *
    • Representing error states in core logic
    • + *
    • Supporting diagnostics and error reporting
    • + *
    • Mapping error codes to messages
    • + *
    + * + * @author Enedis Smarties team + */ public enum TICCoreErrorCode { + /** No error occurred. */ NO_ERROR(0), + /** The specified port ID was not found. */ STREAM_PORT_ID_NOT_FOUND(1), + /** The specified port name was not found. */ STREAM_PORT_NAME_NOT_FOUND(2), + /** The port descriptor is empty or missing required information. */ STREAM_PORT_DESCRIPTOR_EMPTY(3), + /** The stream mode is not defined or invalid. */ STREAM_MODE_NOT_DEFINED(4), + /** The stream identifier was not found or does not match any known stream. */ STREAM_IDENTIFIER_NOT_FOUND(5), + /** The stream was unplugged or disconnected. */ STREAM_UNPLUGGED(6), + /** Data read operation timed out before completion. */ DATA_READ_TIMEOUT(7), + /** An error occurred for another or unspecified reason. */ OTHER_REASON(99); private int code; diff --git a/src/main/java/enedis/tic/core/TICCoreException.java b/src/main/java/enedis/tic/core/TICCoreException.java index 9f75b55..1d97620 100644 --- a/src/main/java/enedis/tic/core/TICCoreException.java +++ b/src/main/java/enedis/tic/core/TICCoreException.java @@ -9,6 +9,14 @@ import enedis.lab.types.ExceptionBase; +/** + * Exception class for representing core errors and failures. + * + *

    This class provides mechanisms for constructing exceptions with error codes and messages, + * supporting structured error handling and reporting. + * + * @author Enedis Smarties team + */ public class TICCoreException extends ExceptionBase { private static final long serialVersionUID = -3285641164559292710L; diff --git a/src/main/java/enedis/tic/core/TICCoreFrame.java b/src/main/java/enedis/tic/core/TICCoreFrame.java index 6101bc8..0908a14 100644 --- a/src/main/java/enedis/tic/core/TICCoreFrame.java +++ b/src/main/java/enedis/tic/core/TICCoreFrame.java @@ -21,9 +21,19 @@ import java.util.Map; /** - * TICCoreFrame class + * Class representing a core frame with identifier, mode, capture time, and content. * - *

    Generated + *

    This class provides mechanisms for constructing, accessing, and managing frame information + * including identifiers, modes, timestamps, and associated content. It is designed for general-purpose frame handling. + * + *

    Common use cases include: + *

      + *
    • Representing frames with structured data
    • + *
    • Managing frame identifiers and modes
    • + *
    • Associating content and timestamps with frames
    • + *
    + * + * @author Enedis Smarties team */ public class TICCoreFrame extends DataDictionaryBase { protected static final String KEY_IDENTIFIER = "identifier"; diff --git a/src/main/java/enedis/tic/core/TICCoreStream.java b/src/main/java/enedis/tic/core/TICCoreStream.java index 33e509e..57a569d 100644 --- a/src/main/java/enedis/tic/core/TICCoreStream.java +++ b/src/main/java/enedis/tic/core/TICCoreStream.java @@ -10,7 +10,21 @@ import enedis.lab.util.task.Notifier; import enedis.lab.util.task.Task; -/** TICCore stream interface */ +/** + * Interface for managing core streams, frame acquisition, and subscriber notifications. + * + *

    This interface defines methods for accessing stream identifiers and managing subscribers. + * It provides a contract for implementations that handle frame delivery and event notification. + * + *

    Common use cases include: + *

      + *
    • Acquiring frames from streams
    • + *
    • Managing and notifying subscribers
    • + *
    • Supporting event-driven stream operations
    • + *
    + * + * @author Enedis Smarties team + */ public interface TICCoreStream extends Notifier, Task { /** * Get identifier diff --git a/src/main/java/enedis/tic/core/TICCoreStreamBase.java b/src/main/java/enedis/tic/core/TICCoreStreamBase.java index f14d461..97435e2 100644 --- a/src/main/java/enedis/tic/core/TICCoreStreamBase.java +++ b/src/main/java/enedis/tic/core/TICCoreStreamBase.java @@ -36,7 +36,21 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -/** TICCore stream implementation */ +/** + * Core stream implementation for frame acquisition and subscriber notifications. + * + *

    This class provides mechanisms for managing streams, acquiring frames, handling errors, + * and notifying subscribers. It implements the contract for event-driven stream operations. + * + *

    Common use cases include: + *

      + *
    • Acquiring frames from streams
    • + *
    • Managing and notifying subscribers
    • + *
    • Handling errors and stream lifecycle
    • + *
    + * + * @author Enedis Smarties team + */ public class TICCoreStreamBase implements TICCoreStream, Task, DataStreamListener { private TICIdentifier identifier; diff --git a/src/main/java/enedis/tic/core/TICCoreSubscriber.java b/src/main/java/enedis/tic/core/TICCoreSubscriber.java index 3f550e4..dd7d16d 100644 --- a/src/main/java/enedis/tic/core/TICCoreSubscriber.java +++ b/src/main/java/enedis/tic/core/TICCoreSubscriber.java @@ -9,7 +9,21 @@ import enedis.lab.util.task.Subscriber; -/** TICCore subscriber interface */ +/** + * Interface for subscribing to core frame and error notifications. + * + *

    This interface defines methods for receiving frame and error events. It is used as a contract + * for objects that handle event-driven notifications in core operations. + * + *

    Common use cases include: + *

      + *
    • Receiving frame notifications
    • + *
    • Handling error events
    • + *
    • Implementing event-driven logic
    • + *
    + * + * @author Enedis Smarties team + */ public interface TICCoreSubscriber extends Subscriber { /** * Notify when data diff --git a/src/main/java/enedis/tic/core/TICIdentifier.java b/src/main/java/enedis/tic/core/TICIdentifier.java index 1e09916..491c55c 100644 --- a/src/main/java/enedis/tic/core/TICIdentifier.java +++ b/src/main/java/enedis/tic/core/TICIdentifier.java @@ -17,9 +17,19 @@ import java.util.Map; /** - * TICIdentifier class + * Class representing a core identifier with port ID, port name, and serial number. * - *

    Generated + *

    This class provides mechanisms for constructing, accessing, and managing identifier information + * including port IDs, port names, and serial numbers. It is designed for general-purpose identifier handling. + * + *

    Common use cases include: + *

      + *
    • Representing identifiers with structured data
    • + *
    • Matching identifiers for streams and devices
    • + *
    • Managing port and serial information
    • + *
    + * + * @author Enedis Smarties team */ public class TICIdentifier extends DataDictionaryBase { protected static final String KEY_PORT_ID = "portId"; From 9bb78434b4b536a36a77bc20dc9aa63261149d67 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Thu, 9 Oct 2025 16:22:22 +0200 Subject: [PATCH 28/59] chore: format enedis.lab.types package --- .../java/enedis/lab/protocol/tic/TICMode.java | 226 ++-- .../lab/protocol/tic/frame/TICError.java | 216 ++-- .../tic/frame/standard/TICException.java | 154 +-- .../java/enedis/lab/types/BytesArray.java | 190 ++- .../java/enedis/lab/types/DataArrayList.java | 11 +- .../java/enedis/lab/types/DataDictionary.java | 281 ++--- .../lab/types/DataDictionaryException.java | 60 +- src/main/java/enedis/lab/types/DataList.java | 114 +- .../java/enedis/lab/types/ExceptionBase.java | 44 +- .../types/configuration/Configuration.java | 112 +- .../configuration/ConfigurationBase.java | 85 +- .../configuration/ConfigurationException.java | 80 +- .../datadictionary/DataDictionaryBase.java | 251 +++- .../types/datadictionary/KeyDescriptor.java | 134 +- .../datadictionary/KeyDescriptorBase.java | 74 +- .../KeyDescriptorDataDictionary.java | 26 +- .../datadictionary/KeyDescriptorEnum.java | 46 +- .../datadictionary/KeyDescriptorList.java | 48 +- .../KeyDescriptorListMinMaxSize.java | 61 +- .../KeyDescriptorLocalDateTime.java | 54 +- .../datadictionary/KeyDescriptorNumber.java | 39 +- .../KeyDescriptorNumberMinMax.java | 57 +- .../datadictionary/KeyDescriptorString.java | 58 +- src/main/java/enedis/lab/util/SystemLibC.java | 50 +- .../enedis/lab/util/message/MessageType.java | 36 +- .../message/factory/BasicMessageFactory.java | 202 +-- .../util/message/factory/EventFactory.java | 36 +- .../util/message/factory/RequestFactory.java | 36 +- .../util/message/factory/ResponseFactory.java | 36 +- .../lab/util/task/FilteredNotifier.java | 190 +-- .../java/enedis/lab/util/task/Notifier.java | 92 +- .../java/enedis/lab/util/task/Subscriber.java | 22 +- src/main/java/enedis/lab/util/task/Task.java | 48 +- src/main/java/enedis/tic/core/TICCore.java | 180 +-- .../enedis/tic/core/TICCoreErrorCode.java | 60 +- .../java/enedis/tic/core/TICCoreStream.java | 42 +- .../enedis/tic/core/TICCoreSubscriber.java | 54 +- .../tic/service/endpoint/EventSender.java | 44 +- .../java/enedis/lab/io/PortFinderMock.java | 122 +- .../java/enedis/lab/mock/FunctionCall.java | 46 +- .../java/enedis/tic/core/TICCoreBaseTest.java | 1084 ++++++++--------- .../enedis/tic/core/TICIdentifierTest.java | 334 ++--- 42 files changed, 2814 insertions(+), 2321 deletions(-) diff --git a/src/main/java/enedis/lab/protocol/tic/TICMode.java b/src/main/java/enedis/lab/protocol/tic/TICMode.java index 4a17ed0..a2df471 100644 --- a/src/main/java/enedis/lab/protocol/tic/TICMode.java +++ b/src/main/java/enedis/lab/protocol/tic/TICMode.java @@ -1,113 +1,113 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic; - -import enedis.lab.protocol.tic.frame.TICError; -import enedis.lab.protocol.tic.frame.standard.TICException; -import java.util.Arrays; - -/** - * Enumeration of available TIC modes and related utilities. - * - *

    This enum defines the supported TIC protocol modes (STANDARD, HISTORIC, AUTO, UNKNOWN) and - * provides utility methods for mode detection from frame or group buffers. It also defines - * protocol-specific separator and buffer start patterns. - * - *

    Key features: - * - *

      - *
    • Defines all supported TIC modes - *
    • Provides methods to detect mode from frame or group buffers - *
    • Defines protocol-specific separators and buffer start patterns - *
    - * - * @author Enedis Smarties team - */ -public enum TICMode { - /** Unknown mode. */ - UNKNOWN, - /** Standard mode. */ - STANDARD, - /** Historic mode. */ - HISTORIC, - /** Auto-detect mode. */ - AUTO; - - /** Separator character (space, 0x20) for historic TIC frames. */ - public static final char HISTORIC_SEPARATOR = ' '; - - /** Separator character (tab, 0x09) for standard TIC frames. */ - public static final char STANDARD_SEPARATOR = '\t'; - - /** Buffer start pattern for historic TIC frames. */ - public static final byte[] HISTORIC_BUFFER_START = {2, 10, 65, 68, 67, 79}; - - /** Buffer start pattern for standard TIC frames. */ - public static final byte[] STANDARD_BUFFER_START = {2, 10, 65, 68, 83, 67}; - - /** - * Detects the TIC mode from the given frame buffer. - * - * @param frameBuffer the byte array containing the frame start - * @return the detected {@link TICMode}, or null if not recognized - * @throws TICException if the buffer is too short or invalid - */ - public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) throws TICException { - byte[] frameBufferStart = new byte[HISTORIC_BUFFER_START.length]; - if (frameBuffer.length < frameBufferStart.length) { - throw new TICException( - "Tic frame read 0x" + bytesToHex(frameBuffer) + " too short to determine TIC Mode !", - TICError.TIC_READER_FRAME_DECODE_FAILED); - } - System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); - if (Arrays.equals(frameBufferStart, HISTORIC_BUFFER_START)) { - return HISTORIC; - } else { - if (STANDARD_BUFFER_START.length != HISTORIC_BUFFER_START.length) { - frameBufferStart = new byte[STANDARD_BUFFER_START.length]; - System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); - } - if (Arrays.equals(frameBufferStart, STANDARD_BUFFER_START)) { - return STANDARD; - } - return null; - } - } - - /** - * Detects the TIC mode from the given group buffer. - * - * @param groupBuffer the byte array containing the group data - * @return the detected {@link TICMode}, or null if not recognized - */ - public static TICMode findModeFromGroupBuffer(byte[] groupBuffer) { - for (int i = 0; i < groupBuffer.length; i++) { - if (groupBuffer[i] == HISTORIC_SEPARATOR) { - return HISTORIC; - } else if (groupBuffer[i] == STANDARD_SEPARATOR) { - return STANDARD; - } - } - return null; - } - - /** - * Converts a byte array to a hexadecimal string (replacement for - * DatatypeConverter.printHexBinary). - * - * @param bytes the byte array to convert - * @return the hexadecimal string representation - */ - private static String bytesToHex(byte[] bytes) { - StringBuilder result = new StringBuilder(); - for (byte b : bytes) { - result.append(String.format("%02X", b)); - } - return result.toString(); - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic; + +import enedis.lab.protocol.tic.frame.TICError; +import enedis.lab.protocol.tic.frame.standard.TICException; +import java.util.Arrays; + +/** + * Enumeration of available TIC modes and related utilities. + * + *

    This enum defines the supported TIC protocol modes (STANDARD, HISTORIC, AUTO, UNKNOWN) and + * provides utility methods for mode detection from frame or group buffers. It also defines + * protocol-specific separator and buffer start patterns. + * + *

    Key features: + * + *

      + *
    • Defines all supported TIC modes + *
    • Provides methods to detect mode from frame or group buffers + *
    • Defines protocol-specific separators and buffer start patterns + *
    + * + * @author Enedis Smarties team + */ +public enum TICMode { + /** Unknown mode. */ + UNKNOWN, + /** Standard mode. */ + STANDARD, + /** Historic mode. */ + HISTORIC, + /** Auto-detect mode. */ + AUTO; + + /** Separator character (space, 0x20) for historic TIC frames. */ + public static final char HISTORIC_SEPARATOR = ' '; + + /** Separator character (tab, 0x09) for standard TIC frames. */ + public static final char STANDARD_SEPARATOR = '\t'; + + /** Buffer start pattern for historic TIC frames. */ + public static final byte[] HISTORIC_BUFFER_START = {2, 10, 65, 68, 67, 79}; + + /** Buffer start pattern for standard TIC frames. */ + public static final byte[] STANDARD_BUFFER_START = {2, 10, 65, 68, 83, 67}; + + /** + * Detects the TIC mode from the given frame buffer. + * + * @param frameBuffer the byte array containing the frame start + * @return the detected {@link TICMode}, or null if not recognized + * @throws TICException if the buffer is too short or invalid + */ + public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) throws TICException { + byte[] frameBufferStart = new byte[HISTORIC_BUFFER_START.length]; + if (frameBuffer.length < frameBufferStart.length) { + throw new TICException( + "Tic frame read 0x" + bytesToHex(frameBuffer) + " too short to determine TIC Mode !", + TICError.TIC_READER_FRAME_DECODE_FAILED); + } + System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); + if (Arrays.equals(frameBufferStart, HISTORIC_BUFFER_START)) { + return HISTORIC; + } else { + if (STANDARD_BUFFER_START.length != HISTORIC_BUFFER_START.length) { + frameBufferStart = new byte[STANDARD_BUFFER_START.length]; + System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); + } + if (Arrays.equals(frameBufferStart, STANDARD_BUFFER_START)) { + return STANDARD; + } + return null; + } + } + + /** + * Detects the TIC mode from the given group buffer. + * + * @param groupBuffer the byte array containing the group data + * @return the detected {@link TICMode}, or null if not recognized + */ + public static TICMode findModeFromGroupBuffer(byte[] groupBuffer) { + for (int i = 0; i < groupBuffer.length; i++) { + if (groupBuffer[i] == HISTORIC_SEPARATOR) { + return HISTORIC; + } else if (groupBuffer[i] == STANDARD_SEPARATOR) { + return STANDARD; + } + } + return null; + } + + /** + * Converts a byte array to a hexadecimal string (replacement for + * DatatypeConverter.printHexBinary). + * + * @param bytes the byte array to convert + * @return the hexadecimal string representation + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02X", b)); + } + return result.toString(); + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java index 9054950..c6c6d6d 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java @@ -1,108 +1,108 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame; - -import jssc.SerialPortException; - -/** - * Enumeration of error codes for TIC frame and serial port operations. - * - *

    This enum defines error codes for serial port issues and TIC frame processing errors. It - * provides a mapping from {@link SerialPortException} types to TIC error codes for unified error - * handling. - * - *

    Key features: - * - *

      - *
    • Defines error codes for serial port and TIC frame errors - *
    • Provides a method to map {@link SerialPortException} to TICError - *
    • Each error code has an associated integer value - *
    - * - * @author Enedis Smarties team - * @see SerialPortException - */ -public enum TICError { - /** No error. */ - NO_ERROR(0), - /** Serial port not found. */ - SERIAL_PORT_NOT_FOUND(17), - /** Incorrect serial port. */ - SERIAL_PORT_INCORRECT_SERIAL_PORT(18), - /** Null not permitted for serial port. */ - SERIAL_PORT_NULL_NOT_PERMITTED(19), - /** Serial port already opened. */ - SERIAL_PORT_ALREADY_OPENED(20), - /** Serial port is busy. */ - SERIAL_PORT_SERIAL_PORT_BUSY(21), - /** Serial port configuration failure. */ - SERIAL_PORT_CONFIGURATION_FAILURE(22), - /** Default error for TIC reader. */ - TIC_READER_DEFAULT_ERROR(23), - /** TIC reader label name not found. */ - TIC_READER_LABEL_NAME_NOT_FOUND(24), - /** TIC reader frame decode failed. */ - TIC_READER_FRAME_DECODE_FAILED(25), - /** TIC reader read frame timeout. */ - TIC_READER_READ_FRAME_TIMEOUT(26), - /** TIC reader read frame with checksum invalid. */ - TIC_READER_READ_FRAME_CHECKSUM_INVALID(27), - ; - - /** Integer value associated with the error code. */ - private int value; - - /** - * Constructs a TICError with the specified integer value. - * - * @param value the integer value for the error code - */ - private TICError(int value) { - this.value = value; - } - - /** - * Returns the integer value associated with this error code. - * - * @return the error code value - */ - public int getValue() { - return this.value; - } - - /** - * Maps a {@link SerialPortException} to the corresponding {@link TICError} code. - * - * @param serialPortException the serial port exception to map - * @return the corresponding {@link TICError}, or null if not recognized - */ - public static TICError getSerialPortError(SerialPortException serialPortException) { - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) { - return TICError.SERIAL_PORT_SERIAL_PORT_BUSY; - } - if (serialPortException - .getExceptionType() - .equals(SerialPortException.TYPE_INCORRECT_SERIAL_PORT)) { - return TICError.SERIAL_PORT_INCORRECT_SERIAL_PORT; - } - if (serialPortException - .getExceptionType() - .equals(SerialPortException.TYPE_NULL_NOT_PERMITTED)) { - return TICError.SERIAL_PORT_NULL_NOT_PERMITTED; - } - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_NOT_FOUND)) { - return TICError.SERIAL_PORT_NOT_FOUND; - } - if (serialPortException - .getExceptionType() - .equals(SerialPortException.TYPE_PORT_ALREADY_OPENED)) { - return TICError.SERIAL_PORT_ALREADY_OPENED; - } - return null; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame; + +import jssc.SerialPortException; + +/** + * Enumeration of error codes for TIC frame and serial port operations. + * + *

    This enum defines error codes for serial port issues and TIC frame processing errors. It + * provides a mapping from {@link SerialPortException} types to TIC error codes for unified error + * handling. + * + *

    Key features: + * + *

      + *
    • Defines error codes for serial port and TIC frame errors + *
    • Provides a method to map {@link SerialPortException} to TICError + *
    • Each error code has an associated integer value + *
    + * + * @author Enedis Smarties team + * @see SerialPortException + */ +public enum TICError { + /** No error. */ + NO_ERROR(0), + /** Serial port not found. */ + SERIAL_PORT_NOT_FOUND(17), + /** Incorrect serial port. */ + SERIAL_PORT_INCORRECT_SERIAL_PORT(18), + /** Null not permitted for serial port. */ + SERIAL_PORT_NULL_NOT_PERMITTED(19), + /** Serial port already opened. */ + SERIAL_PORT_ALREADY_OPENED(20), + /** Serial port is busy. */ + SERIAL_PORT_SERIAL_PORT_BUSY(21), + /** Serial port configuration failure. */ + SERIAL_PORT_CONFIGURATION_FAILURE(22), + /** Default error for TIC reader. */ + TIC_READER_DEFAULT_ERROR(23), + /** TIC reader label name not found. */ + TIC_READER_LABEL_NAME_NOT_FOUND(24), + /** TIC reader frame decode failed. */ + TIC_READER_FRAME_DECODE_FAILED(25), + /** TIC reader read frame timeout. */ + TIC_READER_READ_FRAME_TIMEOUT(26), + /** TIC reader read frame with checksum invalid. */ + TIC_READER_READ_FRAME_CHECKSUM_INVALID(27), + ; + + /** Integer value associated with the error code. */ + private int value; + + /** + * Constructs a TICError with the specified integer value. + * + * @param value the integer value for the error code + */ + private TICError(int value) { + this.value = value; + } + + /** + * Returns the integer value associated with this error code. + * + * @return the error code value + */ + public int getValue() { + return this.value; + } + + /** + * Maps a {@link SerialPortException} to the corresponding {@link TICError} code. + * + * @param serialPortException the serial port exception to map + * @return the corresponding {@link TICError}, or null if not recognized + */ + public static TICError getSerialPortError(SerialPortException serialPortException) { + if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) { + return TICError.SERIAL_PORT_SERIAL_PORT_BUSY; + } + if (serialPortException + .getExceptionType() + .equals(SerialPortException.TYPE_INCORRECT_SERIAL_PORT)) { + return TICError.SERIAL_PORT_INCORRECT_SERIAL_PORT; + } + if (serialPortException + .getExceptionType() + .equals(SerialPortException.TYPE_NULL_NOT_PERMITTED)) { + return TICError.SERIAL_PORT_NULL_NOT_PERMITTED; + } + if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_NOT_FOUND)) { + return TICError.SERIAL_PORT_NOT_FOUND; + } + if (serialPortException + .getExceptionType() + .equals(SerialPortException.TYPE_PORT_ALREADY_OPENED)) { + return TICError.SERIAL_PORT_ALREADY_OPENED; + } + return null; + } +} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java index 67f238b..1374d66 100644 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java +++ b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java @@ -1,77 +1,77 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.standard; - -import enedis.lab.protocol.tic.frame.TICError; - -/** - * Exception class for errors encountered during TIC frame processing. - * - *

    This exception is thrown when a TIC frame cannot be parsed, validated, or processed correctly. - * It encapsulates a {@link TICError} code to provide additional context about the error type. - * - *

    Key features: - * - *

      - *
    • Associates a {@link TICError} with each exception instance - *
    • Supports standard exception message and error code - *
    • Provides a reset method to restore default error state - *
    - * - * @author Enedis Smarties team - * @see TICError - * @see Exception - */ -public class TICException extends Exception { - /** Unique identifier used for serialization. */ - private static final long serialVersionUID = -2780151361870269473L; - - /** The TIC error code associated with this exception. */ - private TICError error; - - /** Constructs a new TICException with default error code. */ - public TICException() { - super(); - this.reset(); - } - - /** - * Constructs a new TICException with the specified error message and default error code. - * - * @param message the detail message - */ - public TICException(String message) { - super(message); - this.error = TICError.TIC_READER_DEFAULT_ERROR; - } - - /** - * Constructs a new TICException with the specified error message and error code. - * - * @param message the detail message - * @param readerError the TIC error code - */ - public TICException(String message, TICError readerError) { - super(message); - this.error = readerError; - } - - /** Resets the error code to the default value. */ - public void reset() { - this.error = TICError.TIC_READER_DEFAULT_ERROR; - } - - /** - * Returns the TIC error code associated with this exception. - * - * @return the TIC error code - */ - public TICError getError() { - return this.error; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.protocol.tic.frame.standard; + +import enedis.lab.protocol.tic.frame.TICError; + +/** + * Exception class for errors encountered during TIC frame processing. + * + *

    This exception is thrown when a TIC frame cannot be parsed, validated, or processed correctly. + * It encapsulates a {@link TICError} code to provide additional context about the error type. + * + *

    Key features: + * + *

      + *
    • Associates a {@link TICError} with each exception instance + *
    • Supports standard exception message and error code + *
    • Provides a reset method to restore default error state + *
    + * + * @author Enedis Smarties team + * @see TICError + * @see Exception + */ +public class TICException extends Exception { + /** Unique identifier used for serialization. */ + private static final long serialVersionUID = -2780151361870269473L; + + /** The TIC error code associated with this exception. */ + private TICError error; + + /** Constructs a new TICException with default error code. */ + public TICException() { + super(); + this.reset(); + } + + /** + * Constructs a new TICException with the specified error message and default error code. + * + * @param message the detail message + */ + public TICException(String message) { + super(message); + this.error = TICError.TIC_READER_DEFAULT_ERROR; + } + + /** + * Constructs a new TICException with the specified error message and error code. + * + * @param message the detail message + * @param readerError the TIC error code + */ + public TICException(String message, TICError readerError) { + super(message); + this.error = readerError; + } + + /** Resets the error code to the default value. */ + public void reset() { + this.error = TICError.TIC_READER_DEFAULT_ERROR; + } + + /** + * Returns the TIC error code associated with this exception. + * + * @return the TIC error code + */ + public TICError getError() { + return this.error; + } +} diff --git a/src/main/java/enedis/lab/types/BytesArray.java b/src/main/java/enedis/lab/types/BytesArray.java index 70203e7..a993f4b 100644 --- a/src/main/java/enedis/lab/types/BytesArray.java +++ b/src/main/java/enedis/lab/types/BytesArray.java @@ -13,18 +13,28 @@ import java.util.List; import java.util.ListIterator; -/** Bytes array */ +/** + * Represents a mutable array of bytes with utility methods for manipulation, searching, and + * slicing. + * + *

    Implements the {@link List} interface for bytes, and provides additional methods for + * byte-level operations, such as splitting, slicing, and computing checksums. Supports conversion + * from integers and arrays, and offers options for greedy, contiguous, and pattern-based + * operations. + * + * @author Enedis Smarties team + */ public class BytesArray implements List { - /** Default option */ + /** Default option for byte array operations. */ public static final int DEFAULT_OPTIONS = 0x0000; - /** Greedy option */ + /** Greedy option for slice/split operations. */ public static final int GREEDY = 0x0001; - /** Contiguous option */ + /** Contiguous option for slice/split operations. */ public static final int CONTIGUOUS = 0x0002; - /** Remove patterns option */ + /** Option to remove pattern bytes from results. */ public static final int REMOVE_PATTERNS = 0x0004; /** @@ -80,15 +90,15 @@ private static int countBytes(int value) { protected byte[] buffer; - /** Default constructor */ + /** Constructs an empty BytesArray. */ public BytesArray() { this.buffer = new byte[0]; } /** - * Constructor from array of byte + * Constructs a BytesArray from a given byte array. * - * @param bytesArray + * @param bytesArray the source array of bytes (copied) */ public BytesArray(byte[] bytesArray) { this.buffer = bytesArray.clone(); @@ -152,20 +162,13 @@ public void add(int index, Byte element) { } else { byte[] bytesArray = new byte[this.buffer.length + 1]; - // Si l'index est le premier if (0 == index) { System.arraycopy(this.buffer, 0, bytesArray, 1, this.buffer.length); - } - - // Sinon, si l'index est le dernier - else if ((this.buffer.length - 1) == index) { + } else if ((this.buffer.length - 1) == index) { System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length - 1); bytesArray[bytesArray.length - 1] = this.buffer[this.buffer.length - 1]; - } - - // Sinon, si l'index est au milieu du tableau - else { + } else { System.arraycopy(this.buffer, 0, bytesArray, 0, index - 1); System.arraycopy( @@ -186,16 +189,11 @@ public boolean remove(Object element) { boolean success = true; if ((null != element) && (element instanceof Number)) { - // Obtenir l'index de la première occurence de l'élément int index = this.indexOf(element); - // Si un tel élément a été trouvé, if (index >= 0) { this.removeIndex(index); - } - - // Sinon, - else { + } else { success = false; } } else { @@ -212,28 +210,25 @@ public Byte remove(int index) { if ((index >= 0) && (index < this.buffer.length)) { element = new Byte(this.buffer[index]); this.removeIndex(index); - } else { - // Aucune action } return element; } /** - * Remove the buffer from an index to other + * Removes a range of bytes from the buffer, from {@code fromIndex} to {@code toIndex} + * (inclusive). * - * @param fromIndex - * @param toIndex - * @return new length of buffer + * @param fromIndex the starting index (inclusive) + * @param toIndex the ending index (inclusive) + * @return the number of bytes removed, or 0 if indices are invalid */ public int remove(int fromIndex, int toIndex) { int length = 0; - if ((fromIndex >= 0) && (toIndex < this.buffer.length) && (toIndex > fromIndex)) { this.removeSubList(fromIndex, toIndex); length = toIndex - fromIndex + 1; } - return length; } @@ -280,17 +275,13 @@ public boolean addAll(int index, Collection collection) { Object[] collectionArray = collection.toArray(); byte[] bytesArray = new byte[this.buffer.length + collection.size()]; - // Cas ou on ajoute en début: if (0 == index) { for (int i = 0; i < collectionArray.length; i++) { bytesArray[i] = ((Byte) collectionArray[i]).byteValue(); } System.arraycopy(this.buffer, 0, bytesArray, collectionArray.length, this.buffer.length); - } - - // Cas où on ajoute à la fin: - else if (this.buffer.length == index) { + } else if (this.buffer.length == index) { System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); int j = index; @@ -298,10 +289,7 @@ else if (this.buffer.length == index) { bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); j++; } - } - - // Cas où on ajoute au milieu: - else { + } else { System.arraycopy(this.buffer, 0, bytesArray, 0, index); int j = index; @@ -379,31 +367,27 @@ public int indexOf(Object element) { } /** - * Get the buffer element of the given index + * Returns the index of the first occurrence of the specified element, starting from the given + * offset. * - * @param element - * @param offset - * @return the element + * @param element the element to search for (must be a Number) + * @param offset the index to start searching from + * @return the index of the element, or -1 if not found */ public int indexOf(Object element, int offset) { int index = -1; - if ((null != element) && (element instanceof Number) && offset >= 0 && offset < this.buffer.length) { Byte byte_element = ((Number) element).byteValue(); - for (int i = offset; i < this.buffer.length; i++) { if (this.buffer[i] == byte_element) { index = i; break; } } - } else { - // Aucune action } - return index; } @@ -420,8 +404,6 @@ public int lastIndexOf(Object element) { break; } } - } else { - // Aucune action } return index; @@ -467,44 +449,39 @@ public BytesArray subList(int fromIndex, int toIndex) { } /** - * Get bytes + * Returns a copy of the underlying byte array. * - * @return bytes + * @return a copy of the bytes in this BytesArray */ public byte[] getBytes() { return this.buffer; } /** - * Copy + * Returns a deep copy of this BytesArray. * - * @return copied BytesArray + * @return a new BytesArray with the same contents */ public BytesArray copy() { return new BytesArray(this.buffer); } /** - * Get index of element + * Returns a list of all indices where the specified element occurs in the buffer. * - * @param element - * @return index of element + * @param element the element to search for (must be a Number) + * @return a list of indices where the element is found */ public List indexesOf(Object element) { List indexesList = new ArrayList(); - if ((null != element) && (element instanceof Number)) { Byte byte_element = ((Number) element).byteValue(); - for (int i = 0; i < this.buffer.length; i++) { if (this.buffer[i] == byte_element) { indexesList.add(i); } } - } else { - // Aucune action } - return indexesList; } @@ -560,7 +537,8 @@ public List split(Number pattern) { List patternsIndexesList = this.indexesOf(pattern.byteValue()); List bytesArraysList = new ArrayList(); - // Si le motif de séparation existe, copier chaque segment dans l'élément associé du tableau + // If the split pattern exists, copy each segment into the corresponding element + // of the array if (false == patternsIndexesList.isEmpty()) { int begin_index = 0; int end_index; @@ -573,17 +551,14 @@ public List split(Number pattern) { segment_length = end_index - begin_index; - // Si la taille du segment est non nulle, + // If the segment size is not zero if (segment_length > 0) { byte[] segment = new byte[segment_length]; System.arraycopy(this.buffer, begin_index, segment, 0, segment_length); bytesArraysList.add(new BytesArray(segment)); - } - - // Sinon, - else { + } else { bytesArraysList.add(new BytesArray()); } @@ -591,7 +566,7 @@ public List split(Number pattern) { } } - // Sinon, copier dans l'unique élément du tableau l'intégralité des données + // Otherwise, copy all data into the single element of the array else { bytesArraysList.add(new BytesArray(this.buffer)); } @@ -639,18 +614,19 @@ public List slice( Number beginPattern, Number endPattern, int occurences, int options) { List bytesArraysList; - // ... Si l'appel de la fonction est "gourmand" (greedy), il n'y aura au mieux qu'un élément à - // retourner qui - // correspondra au plus grand segment [begin_pattern, end_pattern] du tableau - // NB le paramètre 'occurences' est alors non signifiant + // ... If the function call is "greedy", there will be at most one element to + // return, + // which will correspond to the largest segment [begin_pattern, end_pattern] of + // the array. + // Note: the 'occurences' parameter is then not significant. if ((options & GREEDY) != 0) { bytesArraysList = this.sliceGreedy(beginPattern, endPattern, options); } - // ... Si l'appel de la fonction est "non gourmand" (not greedy), les éléments du tableau à - // retourner - // correspondront aux segments [begin_pattern, end_pattern] - // les plus rationnels, dans la limite des n premières occurences demandées + // ... If the function call is "not greedy", the elements to return will + // correspond + // to the most rational segments [begin_pattern, end_pattern], within the limit + // of the first n occurrences requested. else { bytesArraysList = this.sliceNotGreedy(beginPattern, endPattern, occurences, options); } @@ -670,10 +646,10 @@ public boolean addAll(byte[] bytesArray) { if (bytesArray != null && bytesArray.length > 0) { byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; - // copie des données existantes + // copy existing data System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); - // copie des nouvelles données + // copy new data System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); this.buffer = newBytesArray; @@ -698,32 +674,15 @@ public boolean addAll(int index, byte[] bytesArray) { if ((index >= 0) && (index <= this.buffer.length)) { byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; - // Cas ou on ajoute au début: if (0 == index) { System.arraycopy(bytesArray, 0, newBytesArray, 0, bytesArray.length); - - // copie des données existantes System.arraycopy(this.buffer, 0, newBytesArray, bytesArray.length, this.buffer.length); - } - - // Cas où on ajoute à la fin: - else if (this.buffer.length == index) { - // copie des données existantes + } else if (this.buffer.length == index) { System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); - - // copie des nouvelles données System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); - } - - // Cas où on ajoute au milieu: - else { - // copie des données existantes (premier segment) + } else { System.arraycopy(this.buffer, 0, newBytesArray, 0, index); - - // copie des nouvelles données System.arraycopy(bytesArray, 0, newBytesArray, index, bytesArray.length); - - // copie des données existantes (deuxième segment) System.arraycopy( this.buffer, index, @@ -860,7 +819,7 @@ private List sliceNotGreedy( Number beginPattern, Number endPattern, int occurences, int options) { List bytesArraysList = new ArrayList(); - // Si la contiguité des segments est requise, + // If segment contiguity is required, if ((options & CONTIGUOUS) != 0) { bytesArraysList = this.sliceContiguous(beginPattern, endPattern, occurences, options); } else { @@ -885,39 +844,41 @@ private List sliceContiguous( int endIndex = -1; for (int i = 0; i < this.buffer.length; i++) { - // Traiter un motif DEBUT + // Handle a BEGIN pattern if (this.buffer[i] == beginPattern.byteValue()) { if ((-1 == endIndex) || (this.buffer[i - 1] == endPattern.byteValue())) { beginIndex = i; } } - // Traiter un motif FIN + // Handle a END pattern else if (this.buffer[i] == endPattern.byteValue()) { - // Si nous ne sommes pas au dernier élément du tableau + // If we are not at the last element of the array if (i < (this.buffer.length - 1)) { - // Si le prochain caractère est le motif de début de trame + // If the next character is the start pattern if (this.buffer[i + 1] == beginPattern.byteValue()) { endIndex = i; this.slicePart(bytesArraysList, beginIndex, endIndex, options); - // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, - // interrompre le traitement + // If a number of occurrences is specified and the number of segments has + // already been reached, + // stop processing if ((occurences > 0) && (bytesArraysList.size() >= occurences)) { break; } } } - // Sinon, si nous sommes au dernier élément du tableau + // Otherwise, if we are at the last element of the array else { endIndex = i; this.slicePart(bytesArraysList, beginIndex, endIndex, options); - // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, - // interrompre le traitement + // If a number of occurrences is specified and the number of segments has + // already been reached, + // stop processing if ((occurences > 0) && (bytesArraysList.size() >= occurences)) { break; } @@ -952,8 +913,9 @@ private List sliceNotContiguous( this.slicePart(bytesArraysList, beginIndex, endIndex, options); - // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, - // interrompre le traitement + // If a number of occurrences is specified and the number of segments has + // already been reached, + // stop processing if ((occurences > 0) && (bytesArraysList.size() >= occurences)) { break; } @@ -993,17 +955,17 @@ private void slicePart( private void removeIndex(int index) { byte[] bytesArray = new byte[this.buffer.length - 1]; - // Si l'index est le premier du tableau + // If the index is the first in the array if (0 == index) { System.arraycopy(this.buffer, 1, bytesArray, 0, bytesArray.length); } - // Sinon, si l'index est le dernier + // Else, if the index is the last else if ((this.buffer.length - 1) == index) { System.arraycopy(this.buffer, 0, bytesArray, 0, bytesArray.length); } - // Sinon, si l'index est au milieu du tableau + // Else, if the index is in the middle of the array else { System.arraycopy(this.buffer, 0, bytesArray, 0, index); diff --git a/src/main/java/enedis/lab/types/DataArrayList.java b/src/main/java/enedis/lab/types/DataArrayList.java index c86a3ca..39b002d 100644 --- a/src/main/java/enedis/lab/types/DataArrayList.java +++ b/src/main/java/enedis/lab/types/DataArrayList.java @@ -37,15 +37,10 @@ public class DataArrayList extends ArrayList implements DataList { /** * Returns a fixed-size data list backed by the specified array. (Changes to the returned data * list "write through" to the array.) This method acts as bridge between array-based and - * collection-based APIs, in combination with {@link Collection#toArray}. The returned list is - * serializable and implements {@link RandomAccess}. - * + * collection-based APIs, in combination with {@link Collection#toArray}. + * *

    This method also provides a convenient way to create a fixed-size data list initialized to - * contain several elements: - * - *

    -   *     DataList<String> stooges = DataArrayList.asList("Larry", "Moe", "Curly");
    -   * 
    + * contain several elements. * * @param the class of the objects in the array * @param items the array by which the data list will be backed diff --git a/src/main/java/enedis/lab/types/DataDictionary.java b/src/main/java/enedis/lab/types/DataDictionary.java index 8746b9a..d543ccd 100644 --- a/src/main/java/enedis/lab/types/DataDictionary.java +++ b/src/main/java/enedis/lab/types/DataDictionary.java @@ -1,136 +1,145 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Map; -import org.json.JSONObject; - -/** Interface used to handle data dictionaries */ -public interface DataDictionary extends Cloneable { - /** - * Check key exists - * - * @param key Key to check - * @return true if key exists, false otherwise - */ - public boolean exists(String key); - - /** - * Get the datadictionary current map keys list - * - * @return the datadictionary current map keys list - */ - public String[] keys(); - - /** - * Check if the datadictionary map contains the given key - * - * @param key - * @return true if the datadictionary map contains the given key - */ - public boolean existsInKeys(String key); - - /** - * Add the given key in the datadictionary map, if the given key already exists, do nothing - * - * @param key - */ - public void addKey(String key); - - /** - * Remove the given key in the datadictionary map - * - * @param key - */ - public void removeKey(String key); - - /** Clear the datadictionary map */ - public void clear(); - - /** - * Get value of the given key - * - * @param key - * @return value of the given key - */ - public Object get(String key); - - /** - * Set value of the given key - * - * @param key - * @param value - * @throws DataDictionaryException - */ - public void set(String key, Object value) throws DataDictionaryException; - - /** - * Copy an other datadictionary into this one - * - * @param other - * @throws DataDictionaryException - */ - public void copy(DataDictionary other) throws DataDictionaryException; - - /** - * Convert this dataditionary to JSON - * - * @return a JSONObject - */ - public JSONObject toJSON(); - - /** - * Convert this dataditionary to Map - * - * @return a JSONObject - */ - public Map toMap(); - - /** - * Convert the given datadictionary to a File - * - * @param file - * @param indentFactor - * @throws IOException - */ - public void toFile(File file, int indentFactor) throws IOException; - - /** - * Convert the given datadictionary to a Stream - * - * @param stream - * @param indentFactor - * @throws IOException - */ - public void toStream(OutputStream stream, int indentFactor) throws IOException; - - @Override - public String toString(); - - /** - * Convert this dataditionary to a String - * - * @param indentFactor - * @return a JSONObject - */ - public String toString(int indentFactor); - - /** - * Convert this dataditionary to Map - * - * @return a JSONObject - * @throws CloneNotSupportedException - */ - public Object clone() throws CloneNotSupportedException; - - /** Print this datactionary in the console */ - public void print(); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; +import org.json.JSONObject; + +/** + * Interface for managing key-value data dictionaries with serialization and utility methods. + * + *

    Provides methods for key management, value access, serialization to JSON and Map, file and + * stream output, and cloning. Implementations may support additional validation and type safety. + * + * @author Enedis Smarties team + */ +public interface DataDictionary extends Cloneable { + + /** + * Checks if the specified key exists in the data dictionary. + * + * @param key the key to check + * @return true if the key exists, false otherwise + */ + public boolean exists(String key); + + /** + * Returns the list of all keys currently present in the data dictionary. + * + * @return an array of key names + */ + public String[] keys(); + + /** + * Checks if the data dictionary contains the specified key in its key set. + * + * @param key the key to check + * @return true if the key is present, false otherwise + */ + public boolean existsInKeys(String key); + + /** + * Adds the specified key to the data dictionary. If the key already exists, no action is taken. + * + * @param key the key to add + */ + public void addKey(String key); + + /** + * Removes the specified key from the data dictionary. + * + * @param key the key to remove + */ + public void removeKey(String key); + + /** Removes all keys and values from the data dictionary. */ + public void clear(); + + /** + * Returns the value associated with the specified key. + * + * @param key the key whose value is to be returned + * @return the value of the given key, or null if not present + */ + public Object get(String key); + + /** + * Sets the value for the specified key in the data dictionary. + * + * @param key the key to set + * @param value the value to associate with the key + * @throws DataDictionaryException if the value is invalid or cannot be set + */ + public void set(String key, Object value) throws DataDictionaryException; + + /** + * Copies all key-value pairs from another data dictionary into this one. + * + * @param other the data dictionary to copy from + * @throws DataDictionaryException if a value cannot be copied + */ + public void copy(DataDictionary other) throws DataDictionaryException; + + /** + * Serializes this data dictionary to a {@link JSONObject}. + * + * @return a JSONObject representing the data dictionary + */ + public JSONObject toJSON(); + + /** + * Serializes this data dictionary to a {@link Map}. + * + * @return a map representing the data dictionary + */ + public Map toMap(); + + /** + * Writes the data dictionary to a file in JSON format. + * + * @param file the file to write to + * @param indentFactor the number of spaces to add to each level of indentation + * @throws IOException if an I/O error occurs + */ + public void toFile(File file, int indentFactor) throws IOException; + + /** + * Writes the data dictionary to an output stream in JSON format. + * + * @param stream the output stream to write to + * @param indentFactor the number of spaces to add to each level of indentation + * @throws IOException if an I/O error occurs + */ + public void toStream(OutputStream stream, int indentFactor) throws IOException; + + @Override + public String toString(); + + /** + * Returns a string representation of the data dictionary in JSON format with the specified + * indentation. + * + * @param indentFactor the number of spaces to add to each level of indentation + * @return a string containing the JSON representation + */ + public String toString(int indentFactor); + + /** + * Creates and returns a deep copy of this data dictionary. + * + * @return a clone of this data dictionary + * @throws CloneNotSupportedException if the object cannot be cloned + */ + public Object clone() throws CloneNotSupportedException; + + /** Prints the contents of this data dictionary to the console. */ + public void print(); +} diff --git a/src/main/java/enedis/lab/types/DataDictionaryException.java b/src/main/java/enedis/lab/types/DataDictionaryException.java index 87ba87c..bf2c966 100644 --- a/src/main/java/enedis/lab/types/DataDictionaryException.java +++ b/src/main/java/enedis/lab/types/DataDictionaryException.java @@ -7,54 +7,68 @@ package enedis.lab.types; -/** Data dictionary exception */ +/** + * Exception type used to represent errors occurring when manipulating or interpreting + * data dictionary elements. + * + *

    This exception typically indicates issues such as invalid data mappings, corrupted + * metadata, or unexpected dictionary structures encountered during runtime.

    + * + *

    It extends {@link java.lang.Exception}, allowing both checked exception handling and + * detailed error propagation with message and cause information.

    + * + * @author Enedis Smarties team + */ public class DataDictionaryException extends Exception { private static final long serialVersionUID = -7967428428453584771L; - /** Default constructor */ + /** + * Creates a new {@code DataDictionaryException} with no detail message or cause. + */ public DataDictionaryException() { super(); } /** - * Constructor using message, cause, enable suppression flag and writable stack trace flag + * Creates a new {@code DataDictionaryException} with the specified detail message. * - * @param message - * @param cause - * @param enableSuppression - * @param writableStackTrace + * @param message the detail message describing the error context */ - public DataDictionaryException( - String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); + public DataDictionaryException(String message) { + super(message); } /** - * Constructor using message and cause + * Creates a new {@code DataDictionaryException} with the specified cause. * - * @param message - * @param cause + * @param cause the underlying cause of the exception */ - public DataDictionaryException(String message, Throwable cause) { - super(message, cause); + public DataDictionaryException(Throwable cause) { + super(cause); } /** - * Constructor using message + * Creates a new {@code DataDictionaryException} with the specified detail message and cause. * - * @param message + * @param message the detail message describing the error context + * @param cause the underlying cause of the exception */ - public DataDictionaryException(String message) { - super(message); + public DataDictionaryException(String message, Throwable cause) { + super(message, cause); } /** - * Constructor using cause + * Creates a new {@code DataDictionaryException} with full control over suppression + * and stack trace writability. * - * @param cause + * @param message the detail message describing the error context + * @param cause the underlying cause of the exception + * @param enableSuppression whether suppression is enabled or disabled + * @param writableStackTrace whether the stack trace should be writable */ - public DataDictionaryException(Throwable cause) { - super(cause); + public DataDictionaryException( + String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); } } diff --git a/src/main/java/enedis/lab/types/DataList.java b/src/main/java/enedis/lab/types/DataList.java index 5266a08..c73b2d8 100644 --- a/src/main/java/enedis/lab/types/DataList.java +++ b/src/main/java/enedis/lab/types/DataList.java @@ -1,57 +1,57 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.List; -import org.json.JSONArray; - -/** - * Interface extending Java standard List to provide JSON serialization mechanism - * - * @param the type of elements in this list - */ -public interface DataList extends List { - /** - * Write this data list to a File - * - * @param file the output file - * @param indentFactor JSON indentation factor - * @throws IOException if writing file fails - */ - public void toFile(File file, int indentFactor) throws IOException; - - /** - * Write this data list to a Stream - * - * @param stream the output stream - * @param indentFactor JSON indentation factor - * @throws IOException if writing stream fails - */ - public void toStream(OutputStream stream, int indentFactor) throws IOException; - - @Override - public String toString(); - - /** - * Convert this data list to a String - * - * @param indentFactor JSON indentation factor - * @return JSON string representation - */ - public String toString(int indentFactor); - - /** - * Convert this data list to JSON array - * - * @return JSON array - */ - public JSONArray toJSON(); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import org.json.JSONArray; + +/** + * Interface extending Java standard List to provide JSON serialization mechanism + * + * @param the type of elements in this list + */ +public interface DataList extends List { + /** + * Write this data list to a File + * + * @param file the output file + * @param indentFactor JSON indentation factor + * @throws IOException if writing file fails + */ + public void toFile(File file, int indentFactor) throws IOException; + + /** + * Write this data list to a Stream + * + * @param stream the output stream + * @param indentFactor JSON indentation factor + * @throws IOException if writing stream fails + */ + public void toStream(OutputStream stream, int indentFactor) throws IOException; + + @Override + public String toString(); + + /** + * Convert this data list to a String + * + * @param indentFactor JSON indentation factor + * @return JSON string representation + */ + public String toString(int indentFactor); + + /** + * Convert this data list to JSON array + * + * @return JSON array + */ + public JSONArray toJSON(); +} diff --git a/src/main/java/enedis/lab/types/ExceptionBase.java b/src/main/java/enedis/lab/types/ExceptionBase.java index 6fc213a..c97b155 100644 --- a/src/main/java/enedis/lab/types/ExceptionBase.java +++ b/src/main/java/enedis/lab/types/ExceptionBase.java @@ -7,59 +7,79 @@ package enedis.lab.types; -/** Exception base */ +/** + * Generic exception base class. + * + *

    This class provides a simple mechanism to associate an error code and an informational + * message with an exception. It can be used as a parent class for more specific exception types + * that require standardized error reporting and structured diagnostic data.

    + * + *

    Both the code and info fields are intended to help identify and categorize the source of + * the problem more precisely in distributed or modular systems.

    + * + * @author Enedis Smarties team + */ @SuppressWarnings("serial") public class ExceptionBase extends Exception { + + /** Numeric error code identifying the specific error type. */ protected int code; + + /** Descriptive information about the error context. */ protected String info; /** - * Constructor + * Creates a new {@code ExceptionBase} instance with the specified error code and message. * - * @param code - * @param info + * @param code the numeric error code associated with this exception + * @param info the descriptive message providing details about the error */ public ExceptionBase(int code, String info) { this.setErrorCode(code); this.setErrorInfo(info); } + /** + * Returns a formatted message including both the descriptive info and the error code. + * + * @return the formatted exception message + */ @Override public String getMessage() { return this.info + " (" + this.code + ")"; } /** - * Get error code + * Returns the error code associated with this exception. * - * @return error code + * @return the numeric error code */ public int getErrorCode() { return this.code; } /** - * Set error code + * Sets the error code for this exception. * - * @param errorCode + * @param errorCode the numeric error code to assign */ public void setErrorCode(int errorCode) { this.code = errorCode; } /** - * Get error info + * Returns the descriptive information message associated with this exception. * - * @return error info + * @return the error information message */ public String getErrorInfo() { return this.info; } /** - * Set error info + * Sets the descriptive information message for this exception. * - * @param errorInfo + * @param errorInfo the descriptive message to assign */ public void setErrorInfo(String errorInfo) { this.info = errorInfo; diff --git a/src/main/java/enedis/lab/types/configuration/Configuration.java b/src/main/java/enedis/lab/types/configuration/Configuration.java index b3ec2c4..a7ccad2 100644 --- a/src/main/java/enedis/lab/types/configuration/Configuration.java +++ b/src/main/java/enedis/lab/types/configuration/Configuration.java @@ -1,43 +1,69 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.configuration; - -import enedis.lab.types.DataDictionary; -import java.io.File; - -/** Interface used to handle any configuration */ -public interface Configuration extends DataDictionary { - - /** - * Load configuration from file - * - * @throws ConfigurationException load file failed - */ - public void load() throws ConfigurationException; - - /** - * Save configuration to file - * - * @throws ConfigurationException save file failed - */ - public void save() throws ConfigurationException; - - /** - * Get config name - * - * @return configuration name - */ - public String getConfigName(); - - /** - * Get config file - * - * @return configuration file reference - */ - public File getConfigFile(); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.configuration; + +import enedis.lab.types.DataDictionary; +import java.io.File; + +/** + * Generic interface for handling application configuration objects. + * + *

    This interface defines the basic operations for loading, saving, and identifying a + * configuration, regardless of its type or format. It extends {@link DataDictionary} to allow + * structured manipulation of configuration parameters. + * + *

      + *
    • Robust loading and saving with exception handling + *
    • Unique identification by name and associated file + *
    • Interoperability with data dictionaries + *
    + * + * @author Enedis Smarties team + * @see DataDictionary + * @see ConfigurationException + */ +public interface Configuration extends DataDictionary { + + /** + * Loads the configuration. + * + *

    Must throw a {@link ConfigurationException} if reading fails, the format is invalid, or the + * data is inconsistent. + * + * @throws ConfigurationException if loading fails or the format is invalid + */ + public void load() throws ConfigurationException; + + /** + * Saves the current configuration. + * + *

    Must throw a {@link ConfigurationException} if writing fails or if there is an access + * problem. + * + * @throws ConfigurationException if saving fails + */ + public void save() throws ConfigurationException; + + /** + * Returns the unique name of the configuration. + * + *

    This name is used to identify the configuration within the application or for user display. + * + * @return the configuration name (never null) + */ + public String getConfigName(); + + /** + * Returns the reference to the associated configuration file. + * + *

    May return {@code null} if the configuration is not linked to a physical file. + * + * @return the configuration file, or {@code null} if not applicable + */ + public File getConfigFile(); +} diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java b/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java index 2884274..3912c4c 100644 --- a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java +++ b/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java @@ -13,50 +13,86 @@ import java.io.File; import java.util.Map; -/** Basic implementation of Configuration */ +/** + * Base implementation of the {@link Configuration} interface for application configuration + * management. + * + *

    This class extends {@link DataDictionaryBase} and provides default mechanisms for loading, + * saving, and identifying configuration objects. + * + *

    Key features include: + * + *

      + *
    • File-based persistence with structured exception reporting + *
    • Support for cloning and copying configuration data + *
    • Consistent identification by name and file reference + *
    + * + *

    Intended to be subclassed for specific configuration types (e.g., channel, datastream). + * + * @author Enedis Smarties team + * @see Configuration + * @see DataDictionaryBase + * @see ConfigurationException + */ public class ConfigurationBase extends DataDictionaryBase implements Configuration { + + /** The unique name identifying this configuration. */ private String name = null; + + /** The file associated with this configuration, or null if not file-based. */ private File file = null; + /** + * Protected default constructor for subclassing. + * + *

    Initializes an empty configuration with no name or file. + */ protected ConfigurationBase() { super(); } /** - * Constructor using datadictionary + * Constructs a configuration by copying from another {@link DataDictionary}. * - * @param other - * @throws DataDictionaryException + * @param other the data dictionary to copy from + * @throws DataDictionaryException if copying fails */ public ConfigurationBase(DataDictionary other) throws DataDictionaryException { super(other); } /** - * Constructor using Map + * Constructs a configuration from a map of key-value pairs. * - * @param map - * @throws DataDictionaryException + * @param map the map containing configuration data + * @throws DataDictionaryException if mapping fails */ public ConfigurationBase(Map map) throws DataDictionaryException { super(map); } /** - * Constructor using name and file + * Constructs a configuration with a specific name and file reference. * - * @param name - * @param file + * @param name the configuration name + * @param file the configuration file */ public ConfigurationBase(String name, File file) { super(); this.init(name, file); } + /** + * Loads the configuration from the associated file. + * + *

    Throws a {@link ConfigurationException} if loading or copying fails. + * + * @throws ConfigurationException if the file cannot be read or parsed + */ @Override public void load() throws ConfigurationException { DataDictionary configuration; - try { configuration = DataDictionaryBase.fromFile(this.file, this.getClass()); } catch (Exception exception) { @@ -84,6 +120,13 @@ public void load() throws ConfigurationException { } } + /** + * Saves the configuration to the associated file. + * + *

    Throws a {@link ConfigurationException} if writing fails. + * + * @throws ConfigurationException if the file cannot be written + */ @Override public void save() throws ConfigurationException { try { @@ -100,25 +143,41 @@ public void save() throws ConfigurationException { } } + /** + * Returns the unique name of this configuration. + * + * @return the configuration name, or null if not set + */ @Override public String getConfigName() { return this.name; } + /** + * Returns the file associated with this configuration. + * + * @return the configuration file, or null if not file-based + */ @Override public File getConfigFile() { return this.file; } /** - * Set config name + * Sets the configuration name. * - * @param configName + * @param configName the new configuration name */ public void setConfigName(String configName) { this.name = configName; } + /** + * Initializes the configuration with a name and file reference. + * + * @param name the configuration name + * @param file the configuration file + */ protected void init(String name, File file) { this.name = name; this.file = file; diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java b/src/main/java/enedis/lab/types/configuration/ConfigurationException.java index 7762c40..907bac1 100644 --- a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java +++ b/src/main/java/enedis/lab/types/configuration/ConfigurationException.java @@ -9,68 +9,84 @@ import enedis.lab.types.DataDictionaryException; -/** Configuration exception */ +/** + * Exception type for configuration-related errors. + * + *

    This exception is thrown to indicate problems encountered during configuration operations, + * such as missing or invalid parameters, file I/O errors, or type mismatches. It extends {@link + * DataDictionaryException} to provide compatibility with the data dictionary error handling + * framework. + * + *

    Static factory methods are provided for common error scenarios, and all standard exception + * constructors are available for flexibility. + * + * @author Enedis Smarties team + * @see DataDictionaryException + */ public class ConfigurationException extends DataDictionaryException { + + /** Serialization identifier for compatibility. */ private static final long serialVersionUID = 8090072693974075297L; /** - * Create missing parameter exception + * Creates an exception indicating that a required configuration parameter is missing. * - * @param parameter - * @return configurationException + * @param parameter the name of the missing parameter + * @return a new {@code ConfigurationException} describing the error */ public static ConfigurationException createMissingParameterException(String parameter) { - return new ConfigurationException("Parameter \'" + parameter + "\' is missing"); + return new ConfigurationException("Parameter '" + parameter + "' is missing"); } /** - * Create unknown parameter exception + * Creates an exception indicating that an unknown configuration parameter was encountered. * - * @param parameter - * @return configurationException + * @param parameter the name of the unknown parameter + * @return a new {@code ConfigurationException} describing the error */ public static ConfigurationException createUnknownParameterException(String parameter) { - return new ConfigurationException("Parameter \'" + parameter + "\' is unknown"); + return new ConfigurationException("Parameter '" + parameter + "' is unknown"); } /** - * Create invalid parameter value exception + * Creates an exception indicating that a configuration parameter has an invalid value. * - * @param parameter - * @param info - * @return configurationException + * @param parameter the name of the parameter + * @param info additional information about the invalid value + * @return a new {@code ConfigurationException} describing the error */ public static ConfigurationException createInvalidParameterValueException( String parameter, String info) { return new ConfigurationException( - "Parameter \'" + parameter + "\' has an invalid value : " + info); + "Parameter '" + parameter + "' has an invalid value : " + info); } /** - * Create invalid paramter type exception + * Creates an exception indicating that a configuration parameter has an invalid type. * - * @param parameter - * @param type - * @return configurationException + * @param parameter the name of the parameter + * @param type the expected type + * @return a new {@code ConfigurationException} describing the error */ public static ConfigurationException createInvalidParameterTypeException( String parameter, Class type) { return new ConfigurationException( - "Parameter \'" + parameter + "\' has an invalid type : " + type.getName()); + "Parameter '" + parameter + "' has an invalid type : " + type.getName()); } - /** Default constructor */ + /** Constructs a new configuration exception with no detail message. */ public ConfigurationException() { super(); } /** - * Constructor using message, cause, enable suppression flag and writable stack trace flag + * Constructs a new configuration exception with the specified detail message, cause, suppression + * enabled or disabled, and writable stack trace enabled or disabled. * - * @param message - * @param cause - * @param enableSuppression - * @param writableStackTrace + * @param message the detail message + * @param cause the cause + * @param enableSuppression whether suppression is enabled or disabled + * @param writableStackTrace whether the stack trace should be writable */ public ConfigurationException( String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { @@ -78,28 +94,28 @@ public ConfigurationException( } /** - * Constructor using message and cause + * Constructs a new configuration exception with the specified detail message and cause. * - * @param message - * @param cause + * @param message the detail message + * @param cause the cause */ public ConfigurationException(String message, Throwable cause) { super(message, cause); } /** - * Constructor using message + * Constructs a new configuration exception with the specified detail message. * - * @param message + * @param message the detail message */ public ConfigurationException(String message) { super(message); } /** - * Constructor using cause + * Constructs a new configuration exception with the specified cause. * - * @param cause + * @param cause the cause */ public ConfigurationException(Throwable cause) { super(cause); diff --git a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java b/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java index a2c3def..26721ad 100644 --- a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java +++ b/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java @@ -33,19 +33,40 @@ import org.json.JSONException; import org.json.JSONObject; -/** DataDictionary basic implementation */ +/** + * Basic implementation of the {@link DataDictionary} interface. + * + *

    This class provides a flexible and extensible data dictionary structure for storing key-value + * pairs, supporting serialization to and from JSON, file and stream I/O, and key descriptor + * management. It is designed to be used as a base class for more specific data dictionary + * implementations. + * + *

    Features include: + * + *

      + *
    • Static factory methods for instantiating from files, streams, strings, JSON objects, and + * maps + *
    • Support for nested data dictionaries and lists + *
    • Key descriptor management for type safety and validation + *
    • Serialization and deserialization to/from JSON and Java Map + *
    • Cloning, equality, and string representation + *
    • Mandatory and optional key validation + *
    + * + * @author Enedis Smarties team + */ public class DataDictionaryBase implements DataDictionary { private static final int JSON_INDENT_FACTOR = 0; /** - * Instantiate a datadictionary from a File + * Creates a new data dictionary instance from a file containing JSON data. * - * @param file - * @param clazz - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - * @throws IOException + * @param file the file to read from + * @param clazz the class of the data dictionary to instantiate + * @return a new data dictionary instance + * @throws JSONException if the file content is not valid JSON + * @throws DataDictionaryException if the dictionary cannot be created + * @throws IOException if an I/O error occurs */ public static DataDictionary fromFile(File file, Class clazz) throws JSONException, DataDictionaryException, IOException { @@ -59,14 +80,14 @@ public static DataDictionary fromFile(File file, Class } /** - * Instantiate a datadictionary from a Stream + * Creates a new data dictionary instance from an input stream containing JSON data. * - * @param stream - * @param clazz - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - * @throws IOException + * @param stream the input stream to read from + * @param clazz the class of the data dictionary to instantiate + * @return a new data dictionary instance + * @throws JSONException if the stream content is not valid JSON + * @throws DataDictionaryException if the dictionary cannot be created + * @throws IOException if an I/O error occurs */ public static DataDictionary fromStream(InputStream stream, Class clazz) throws JSONException, DataDictionaryException, IOException { @@ -83,13 +104,13 @@ public static DataDictionary fromStream(InputStream stream, Class clazz) throws JSONException, DataDictionaryException { @@ -103,13 +124,13 @@ public static DataDictionary fromString(String text, Class clazz) @@ -122,13 +143,12 @@ public static DataDictionary fromJSON( } /** - * Instantiate a datadictionary from a Map + * Creates a new data dictionary instance from a map of key-value pairs and a dictionary class. * - * @param map - * @param clazz - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException + * @param map the map containing key-value pairs + * @param clazz the class of the data dictionary to instantiate + * @return a new data dictionary instance + * @throws DataDictionaryException if the dictionary cannot be created */ public static DataDictionary fromMap( Map map, Class clazz) @@ -158,12 +178,11 @@ public static DataDictionary fromMap( } /** - * Instantiate a datadictionary from a Map + * Creates a new data dictionary instance from a map of key-value pairs. * - * @param map - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException + * @param map the map containing key-value pairs + * @return a new data dictionary instance + * @throws DataDictionaryException if the dictionary cannot be created */ public static DataDictionary fromMap(Map map) throws DataDictionaryException { if (map == null) { @@ -188,19 +207,22 @@ public static DataDictionary fromMap(Map map) throws DataDiction return dataDictionnary; } + /** List of key descriptors defining the structure and validation rules for this dictionary. */ private List> keyDescriptors; + + /** Internal map storing the key-value pairs of the dictionary. */ protected HashMap data; - /** Default constructor */ + /** Constructs an empty data dictionary with no key descriptors or data. */ public DataDictionaryBase() { this.init(); } /** - * Constructor using map and setting key not defined allowed flag + * Constructs a data dictionary from a map of key-value pairs. * - * @param map - * @throws DataDictionaryException + * @param map the map containing initial key-value pairs + * @throws DataDictionaryException if an error occurs during initialization */ public DataDictionaryBase(Map map) throws DataDictionaryException { this(); @@ -208,10 +230,10 @@ public DataDictionaryBase(Map map) throws DataDictionaryExceptio } /** - * Constructor using dataDictionary and setting key not defined allowed flag + * Constructs a data dictionary by copying another data dictionary. * - * @param other - * @throws DataDictionaryException + * @param other the data dictionary to copy + * @throws DataDictionaryException if an error occurs during initialization */ public DataDictionaryBase(DataDictionary other) throws DataDictionaryException { this(); @@ -219,18 +241,33 @@ public DataDictionaryBase(DataDictionary other) throws DataDictionaryException { } @Override + /** + * Checks if a key exists in the data dictionary. + * + * @param key the key to check + * @return true if the key exists, false otherwise + */ public final boolean exists(String key) { return this.data.containsKey(key); } @Override + /** + * Checks if a key exists in the key descriptors. + * + * @param key the key to check + * @return true if the key exists in the key descriptors, false otherwise + */ public final boolean existsInKeys(String key) { - // @formatter:off return this.keyDescriptors.stream().filter(k -> k.getName().equals(key)).findAny().isPresent(); - // @formatter:on } @Override + /** + * Returns all keys present in the data dictionary. + * + * @return an array of all keys + */ public final String[] keys() { Set setKeys = this.data.keySet(); String[] keys = new String[setKeys.size()]; @@ -241,6 +278,11 @@ public final String[] keys() { } @Override + /** + * Adds a key to the data dictionary if it does not already exist. + * + * @param key the key to add + */ public final void addKey(String key) { if (key != null) { this.data.putIfAbsent(key, null); @@ -248,6 +290,11 @@ public final void addKey(String key) { } @Override + /** + * Removes a key from the data dictionary. + * + * @param key the key to remove + */ public final void removeKey(String key) { if (key != null) { this.data.remove(key); @@ -255,11 +302,24 @@ public final void removeKey(String key) { } @Override + /** + * Retrieves the value associated with a key in the data dictionary. + * + * @param key the key whose value is to be returned + * @return the value associated with the key, or null if not present + */ public final Object get(String key) { return this.data.get(key); } @Override + /** + * Sets the value for a key in the data dictionary. + * + * @param key the key to set + * @param value the value to associate with the key + * @throws DataDictionaryException if the key is not allowed or an error occurs + */ public final void set(String key, Object value) throws DataDictionaryException { if (key == null) { throw new DataDictionaryException("Key null not allowed"); @@ -272,12 +332,10 @@ public final void set(String key, Object value) throws DataDictionaryException { if (keyDescriptor.isPresent()) { try { String setterName = this.getSetterName(key); - // @formatter:off List methodNameList = Arrays.asList(this.getClass().getDeclaredMethods()).stream() .map(m -> m.getName()) .collect(Collectors.toList()); - // @formatter:on if (methodNameList.contains(setterName)) { Method method = this.getClass().getDeclaredMethod(setterName, Object.class); method.setAccessible(true); @@ -313,6 +371,12 @@ public final void set(String key, Object value) throws DataDictionaryException { } @Override + /** + * Copies all key-value pairs from another data dictionary into this one. + * + * @param other the data dictionary to copy from + * @throws DataDictionaryException if an error occurs during copying + */ public void copy(DataDictionary other) throws DataDictionaryException { String[] otherKeys = other.keys(); @@ -325,11 +389,17 @@ public void copy(DataDictionary other) throws DataDictionaryException { } @Override + /** Removes all key-value pairs from the data dictionary. */ public final void clear() { this.data.clear(); } @Override + /** + * Returns the hash code value for this data dictionary. + * + * @return the hash code value + */ public final int hashCode() { final int prime = 31; int result = 1; @@ -338,6 +408,12 @@ public final int hashCode() { } @Override + /** + * Compares this data dictionary to another object for equality. + * + * @param object the object to compare + * @return true if the objects are equal, false otherwise + */ public final boolean equals(Object object) { if (object == null) { return false; @@ -430,6 +506,13 @@ public final boolean equals(Object object) { } @Override + /** + * Writes the data dictionary to a file as JSON. + * + * @param file the file to write to + * @param indentFactor the number of spaces to add to each level of indentation + * @throws IOException if an I/O error occurs + */ public final void toFile(File file, int indentFactor) throws IOException { OutputStream stream = new FileOutputStream(file); @@ -437,6 +520,13 @@ public final void toFile(File file, int indentFactor) throws IOException { } @Override + /** + * Writes the data dictionary to an output stream as JSON. + * + * @param stream the output stream to write to + * @param indentFactor the number of spaces to add to each level of indentation + * @throws IOException if an I/O error occurs + */ public final void toStream(OutputStream stream, int indentFactor) throws IOException { String text = this.toString(indentFactor); @@ -444,6 +534,11 @@ public final void toStream(OutputStream stream, int indentFactor) throws IOExcep } @Override + /** + * Serializes the data dictionary to a {@link JSONObject}. + * + * @return the JSON representation of the data dictionary + */ public JSONObject toJSON() { JSONObject jsonObject = new JSONObject(); String[] keys = this.keys(); @@ -467,21 +562,42 @@ public JSONObject toJSON() { @SuppressWarnings("unchecked") @Override + /** + * Returns a shallow copy of the internal map representing the data dictionary. + * + * @return a map containing all key-value pairs + */ public final Map toMap() { return (Map) this.data.clone(); } @Override + /** + * Returns a string representation of the data dictionary in JSON format. + * + * @return the string representation of the data dictionary + */ public String toString() { return this.toString(DataDictionaryBase.JSON_INDENT_FACTOR); } @Override + /** + * Returns a string representation of the data dictionary in JSON format with indentation. + * + * @param identFactor the number of spaces to add to each level of indentation + * @return the string representation of the data dictionary + */ public String toString(int identFactor) { return this.toJSON().toString(identFactor); } @Override + /** + * Creates and returns a deep copy of this data dictionary. + * + * @return a clone of this data dictionary + */ public DataDictionaryBase clone() { try { Constructor constructor = this.getClass().getConstructor(DataDictionary.class); @@ -492,15 +608,26 @@ public DataDictionaryBase clone() { } @Override + /** Prints the data dictionary to the standard output in pretty JSON format. */ public void print() { System.out.println(this.toString(2)); } + /** + * Updates optional parameters and checks that all mandatory parameters are present. + * + * @throws DataDictionaryException if a mandatory parameter is missing or invalid + */ protected final void checkAndUpdate() throws DataDictionaryException { this.updateOptionalParameters(); this.checkMandatoryParameters(); } + /** + * Checks that all mandatory parameters defined by key descriptors are present in the dictionary. + * + * @throws DataDictionaryException if a mandatory key is missing + */ protected void checkMandatoryParameters() throws DataDictionaryException { for (KeyDescriptor key : this.keyDescriptors) { if (key.isMandatory() && !this.exists(key.getName())) { @@ -509,8 +636,20 @@ protected void checkMandatoryParameters() throws DataDictionaryException { } } + /** + * Updates optional parameters in the data dictionary. Subclasses may override to provide custom + * logic. + * + * @throws DataDictionaryException if an error occurs during update + */ protected void updateOptionalParameters() throws DataDictionaryException {} + /** + * Adds a key descriptor to the list of key descriptors for this dictionary. + * + * @param keyDescriptor the key descriptor to add + * @throws DataDictionaryException if the key already exists + */ protected void addKeyDescriptor(KeyDescriptor keyDescriptor) throws DataDictionaryException { if (this.existsInKeys(keyDescriptor.getName())) { throw new DataDictionaryException("Key " + keyDescriptor.getName() + "already exists"); @@ -518,6 +657,12 @@ protected void addKeyDescriptor(KeyDescriptor keyDescriptor) throws DataDicti this.keyDescriptors.add(keyDescriptor); } + /** + * Adds all key descriptors from the provided list to this dictionary. + * + * @param keyDescriptor the list of key descriptors to add + * @throws DataDictionaryException if a key already exists + */ protected void addAllKeyDescriptor(List> keyDescriptor) throws DataDictionaryException { for (KeyDescriptor key : keyDescriptor) { @@ -525,10 +670,14 @@ protected void addAllKeyDescriptor(List> keyDescriptor) } } + /** + * Retrieves the key descriptor for the specified key name, if present. + * + * @param name the name of the key + * @return an {@link Optional} containing the key descriptor if found, or empty otherwise + */ protected Optional> getKeyDescriptor(String name) { - // @formatter:off return this.keyDescriptors.stream().filter(k -> k.getName().equals(name)).findFirst(); - // @formatter:on } private String getSetterName(String key) { diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java index 5d62b32..40c9106 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java @@ -1,63 +1,71 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor interface - * - * @param - */ -public interface KeyDescriptor { - /** - * Get key name - * - * @return key name - */ - public String getName(); - - /** - * Get mandatory flag - * - * @return mandatory flag - */ - public boolean isMandatory(); - - /** - * Set mandatory flag - * - * @param mandatory - */ - public void setMandatory(boolean mandatory); - - /** - * Set a list of accepted values - * - * @param acceptedValues - */ - @SuppressWarnings("unchecked") - public void setAcceptedValues(T... acceptedValues); - - /** - * Convert a Object value to a T value - * - * @param value object to convert - * @return value converted to T type - * @throws DataDictionaryException - */ - public T convert(Object value) throws DataDictionaryException; - - /** - * Convert a T value to String - * - * @param value value to convert to String - * @return String representation of the value - */ - public String toString(T value); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.types.datadictionary; + +import enedis.lab.types.DataDictionaryException; + +/** + * Interface for describing a key in a data dictionary structure. + * + *

    A key descriptor defines the name, type, and validation rules for a key in a {@link + * enedis.lab.types.DataDictionary}. Implementations may specify accepted values, conversion logic, + * and whether the key is mandatory. + * + * @param the type of value associated with the key + * @author Enedis Smarties team + */ +public interface KeyDescriptor { + + /** + * Returns the name of the key described by this descriptor. + * + * @return the key name (never null) + */ + String getName(); + + /** + * Indicates whether the key is mandatory in the data dictionary. + * + * @return true if the key is mandatory, false otherwise + */ + boolean isMandatory(); + + /** + * Sets whether the key is mandatory in the data dictionary. + * + * @param mandatory true to mark the key as mandatory, false otherwise + */ + void setMandatory(boolean mandatory); + + /** + * Sets the list of accepted values for this key. If not set, any value of type T may be accepted + * depending on implementation. + * + * @param acceptedValues the accepted values for this key + */ + @SuppressWarnings("unchecked") + void setAcceptedValues(T... acceptedValues); + + /** + * Converts an object to the value type T for this key. Implementations should validate and + * convert the input as needed. + * + * @param value the object to convert + * @return the converted value of type T + * @throws DataDictionaryException if the value cannot be converted or is invalid + */ + T convert(Object value) throws DataDictionaryException; + + /** + * Converts a value of type T to its string representation. + * + * @param value the value to convert to String + * @return the string representation of the value + */ + String toString(T value); +} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java index c81459f..67d3df3 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java @@ -12,20 +12,29 @@ import java.util.List; /** - * DataDictionary key descriptor base + * Abstract base class for key descriptors in a data dictionary. * - * @param + *

    Provides common logic for key name, mandatory flag, accepted values, and value + * conversion/validation. Subclasses must implement the type-specific conversion logic. + * + * @param the type of value associated with the key + * @author Enedis Smarties team */ public abstract class KeyDescriptorBase implements KeyDescriptor { + /** The name of the key described by this descriptor. */ private String name; + + /** Whether the key is mandatory in the data dictionary. */ private boolean mandatory; + + /** List of accepted values for this key, or null if any value is accepted. */ protected List acceptedValues; /** - * Constructor setting all attributes + * Constructs a key descriptor with the given name and mandatory flag. * - * @param name - * @param mandatory + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise */ public KeyDescriptorBase(String name, boolean mandatory) { super(); @@ -33,6 +42,14 @@ public KeyDescriptorBase(String name, boolean mandatory) { this.mandatory = mandatory; } + /** + * Converts an object to the value type T for this key, checking accepted values if set. + * + * @param value the object to convert + * @return the converted value of type T + * @throws DataDictionaryException if the value is null and mandatory, or not accepted, or cannot + * be converted + */ @Override public final T convert(Object value) throws DataDictionaryException { T convertedValue = null; @@ -49,6 +66,12 @@ public final T convert(Object value) throws DataDictionaryException { return convertedValue; } + /** + * Returns the string representation of a value of type T. + * + * @param value the value to convert to String + * @return the string representation, or null if value is null + */ @Override public String toString(T value) { if (value == null) { @@ -59,9 +82,9 @@ public String toString(T value) { } /** - * Get key name + * Returns the name of the key described by this descriptor. * - * @return key name + * @return the key name */ @Override public String getName() { @@ -69,18 +92,18 @@ public String getName() { } /** - * Set key name + * Sets the name of the key described by this descriptor. * - * @param name + * @param name the key name */ public void setName(String name) { this.name = name; } /** - * Get mandatory flag + * Indicates whether the key is mandatory in the data dictionary. * - * @return mandatory flag + * @return true if the key is mandatory, false otherwise */ @Override public boolean isMandatory() { @@ -88,9 +111,9 @@ public boolean isMandatory() { } /** - * Set mandatory flag + * Sets whether the key is mandatory in the data dictionary. * - * @param mandatory + * @param mandatory true to mark the key as mandatory, false otherwise */ @Override public void setMandatory(boolean mandatory) { @@ -98,9 +121,9 @@ public void setMandatory(boolean mandatory) { } /** - * Set mandatory value + * Sets the list of accepted values for this key. * - * @param acceptedValues + * @param acceptedValues the accepted values for this key */ @SuppressWarnings("unchecked") @Override @@ -108,14 +131,35 @@ public void setAcceptedValues(T... acceptedValues) { this.acceptedValues = Arrays.asList(acceptedValues); } + /** + * Converts an object to the value type T for this key (type-specific logic). Subclasses must + * implement this method. + * + * @param value the object to convert + * @return the converted value of type T + * @throws DataDictionaryException if the value cannot be converted + */ protected abstract T convertValue(Object value) throws DataDictionaryException; + /** + * Handles the case where a null value is set for this key. Throws an exception if the key is + * mandatory. + * + * @throws DataDictionaryException if the key is mandatory + */ protected final void handleNullValue() throws DataDictionaryException { if (this.isMandatory()) { throw new DataDictionaryException("Cannot set null " + this.getName()); } } + /** + * Checks if the given value is among the accepted values for this key, if any are set. Throws an + * exception if the value is not accepted. + * + * @param value the value to check + * @throws DataDictionaryException if the value is not accepted + */ protected void checkAcceptedValues(T value) throws DataDictionaryException { if (this.acceptedValues != null) { boolean accepted = false; diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java index 23fe95c..80de482 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java @@ -14,26 +14,40 @@ import java.util.Map; /** - * DataDictionary key descriptor DataDictionary + * Key descriptor for values that are themselves data dictionaries. * - * @param + *

    This descriptor allows a key to be associated with a nested {@link DataDictionaryBase} value. + * It supports conversion from strings, maps, and other data dictionary instances to the target + * type. + * + * @param the type of data dictionary accepted as value + * @author Enedis Smarties team */ public class KeyDescriptorDataDictionary extends KeyDescriptorBase { + /** The class of the data dictionary accepted as value for this key. */ private Class dataDictionaryClass; /** - * Default constructor + * Constructs a key descriptor for a data dictionary value. * - * @param name - * @param mandatory - * @param dataDictionaryClass + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise + * @param dataDictionaryClass the class of the data dictionary accepted as value */ public KeyDescriptorDataDictionary(String name, boolean mandatory, Class dataDictionaryClass) { super(name, mandatory); this.dataDictionaryClass = dataDictionaryClass; } + /** + * Converts an object to the data dictionary type T for this key. Accepts instances of T, strings + * (parsed as JSON), other DataDictionary, or Map. + * + * @param value the object to convert + * @return the converted data dictionary value + * @throws DataDictionaryException if the value cannot be converted + */ @SuppressWarnings("unchecked") @Override public T convertValue(Object value) throws DataDictionaryException { diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java index cc1f9f9..1e1717b 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java @@ -10,33 +10,41 @@ import enedis.lab.types.DataDictionaryException; /** - * DataDictionary key descriptor Enum + * Key descriptor for values that are Java enums. * - * @param + *

    This descriptor allows a key to be associated with an enum value, supporting conversion from + * strings and type-safe validation. Optionally, a prefix can be added to the string value before + * conversion. + * + * @param the enum type accepted as value + * @author Enedis Smarties team */ @SuppressWarnings("rawtypes") public class KeyDescriptorEnum extends KeyDescriptorBase { + /** The enum class accepted as value for this key. */ private Class enumClass; + + /** Optional prefix to prepend to the string value before conversion. */ private String prefix; /** - * Default constructor + * Constructs a key descriptor for an enum value with no prefix. * - * @param name - * @param mandatory - * @param enumClass + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise + * @param enumClass the enum class accepted as value */ public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass) { this(name, mandatory, enumClass, ""); } /** - * Constructor setting all parameters + * Constructs a key descriptor for an enum value with a prefix. * - * @param name - * @param mandatory - * @param enumClass - * @param prefix + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise + * @param enumClass the enum class accepted as value + * @param prefix the prefix to prepend to the string value before conversion */ public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass, String prefix) { super(name, mandatory); @@ -44,6 +52,14 @@ public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass, Str this.prefix = prefix; } + /** + * Converts an object to the enum type T for this key. Accepts instances of T or strings + * (converted to enum constant). + * + * @param value the object to convert + * @return the converted enum value + * @throws DataDictionaryException if the value cannot be converted + */ @Override public T convertValue(Object value) throws DataDictionaryException { T convertedValue = null; @@ -65,6 +81,14 @@ public T convertValue(Object value) throws DataDictionaryException { return convertedValue; } + /** + * Converts a string to the enum type T, applying the prefix if set. The string is uppercased and + * dots/hyphens are replaced with underscores. + * + * @param value the string value to convert + * @return the corresponding enum constant + * @throws DataDictionaryException if the value does not match any enum constant + */ @SuppressWarnings("unchecked") protected T toEnum(String value) throws DataDictionaryException { T convertedValue = null; diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java index bf7e71c..baf6ab3 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java @@ -19,25 +19,39 @@ import org.json.JSONObject; /** - * DataDictionary key descriptor List + * Key descriptor for values that are lists of a specific type. * - * @param + *

    This descriptor allows a key to be associated with a list of items, supporting conversion from + * arrays, lists, maps, and JSON objects. It handles type conversion for each item, including + * support for nested data dictionaries and enums. + * + * @param the type of item in the list + * @author Enedis Smarties team */ public class KeyDescriptorList extends KeyDescriptorBase> { + /** The class of the item accepted in the list for this key. */ private Class itemClass; /** - * Default constructor + * Constructs a key descriptor for a list value. * - * @param name - * @param mandatory - * @param itemClass + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise + * @param itemClass the class of the item accepted in the list */ public KeyDescriptorList(String name, boolean mandatory, Class itemClass) { super(name, mandatory); this.itemClass = itemClass; } + /** + * Converts an object to a list of items of type T for this key. Accepts single items, lists, + * arrays, or maps, and converts each item as needed. + * + * @param value the object to convert + * @return the converted list of items + * @throws DataDictionaryException if the value or any item cannot be converted + */ @Override public List convertValue(Object value) throws DataDictionaryException { List convertedValue = new ArrayList(); @@ -71,6 +85,13 @@ public List convertValue(Object value) throws DataDictionaryException { return convertedValue; } + /** + * Converts a single item to the type T, handling data dictionaries and enums. + * + * @param item the item to convert + * @return the converted item + * @throws DataDictionaryException if the item cannot be converted + */ private T convertItem(Object item) throws DataDictionaryException { T convertedItem = null; if (this.itemClass.isAssignableFrom(item.getClass())) { @@ -91,6 +112,13 @@ private T convertItem(Object item) throws DataDictionaryException { return convertedItem; } + /** + * Converts an item to a data dictionary of type T, using a Map or JSONObject. + * + * @param item the item to convert + * @return the converted data dictionary item + * @throws DataDictionaryException if the item cannot be converted + */ private T convertDataDictionaryItem(Object item) throws DataDictionaryException { T convertedItem = null; if (item instanceof JSONObject) { @@ -130,6 +158,13 @@ private T convertDataDictionaryItem(Object item) throws DataDictionaryException return convertedItem; } + /** + * Converts an item to an enum of type T, using the valueOf method. + * + * @param item the item to convert (must be a String) + * @return the converted enum item + * @throws DataDictionaryException if the item cannot be converted + */ @SuppressWarnings("unchecked") private T convertEnumItem(Object item) throws DataDictionaryException { T convertedItem = null; @@ -146,7 +181,6 @@ private T convertEnumItem(Object item) throws DataDictionaryException { } catch (InvocationTargetException e) { e.printStackTrace(); } - } else { throw new DataDictionaryException( "Key " diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java index 68cbd3d..84c6210 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java @@ -12,32 +12,37 @@ import java.util.List; /** - * DataDictionary key descriptor Number min max + * Key descriptor for list values with minimum and maximum size constraints. * - * @param + *

    This descriptor extends {@link KeyDescriptorList} to enforce that the list size is within + * specified bounds. It uses a {@link MinMaxChecker} to validate the size after conversion. + * + * @param the type of item in the list + * @author Enedis Smarties team */ public class KeyDescriptorListMinMaxSize extends KeyDescriptorList { + /** Utility for checking minimum and maximum list size constraints. */ private MinMaxChecker minMaxChecker; /** - * Default constructor + * Constructs a key descriptor for a list value with no size constraints. * - * @param name - * @param mandatory - * @param itemClass + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise + * @param itemClass the class of the item accepted in the list */ public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass) { this(name, mandatory, itemClass, null, null); } /** - * Constructor setting all attributes + * Constructs a key descriptor for a list value with minimum and maximum size constraints. * - * @param name - * @param mandatory - * @param itemClass - * @param min - * @param max + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise + * @param itemClass the class of the item accepted in the list + * @param min the minimum allowed list size (nullable) + * @param max the maximum allowed list size (nullable) */ public KeyDescriptorListMinMaxSize( String name, boolean mandatory, Class itemClass, Integer min, Integer max) { @@ -45,28 +50,34 @@ public KeyDescriptorListMinMaxSize( this.minMaxChecker = new MinMaxChecker(min, max); } + /** + * Converts an object to a list of items of type T and checks the list size constraints. + * + * @param value the object to convert + * @return the converted list of items + * @throws DataDictionaryException if the value or any item cannot be converted, or if the list + * size is out of bounds + */ @Override public List convertValue(Object value) throws DataDictionaryException { List convertedValue = super.convertValue(value); - this.check(convertedValue); - return convertedValue; } /** - * Get min + * Returns the minimum allowed list size, or null if not set. * - * @return min + * @return the minimum allowed list size */ public Integer getMin() { return this.minMaxChecker.getMin().intValue(); } /** - * Set min + * Sets the minimum allowed list size. * - * @param min + * @param min the minimum allowed list size * @throws IllegalArgumentException if min is greater than max */ public void setMin(Integer min) { @@ -74,24 +85,30 @@ public void setMin(Integer min) { } /** - * Get max + * Returns the maximum allowed list size, or null if not set. * - * @return max + * @return the maximum allowed list size */ public Integer getMax() { return this.minMaxChecker.getMax().intValue(); } /** - * Set max + * Sets the maximum allowed list size. * - * @param max + * @param max the maximum allowed list size * @throws IllegalArgumentException if max is smaller than min */ public void setMax(Integer max) { this.minMaxChecker.setMax(max); } + /** + * Checks that the list size is within the allowed bounds. + * + * @param list the list to check + * @throws DataDictionaryException if the list size is out of bounds + */ private void check(List list) throws DataDictionaryException { if (list != null) { if (!this.minMaxChecker.check(list.size())) { diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java index 8e53fb2..4af8812 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java @@ -12,52 +12,74 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -/** DataDictionary key descriptor LocalDateTime */ +/** + * Key descriptor for values that are {@link LocalDateTime} instances. + * + *

    This descriptor allows a key to be associated with a date-time value, supporting conversion + * from strings using a configurable pattern. It provides default formatting and parsing logic for + * date-time values. + * + * @author Enedis Smarties team + */ public class KeyDescriptorLocalDateTime extends KeyDescriptorBase { - /** Default pattern used */ + /** Default date-time pattern used for formatting and parsing. */ public static final String DEFAULT_PATTERN = "dd/MM/yyyy HH:mm:ss"; - /** Default formatter used */ + /** Default formatter used for date-time values. */ public static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_PATTERN); + /** Formatter used for this key descriptor. */ private DateTimeFormatter formatter; /** - * Default constructor + * Constructs a key descriptor for a date-time value using the default pattern. * - * @param name - * @param mandatory + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise */ public KeyDescriptorLocalDateTime(String name, boolean mandatory) { this(name, mandatory, DEFAULT_PATTERN); } /** - * Default constructor + * Constructs a key descriptor for a date-time value using a custom pattern. * - * @param name - * @param mandatory - * @param formatterPattern + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise + * @param formatterPattern the date-time pattern to use for formatting and parsing */ public KeyDescriptorLocalDateTime(String name, boolean mandatory, String formatterPattern) { super(name, mandatory); this.formatter = DateTimeFormatter.ofPattern(formatterPattern); } + /** + * Converts a {@link LocalDateTime} value to its string representation using the configured + * formatter. + * + * @param value the date-time value to convert + * @return the formatted string, or null if value is null + */ @Override public String toString(LocalDateTime value) { if (value == null) { return null; } - return this.formatter.format(value); } + /** + * Converts an object to a {@link LocalDateTime} value for this key. Accepts {@link LocalDateTime} + * instances or strings (parsed using the configured formatter). + * + * @param value the object to convert + * @return the converted date-time value + * @throws DataDictionaryException if the value cannot be converted + */ @Override public LocalDateTime convertValue(Object value) throws DataDictionaryException { LocalDateTime convertedValue = null; - if (value instanceof LocalDateTime) { convertedValue = (LocalDateTime) value; } else if (value instanceof String) { @@ -70,10 +92,16 @@ public LocalDateTime convertValue(Object value) throws DataDictionaryException { + value.getClass().getSimpleName() + " to LocalDateTime"); } - return convertedValue; } + /** + * Parses a string to a {@link LocalDateTime} using the configured formatter. + * + * @param value the string to parse + * @return the parsed date-time value + * @throws DataDictionaryException if the string cannot be parsed + */ private LocalDateTime toLocalDateTime(String value) throws DataDictionaryException { LocalDateTime convertedValue = null; try { diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java index c337601..cda1166 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java @@ -9,18 +9,34 @@ import enedis.lab.types.DataDictionaryException; -/** DataDictionary key descriptor Number */ +/** + * Key descriptor for values that are {@link Number} instances. + * + *

    This descriptor allows a key to be associated with a numeric value, supporting conversion from + * strings and type-safe validation. It provides logic for accepted values and conversion from + * string representations. + * + * @author Enedis Smarties team + */ public class KeyDescriptorNumber extends KeyDescriptorBase { /** - * Default constructor + * Constructs a key descriptor for a numeric value. * - * @param name - * @param mandatory + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise */ public KeyDescriptorNumber(String name, boolean mandatory) { super(name, mandatory); } + /** + * Converts an object to a {@link Number} value for this key. Accepts {@link Number} instances or + * strings (parsed as double). + * + * @param value the object to convert + * @return the converted number value + * @throws DataDictionaryException if the value cannot be converted + */ @Override public Number convertValue(Object value) throws DataDictionaryException { Number convertedValue = null; @@ -41,6 +57,14 @@ public Number convertValue(Object value) throws DataDictionaryException { return convertedValue; } + /** + * Checks if the given value is among the accepted values for this key, if any are set. Uses + * double value comparison for numeric equality. + * + * @param value the value to check + * @throws DataDictionaryException if the value is not accepted + */ + @Override protected void checkAcceptedValues(Number value) throws DataDictionaryException { if (this.acceptedValues != null) { boolean accepted = false; @@ -59,6 +83,13 @@ protected void checkAcceptedValues(Number value) throws DataDictionaryException } } + /** + * Converts a string to a {@link Number} (as double). + * + * @param value the string to convert + * @return the parsed number + * @throws DataDictionaryException if the string cannot be parsed as a number + */ private Number toNumber(String value) throws DataDictionaryException { Number out = null; try { diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java index 1a81202..c5fc481 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java @@ -10,55 +10,68 @@ import enedis.lab.types.DataDictionaryException; import enedis.lab.util.MinMaxChecker; -/** DataDictionary key descriptor Number min max */ +/** + * Key descriptor for numeric values with minimum and maximum constraints. + * + *

    This descriptor extends {@link KeyDescriptorNumber} to enforce that the value is within + * specified bounds. It uses a {@link MinMaxChecker} to validate the value after conversion. + * + * @author Enedis Smarties team + */ public class KeyDescriptorNumberMinMax extends KeyDescriptorNumber { + /** Utility for checking minimum and maximum value constraints. */ private MinMaxChecker minMaxChecker; /** - * Default constructor + * Constructs a key descriptor for a numeric value with no min/max constraints. * - * @param name - * @param mandatory + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise */ public KeyDescriptorNumberMinMax(String name, boolean mandatory) { this(name, mandatory, null, null); } /** - * Constructor setting all attributes + * Constructs a key descriptor for a numeric value with minimum and maximum constraints. * - * @param name - * @param mandatory - * @param min - * @param max + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise + * @param min the minimum allowed value (nullable) + * @param max the maximum allowed value (nullable) */ public KeyDescriptorNumberMinMax(String name, boolean mandatory, Number min, Number max) { super(name, mandatory); this.minMaxChecker = new MinMaxChecker(min, max); } + /** + * Converts an object to a {@link Number} value and checks the min/max constraints. + * + * @param value the object to convert + * @return the converted number value + * @throws DataDictionaryException if the value cannot be converted or is out of bounds + */ @Override public Number convertValue(Object value) throws DataDictionaryException { Number convertedValue = super.convertValue(value); - this.check(convertedValue); - return convertedValue; } /** - * Get min + * Returns the minimum allowed value, or null if not set. * - * @return min + * @return the minimum allowed value */ public Number getMin() { return this.minMaxChecker.getMin(); } /** - * Set min + * Sets the minimum allowed value. * - * @param min + * @param min the minimum allowed value * @throws IllegalArgumentException if min is greater than max */ public void setMin(Number min) { @@ -66,24 +79,30 @@ public void setMin(Number min) { } /** - * Get max + * Returns the maximum allowed value, or null if not set. * - * @return max + * @return the maximum allowed value */ public Number getMax() { return this.minMaxChecker.getMax(); } /** - * Set max + * Sets the maximum allowed value. * - * @param max + * @param max the maximum allowed value * @throws IllegalArgumentException if max is smaller than min */ public void setMax(Number max) { this.minMaxChecker.setMax(max); } + /** + * Checks that the value is within the allowed bounds. + * + * @param convertedValue the value to check + * @throws DataDictionaryException if the value is out of bounds + */ private void check(Number convertedValue) throws DataDictionaryException { if (convertedValue != null) { if (!this.minMaxChecker.check(convertedValue)) { diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java index 9268fd1..d58da9b 100644 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java +++ b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java @@ -9,63 +9,87 @@ import enedis.lab.types.DataDictionaryException; -/** DataDictionary key descriptor String */ +/** + * Key descriptor for values that are strings. + * + *

    This descriptor allows a key in a data dictionary to be associated with a string value, with + * optional control over whether empty strings are allowed. Provides conversion and validation logic + * for string values. + * + * @author Enedis Smarties team + */ public class KeyDescriptorString extends KeyDescriptorBase { + /** Default flag indicating whether empty strings are allowed. */ private static final boolean DEFAULT_EMPTY_ALLOW_FLAG = true; + /** Indicates whether empty strings are allowed for this key. */ private boolean emptyAllow; /** - * Default constructor + * Constructs a string key descriptor with the given name and mandatory flag. * - * @param name - * @param mandatory + *

    By default, empty strings are allowed. + * + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise */ public KeyDescriptorString(String name, boolean mandatory) { this(name, mandatory, DEFAULT_EMPTY_ALLOW_FLAG); } /** - * Default constructor + * Constructs a string key descriptor with the given name, mandatory flag, and empty string + * allowance. * - * @param name - * @param mandatory - * @param emptyAllow + * @param name the key name (must not be null) + * @param mandatory true if the key is mandatory, false otherwise + * @param emptyAllow true if empty strings are allowed, false otherwise */ public KeyDescriptorString(String name, boolean mandatory, boolean emptyAllow) { super(name, mandatory); this.emptyAllow = emptyAllow; } + /** + * Converts the given object to a string value for this key, applying validation rules. + * + *

    The value is converted using {@code toString()} and checked for emptiness if not allowed. + * + * @param value the object to convert + * @return the converted string value + * @throws DataDictionaryException if the value is empty and empty strings are not allowed + */ @Override public String convertValue(Object value) throws DataDictionaryException { - String convertedValue = null; - - convertedValue = value.toString(); - + String convertedValue = value.toString(); this.check(convertedValue); - return convertedValue; } /** - * Get empty allow flag + * Returns whether empty strings are allowed for this key. * - * @return empty allow flag + * @return true if empty strings are allowed, false otherwise */ public boolean isEmptyAllow() { return this.emptyAllow; } /** - * Set empty allow flag + * Sets whether empty strings are allowed for this key. * - * @param emptyAllow + * @param emptyAllow true to allow empty strings, false to disallow */ public void setEmptyAllow(boolean emptyAllow) { this.emptyAllow = emptyAllow; } + /** + * Checks if the given string value is valid according to the empty string allowance. + * + * @param value the string value to check + * @throws DataDictionaryException if the value is empty and empty strings are not allowed + */ private void check(String value) throws DataDictionaryException { if (!this.emptyAllow && value.trim().isEmpty()) { throw new DataDictionaryException("Key " + this.getName() + ": value can't be empty"); diff --git a/src/main/java/enedis/lab/util/SystemLibC.java b/src/main/java/enedis/lab/util/SystemLibC.java index 13f97dd..2ce675a 100644 --- a/src/main/java/enedis/lab/util/SystemLibC.java +++ b/src/main/java/enedis/lab/util/SystemLibC.java @@ -1,25 +1,25 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util; - -import com.sun.jna.Library; -import com.sun.jna.Native; - -/** Interface for system of C library */ -public interface SystemLibC extends Library { - /** Instance */ - SystemLibC INSTANCE = Native.load("c", SystemLibC.class); - - /** - * Get string error from code - * - * @param code - * @return string error - */ - public String strerror(int code); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util; + +import com.sun.jna.Library; +import com.sun.jna.Native; + +/** Interface for system of C library */ +public interface SystemLibC extends Library { + /** Instance */ + SystemLibC INSTANCE = Native.load("c", SystemLibC.class); + + /** + * Get string error from code + * + * @param code + * @return string error + */ + public String strerror(int code); +} diff --git a/src/main/java/enedis/lab/util/message/MessageType.java b/src/main/java/enedis/lab/util/message/MessageType.java index fdec4e4..b439314 100644 --- a/src/main/java/enedis/lab/util/message/MessageType.java +++ b/src/main/java/enedis/lab/util/message/MessageType.java @@ -1,18 +1,18 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -/** Message type */ -public enum MessageType { - /** Event */ - EVENT, - /** Request */ - REQUEST, - /** Response */ - RESPONSE; -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message; + +/** Message type */ +public enum MessageType { + /** Event */ + EVENT, + /** Request */ + REQUEST, + /** Response */ + RESPONSE; +} diff --git a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java index 3169c87..1735a53 100644 --- a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java @@ -1,101 +1,101 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Message; -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import org.json.JSONException; -import org.json.JSONObject; - -/** Message factory */ -public abstract class BasicMessageFactory { - /** - * Get message from text - * - * @param text - * @return message - * @throws MessageInvalidFormatException - * @throws MessageKeyTypeDoesntExistException - * @throws MessageKeyNameDoesntExistException - * @throws MessageInvalidTypeException - */ - public static Message getMessage(String text) - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException { - JSONObject messageJson = convertTextToJson(text); - - MessageType type = extractType(messageJson); - String name = extractName(messageJson); - - return createMessage(type, name); - } - - private static JSONObject convertTextToJson(String text) throws MessageInvalidFormatException { - try { - return new JSONObject(text); - } catch (JSONException e) { - throw new MessageInvalidFormatException( - "Invalid format, it should be JSON : " + e.getMessage()); - } - } - - private static MessageType extractType(JSONObject jsonObj) - throws MessageKeyTypeDoesntExistException, - MessageInvalidFormatException, - MessageInvalidTypeException { - try { - checkKeyTypeExists(jsonObj); - String typeStr = jsonObj.getString(Message.KEY_TYPE); - MessageType type = MessageType.valueOf(typeStr); - return type; - } catch (JSONException e) { - throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); - } catch (IllegalArgumentException e) { - throw new MessageInvalidTypeException("Invalid format : " + e.getMessage()); - } - } - - private static void checkKeyTypeExists(JSONObject jsonObj) - throws MessageKeyTypeDoesntExistException { - if (!jsonObj.has(Message.KEY_TYPE)) { - throw new MessageKeyTypeDoesntExistException("Key type missing"); - } - } - - private static String extractName(JSONObject jsonObj) - throws MessageInvalidFormatException, MessageKeyNameDoesntExistException { - try { - checkKeyNameExists(jsonObj); - return jsonObj.getString(Message.KEY_NAME); - } catch (JSONException e) { - throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); - } - } - - private static void checkKeyNameExists(JSONObject jsonObj) - throws MessageKeyNameDoesntExistException { - if (!jsonObj.has(Message.KEY_NAME)) { - throw new MessageKeyNameDoesntExistException("Key name missing"); - } - } - - private static Message createMessage(MessageType type, String name) { - try { - return new Message(type, name); - } catch (DataDictionaryException e) { - return null; - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import enedis.lab.types.DataDictionaryException; +import enedis.lab.util.message.Message; +import enedis.lab.util.message.MessageType; +import enedis.lab.util.message.exception.MessageInvalidFormatException; +import enedis.lab.util.message.exception.MessageInvalidTypeException; +import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; +import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; +import org.json.JSONException; +import org.json.JSONObject; + +/** Message factory */ +public abstract class BasicMessageFactory { + /** + * Get message from text + * + * @param text + * @return message + * @throws MessageInvalidFormatException + * @throws MessageKeyTypeDoesntExistException + * @throws MessageKeyNameDoesntExistException + * @throws MessageInvalidTypeException + */ + public static Message getMessage(String text) + throws MessageInvalidFormatException, + MessageKeyTypeDoesntExistException, + MessageKeyNameDoesntExistException, + MessageInvalidTypeException { + JSONObject messageJson = convertTextToJson(text); + + MessageType type = extractType(messageJson); + String name = extractName(messageJson); + + return createMessage(type, name); + } + + private static JSONObject convertTextToJson(String text) throws MessageInvalidFormatException { + try { + return new JSONObject(text); + } catch (JSONException e) { + throw new MessageInvalidFormatException( + "Invalid format, it should be JSON : " + e.getMessage()); + } + } + + private static MessageType extractType(JSONObject jsonObj) + throws MessageKeyTypeDoesntExistException, + MessageInvalidFormatException, + MessageInvalidTypeException { + try { + checkKeyTypeExists(jsonObj); + String typeStr = jsonObj.getString(Message.KEY_TYPE); + MessageType type = MessageType.valueOf(typeStr); + return type; + } catch (JSONException e) { + throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); + } catch (IllegalArgumentException e) { + throw new MessageInvalidTypeException("Invalid format : " + e.getMessage()); + } + } + + private static void checkKeyTypeExists(JSONObject jsonObj) + throws MessageKeyTypeDoesntExistException { + if (!jsonObj.has(Message.KEY_TYPE)) { + throw new MessageKeyTypeDoesntExistException("Key type missing"); + } + } + + private static String extractName(JSONObject jsonObj) + throws MessageInvalidFormatException, MessageKeyNameDoesntExistException { + try { + checkKeyNameExists(jsonObj); + return jsonObj.getString(Message.KEY_NAME); + } catch (JSONException e) { + throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); + } + } + + private static void checkKeyNameExists(JSONObject jsonObj) + throws MessageKeyNameDoesntExistException { + if (!jsonObj.has(Message.KEY_NAME)) { + throw new MessageKeyNameDoesntExistException("Key name missing"); + } + } + + private static Message createMessage(MessageType type, String name) { + try { + return new Message(type, name); + } catch (DataDictionaryException e) { + return null; + } + } +} diff --git a/src/main/java/enedis/lab/util/message/factory/EventFactory.java b/src/main/java/enedis/lab/util/message/factory/EventFactory.java index 5613aca..4945653 100644 --- a/src/main/java/enedis/lab/util/message/factory/EventFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/EventFactory.java @@ -1,18 +1,18 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Event; - -/** Request factory */ -public class EventFactory extends AbstractMessageFactory { - /** Default constructor */ - public EventFactory() { - super(Event.class); - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import enedis.lab.util.message.Event; + +/** Request factory */ +public class EventFactory extends AbstractMessageFactory { + /** Default constructor */ + public EventFactory() { + super(Event.class); + } +} diff --git a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java index 974a7c3..08c8b06 100644 --- a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java @@ -1,18 +1,18 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Request; - -/** Request factory */ -public class RequestFactory extends AbstractMessageFactory { - /** Default constructor */ - public RequestFactory() { - super(Request.class); - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import enedis.lab.util.message.Request; + +/** Request factory */ +public class RequestFactory extends AbstractMessageFactory { + /** Default constructor */ + public RequestFactory() { + super(Request.class); + } +} diff --git a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java index 4f7aaac..b96bab9 100644 --- a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java @@ -1,18 +1,18 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Response; - -/** Request factory */ -public class ResponseFactory extends AbstractMessageFactory { - /** Default constructor */ - public ResponseFactory() { - super(Response.class); - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.message.factory; + +import enedis.lab.util.message.Response; + +/** Request factory */ +public class ResponseFactory extends AbstractMessageFactory { + /** Default constructor */ + public ResponseFactory() { + super(Response.class); + } +} diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifier.java b/src/main/java/enedis/lab/util/task/FilteredNotifier.java index 9ca7ea5..55560c0 100644 --- a/src/main/java/enedis/lab/util/task/FilteredNotifier.java +++ b/src/main/java/enedis/lab/util/task/FilteredNotifier.java @@ -1,95 +1,95 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.Collection; -import java.util.function.Predicate; - -/** - * Notifier interface with filter - * - * @param the filter - * @param the subscriber - */ -public interface FilteredNotifier extends Notifier { - /** - * Add a subscriber with filter - * - * @param filter the filter used for subscription - * @param listener the subscriber reference - * @throws Exception on subscription failure - */ - public void subscribe(F filter, T listener) throws Exception; - - /** - * Remove a subscriber with filter - * - * @param filter the filter used for subscription - * @param listener the subscriber reference - * @throws Exception if filter or listener not found - */ - public void unsubscribe(F filter, T listener) throws Exception; - - /** - * Check if the given filter has a given subscriber - * - * @param filter the filter used for subscription - * @param listener the subscriber reference - * @return true if the given subscriber exists - */ - public boolean hasSubscriber(F filter, T listener); - - /** - * Get subscribers associated with filter - * - * @param filter the filter used for subscription - * @return The subscribers collection - */ - public Collection getSubscribers(F filter); - - /** - * Get subscribers including global and/or filters - * - * @param includeGlobal indicates if includes global subscribers - * @param includeFilter indicates if includes filters subscribers - * @return The subscribers collection - */ - public Collection getSubscribers(boolean includeGlobal, boolean includeFilter); - - /** - * Get subscribers associated with predicate filter - * - * @param predicate the predicate used to test with filters used for subscription - * @param includeGlobal indicates if includes global subscribers - * @return The subscribers collection - */ - public Collection getSubscribers(Predicate predicate, boolean includeGlobal); - - /** - * Check if the given filter has been set - * - * @param filter the filter used for subscription - * @return true if the given filter exists - */ - public boolean hasFilter(F filter); - - /** - * Get filters - * - * @return The filters collection - */ - public Collection getFilters(); - - /** - * Get filters associated with listener - * - * @param listener the subscriber reference - * @return The filters collection - */ - public Collection getFilters(T listener); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.Collection; +import java.util.function.Predicate; + +/** + * Notifier interface with filter + * + * @param the filter + * @param the subscriber + */ +public interface FilteredNotifier extends Notifier { + /** + * Add a subscriber with filter + * + * @param filter the filter used for subscription + * @param listener the subscriber reference + * @throws Exception on subscription failure + */ + public void subscribe(F filter, T listener) throws Exception; + + /** + * Remove a subscriber with filter + * + * @param filter the filter used for subscription + * @param listener the subscriber reference + * @throws Exception if filter or listener not found + */ + public void unsubscribe(F filter, T listener) throws Exception; + + /** + * Check if the given filter has a given subscriber + * + * @param filter the filter used for subscription + * @param listener the subscriber reference + * @return true if the given subscriber exists + */ + public boolean hasSubscriber(F filter, T listener); + + /** + * Get subscribers associated with filter + * + * @param filter the filter used for subscription + * @return The subscribers collection + */ + public Collection getSubscribers(F filter); + + /** + * Get subscribers including global and/or filters + * + * @param includeGlobal indicates if includes global subscribers + * @param includeFilter indicates if includes filters subscribers + * @return The subscribers collection + */ + public Collection getSubscribers(boolean includeGlobal, boolean includeFilter); + + /** + * Get subscribers associated with predicate filter + * + * @param predicate the predicate used to test with filters used for subscription + * @param includeGlobal indicates if includes global subscribers + * @return The subscribers collection + */ + public Collection getSubscribers(Predicate predicate, boolean includeGlobal); + + /** + * Check if the given filter has been set + * + * @param filter the filter used for subscription + * @return true if the given filter exists + */ + public boolean hasFilter(F filter); + + /** + * Get filters + * + * @return The filters collection + */ + public Collection getFilters(); + + /** + * Get filters associated with listener + * + * @param listener the subscriber reference + * @return The filters collection + */ + public Collection getFilters(T listener); +} diff --git a/src/main/java/enedis/lab/util/task/Notifier.java b/src/main/java/enedis/lab/util/task/Notifier.java index f764b14..c80cd3d 100644 --- a/src/main/java/enedis/lab/util/task/Notifier.java +++ b/src/main/java/enedis/lab/util/task/Notifier.java @@ -1,46 +1,46 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.Collection; - -/** - * Notifier interface - * - * @param - */ -public interface Notifier { - /** - * Add a subscriber - * - * @param subscriber - */ - public void subscribe(T subscriber); - - /** - * Remove a subscriber - * - * @param subscriber - */ - public void unsubscribe(T subscriber); - - /** - * Check if the given subscriber has been added - * - * @param subscriber - * @return true if the given subscriber has been added - */ - public boolean hasSubscriber(T subscriber); - - /** - * Get subscribers - * - * @return The subscribers collection - */ - public Collection getSubscribers(); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +import java.util.Collection; + +/** + * Notifier interface + * + * @param + */ +public interface Notifier { + /** + * Add a subscriber + * + * @param subscriber + */ + public void subscribe(T subscriber); + + /** + * Remove a subscriber + * + * @param subscriber + */ + public void unsubscribe(T subscriber); + + /** + * Check if the given subscriber has been added + * + * @param subscriber + * @return true if the given subscriber has been added + */ + public boolean hasSubscriber(T subscriber); + + /** + * Get subscribers + * + * @return The subscribers collection + */ + public Collection getSubscribers(); +} diff --git a/src/main/java/enedis/lab/util/task/Subscriber.java b/src/main/java/enedis/lab/util/task/Subscriber.java index 9e1dd91..fe95060 100644 --- a/src/main/java/enedis/lab/util/task/Subscriber.java +++ b/src/main/java/enedis/lab/util/task/Subscriber.java @@ -1,11 +1,11 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -/** Subscriber interface */ -public interface Subscriber {} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +/** Subscriber interface */ +public interface Subscriber {} diff --git a/src/main/java/enedis/lab/util/task/Task.java b/src/main/java/enedis/lab/util/task/Task.java index 85cd5b8..c0079ad 100644 --- a/src/main/java/enedis/lab/util/task/Task.java +++ b/src/main/java/enedis/lab/util/task/Task.java @@ -1,24 +1,24 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -/** Task interface */ -public interface Task { - /** Start task */ - public void start(); - - /** Stop task */ - public void stop(); - - /** - * Get the task running state - * - * @return true if the task running - */ - public boolean isRunning(); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.util.task; + +/** Task interface */ +public interface Task { + /** Start task */ + public void start(); + + /** Stop task */ + public void stop(); + + /** + * Get the task running state + * + * @return true if the task running + */ + public boolean isRunning(); +} diff --git a/src/main/java/enedis/tic/core/TICCore.java b/src/main/java/enedis/tic/core/TICCore.java index 477776d..bce1d75 100644 --- a/src/main/java/enedis/tic/core/TICCore.java +++ b/src/main/java/enedis/tic/core/TICCore.java @@ -1,90 +1,90 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.util.task.Task; -import java.util.List; - -/** TICCore interface */ -public interface TICCore extends Task { - /** - * Get available TICs - * - * @return The collection of TIC identifiers - */ - public List getAvailableTICs(); - - /** - * Get modems informations - * - * @return The collection of TIC port descriptors - */ - public List getModemsInfo(); - - /** - * Read next frame - * - * @param identifier the TIC identifier - * @return Frame read or null if timed out - * @throws TICCoreException if identifier not found or if any error occurs - */ - public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException; - - /** - * Read next frame - * - * @param identifier the TIC identifier - * @param timeout the read timeout in milliseconds - * @return Frame read or null if timed out - * @throws TICCoreException if identifier not found or if any error occurs - */ - public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException; - - /** - * Add a subscriber - * - * @param subscriber - */ - public void subscribe(TICCoreSubscriber subscriber); - - /** - * Remove a subscriber - * - * @param subscriber - */ - public void unsubscribe(TICCoreSubscriber subscriber); - - /** - * Add a subscriber with identifier - * - * @param identifier the identifier used for subscription - * @param listener the subscriber reference - * @throws Exception on subscription failure - */ - public void subscribe(TICIdentifier identifier, TICCoreSubscriber listener) - throws TICCoreException; - - /** - * Remove a subscriber with identifier - * - * @param identifier the identifier used for subscription - * @param listener the subscriber reference - * @throws Exception if filter or listener not found - */ - public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber listener) - throws TICCoreException; - - /** - * Get TICs identifier associated with a subscriber - * - * @param listener the subscriber reference - * @return The collection of TIC identifiers for the subscriber - */ - public List getIndentifiers(TICCoreSubscriber listener); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.util.task.Task; +import java.util.List; + +/** TICCore interface */ +public interface TICCore extends Task { + /** + * Get available TICs + * + * @return The collection of TIC identifiers + */ + public List getAvailableTICs(); + + /** + * Get modems informations + * + * @return The collection of TIC port descriptors + */ + public List getModemsInfo(); + + /** + * Read next frame + * + * @param identifier the TIC identifier + * @return Frame read or null if timed out + * @throws TICCoreException if identifier not found or if any error occurs + */ + public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException; + + /** + * Read next frame + * + * @param identifier the TIC identifier + * @param timeout the read timeout in milliseconds + * @return Frame read or null if timed out + * @throws TICCoreException if identifier not found or if any error occurs + */ + public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException; + + /** + * Add a subscriber + * + * @param subscriber + */ + public void subscribe(TICCoreSubscriber subscriber); + + /** + * Remove a subscriber + * + * @param subscriber + */ + public void unsubscribe(TICCoreSubscriber subscriber); + + /** + * Add a subscriber with identifier + * + * @param identifier the identifier used for subscription + * @param listener the subscriber reference + * @throws Exception on subscription failure + */ + public void subscribe(TICIdentifier identifier, TICCoreSubscriber listener) + throws TICCoreException; + + /** + * Remove a subscriber with identifier + * + * @param identifier the identifier used for subscription + * @param listener the subscriber reference + * @throws Exception if filter or listener not found + */ + public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber listener) + throws TICCoreException; + + /** + * Get TICs identifier associated with a subscriber + * + * @param listener the subscriber reference + * @return The collection of TIC identifiers for the subscriber + */ + public List getIndentifiers(TICCoreSubscriber listener); +} diff --git a/src/main/java/enedis/tic/core/TICCoreErrorCode.java b/src/main/java/enedis/tic/core/TICCoreErrorCode.java index b157360..e6474ba 100644 --- a/src/main/java/enedis/tic/core/TICCoreErrorCode.java +++ b/src/main/java/enedis/tic/core/TICCoreErrorCode.java @@ -1,30 +1,30 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -public enum TICCoreErrorCode { - NO_ERROR(0), - STREAM_PORT_ID_NOT_FOUND(1), - STREAM_PORT_NAME_NOT_FOUND(2), - STREAM_PORT_DESCRIPTOR_EMPTY(3), - STREAM_MODE_NOT_DEFINED(4), - STREAM_IDENTIFIER_NOT_FOUND(5), - STREAM_UNPLUGGED(6), - DATA_READ_TIMEOUT(7), - OTHER_REASON(99); - - private int code; - - private TICCoreErrorCode(int code) { - this.code = code; - } - - public int getCode() { - return this.code; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +public enum TICCoreErrorCode { + NO_ERROR(0), + STREAM_PORT_ID_NOT_FOUND(1), + STREAM_PORT_NAME_NOT_FOUND(2), + STREAM_PORT_DESCRIPTOR_EMPTY(3), + STREAM_MODE_NOT_DEFINED(4), + STREAM_IDENTIFIER_NOT_FOUND(5), + STREAM_UNPLUGGED(6), + DATA_READ_TIMEOUT(7), + OTHER_REASON(99); + + private int code; + + private TICCoreErrorCode(int code) { + this.code = code; + } + + public int getCode() { + return this.code; + } +} diff --git a/src/main/java/enedis/tic/core/TICCoreStream.java b/src/main/java/enedis/tic/core/TICCoreStream.java index 33e509e..31c2b98 100644 --- a/src/main/java/enedis/tic/core/TICCoreStream.java +++ b/src/main/java/enedis/tic/core/TICCoreStream.java @@ -1,21 +1,21 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.util.task.Notifier; -import enedis.lab.util.task.Task; - -/** TICCore stream interface */ -public interface TICCoreStream extends Notifier, Task { - /** - * Get identifier - * - * @return The TICIdentifier associated with the stream - */ - public TICIdentifier getIdentifier(); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.util.task.Notifier; +import enedis.lab.util.task.Task; + +/** TICCore stream interface */ +public interface TICCoreStream extends Notifier, Task { + /** + * Get identifier + * + * @return The TICIdentifier associated with the stream + */ + public TICIdentifier getIdentifier(); +} diff --git a/src/main/java/enedis/tic/core/TICCoreSubscriber.java b/src/main/java/enedis/tic/core/TICCoreSubscriber.java index 3f550e4..4dd799f 100644 --- a/src/main/java/enedis/tic/core/TICCoreSubscriber.java +++ b/src/main/java/enedis/tic/core/TICCoreSubscriber.java @@ -1,27 +1,27 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.util.task.Subscriber; - -/** TICCore subscriber interface */ -public interface TICCoreSubscriber extends Subscriber { - /** - * Notify when data - * - * @param frame the frame received - */ - public void onData(TICCoreFrame frame); - - /** - * Notify when error - * - * @param error the error detected - */ - public void onError(TICCoreError error); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.util.task.Subscriber; + +/** TICCore subscriber interface */ +public interface TICCoreSubscriber extends Subscriber { + /** + * Notify when data + * + * @param frame the frame received + */ + public void onData(TICCoreFrame frame); + + /** + * Notify when error + * + * @param error the error detected + */ + public void onError(TICCoreError error); +} diff --git a/src/main/java/enedis/tic/service/endpoint/EventSender.java b/src/main/java/enedis/tic/service/endpoint/EventSender.java index 37b90d7..6e10ed8 100644 --- a/src/main/java/enedis/tic/service/endpoint/EventSender.java +++ b/src/main/java/enedis/tic/service/endpoint/EventSender.java @@ -1,22 +1,22 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.endpoint; - -import enedis.lab.util.message.Event; -import io.netty.channel.Channel; - -/** Event sender interface */ -public interface EventSender { - /** - * Send event - * - * @param channel - * @param event - */ - public void sendEvent(Channel channel, Event event); -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.service.endpoint; + +import enedis.lab.util.message.Event; +import io.netty.channel.Channel; + +/** Event sender interface */ +public interface EventSender { + /** + * Send event + * + * @param channel + * @param event + */ + public void sendEvent(Channel channel, Event event); +} diff --git a/src/test/java/enedis/lab/io/PortFinderMock.java b/src/test/java/enedis/lab/io/PortFinderMock.java index ce6e983..d1421ec 100644 --- a/src/test/java/enedis/lab/io/PortFinderMock.java +++ b/src/test/java/enedis/lab/io/PortFinderMock.java @@ -1,61 +1,61 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io; - -import enedis.lab.mock.FunctionCall; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; -import java.util.ArrayList; -import java.util.List; - -@SuppressWarnings("javadoc") -public class PortFinderMock implements PortFinder { - public List findAllCalls = new ArrayList(); - public DataList descriptorList = new DataArrayList(); - - public PortFinderMock() { - this.descriptorList = new DataArrayList(); - } - - @Override - public DataList findAll() { - this.findAllCalls.add(new FunctionCall()); - return this.getDescriptors(); - } - - public DataList getDescriptors() { - DataList descriptors = new DataArrayList(); - - synchronized (this.descriptorList) { - descriptors.addAll(this.descriptorList); - } - - return descriptors; - } - - public DataList setDescriptors(DataList descriptors) { - synchronized (this.descriptorList) { - this.descriptorList.clear(); - this.descriptorList.addAll(descriptors); - } - - return descriptors; - } - - public void addDescriptor(T descriptor) { - synchronized (this.descriptorList) { - this.descriptorList.add(descriptor); - } - } - - public void removeDescriptor(T descriptor) { - synchronized (this.descriptorList) { - this.descriptorList.remove(descriptor); - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.io; + +import enedis.lab.mock.FunctionCall; +import enedis.lab.types.DataArrayList; +import enedis.lab.types.DataList; +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("javadoc") +public class PortFinderMock implements PortFinder { + public List findAllCalls = new ArrayList(); + public DataList descriptorList = new DataArrayList(); + + public PortFinderMock() { + this.descriptorList = new DataArrayList(); + } + + @Override + public DataList findAll() { + this.findAllCalls.add(new FunctionCall()); + return this.getDescriptors(); + } + + public DataList getDescriptors() { + DataList descriptors = new DataArrayList(); + + synchronized (this.descriptorList) { + descriptors.addAll(this.descriptorList); + } + + return descriptors; + } + + public DataList setDescriptors(DataList descriptors) { + synchronized (this.descriptorList) { + this.descriptorList.clear(); + this.descriptorList.addAll(descriptors); + } + + return descriptors; + } + + public void addDescriptor(T descriptor) { + synchronized (this.descriptorList) { + this.descriptorList.add(descriptor); + } + } + + public void removeDescriptor(T descriptor) { + synchronized (this.descriptorList) { + this.descriptorList.remove(descriptor); + } + } +} diff --git a/src/test/java/enedis/lab/mock/FunctionCall.java b/src/test/java/enedis/lab/mock/FunctionCall.java index 13b4371..d26b531 100644 --- a/src/test/java/enedis/lab/mock/FunctionCall.java +++ b/src/test/java/enedis/lab/mock/FunctionCall.java @@ -1,23 +1,23 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.mock; - -@SuppressWarnings("javadoc") -public class FunctionCall { - public long captureTime; - - public FunctionCall() { - super(); - this.captureTime = System.nanoTime(); - } - - public FunctionCall(long captureTime) { - super(); - this.captureTime = captureTime; - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.lab.mock; + +@SuppressWarnings("javadoc") +public class FunctionCall { + public long captureTime; + + public FunctionCall() { + super(); + this.captureTime = System.nanoTime(); + } + + public FunctionCall(long captureTime) { + super(); + this.captureTime = captureTime; + } +} diff --git a/src/test/java/enedis/tic/core/TICCoreBaseTest.java b/src/test/java/enedis/tic/core/TICCoreBaseTest.java index 9d83c1a..c8a6345 100644 --- a/src/test/java/enedis/tic/core/TICCoreBaseTest.java +++ b/src/test/java/enedis/tic/core/TICCoreBaseTest.java @@ -1,542 +1,542 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.io.tic.TICModemType; -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.io.tic.TICPortFinderMock; -import enedis.lab.mock.FunctionCall; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.util.task.Task; -import enedis.lab.util.time.Time; -import java.time.LocalDateTime; -import java.util.List; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -@SuppressWarnings("javadoc") -public class TICCoreBaseTest { - private static final int NOTIFICATION_TIMEOUT = 1000; - - private TICPortFinderMock ticPortFinder; - private long plugNotifierPeriod; - private TICCoreBase ticCore; - - @Before - public void startTICCore() throws TICCoreException { - this.ticPortFinder = new TICPortFinderMock(); - this.plugNotifierPeriod = 50; - this.ticCore = - new TICCoreBase( - this.ticPortFinder, - this.plugNotifierPeriod, - TICCoreStreamMock.class, - TICMode.AUTO, - null); - - this.ticCore.start(); - this.waitTaskRunning(this.ticCore); - } - - @After - public void stopTICCore() throws TICCoreException { - this.ticCore.stop(); - TICCoreStreamMock.streams.clear(); - } - - @Test - public void test_getAvailableTICs() throws TICCoreException, DataDictionaryException { - List availableTICs = this.ticCore.getAvailableTICs(); - Assert.assertNotNull(availableTICs); - Assert.assertEquals(0, availableTICs.size()); - - TICPortDescriptor descriptor1 = - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); - this.plugModem(descriptor1); - this.plugModem(descriptor2); - - availableTICs = this.ticCore.getAvailableTICs(); - Assert.assertNotNull(availableTICs); - Assert.assertEquals(2, availableTICs.size()); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("2", "COM4", null))); - - this.unplugModem(descriptor2); - - availableTICs = this.ticCore.getAvailableTICs(); - Assert.assertNotNull(availableTICs); - Assert.assertEquals(1, availableTICs.size()); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); - } - - @Test - public void test_getModemsInfo() throws TICCoreException, DataDictionaryException { - List modemsInfo = this.ticCore.getModemsInfo(); - Assert.assertNotNull(modemsInfo); - Assert.assertEquals(0, modemsInfo.size()); - - TICPortDescriptor descriptor1 = - new TICPortDescriptor(null, "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = - new TICPortDescriptor(null, "COM4", null, null, null, null, TICModemType.TELEINFO); - this.plugModem(descriptor1); - this.plugModem(descriptor2); - - modemsInfo = this.ticCore.getModemsInfo(); - Assert.assertNotNull(modemsInfo); - Assert.assertEquals(2, modemsInfo.size()); - Assert.assertTrue(modemsInfo.contains(descriptor1)); - Assert.assertTrue(modemsInfo.contains(descriptor2)); - - this.unplugModem(descriptor2); - modemsInfo = this.ticCore.getModemsInfo(); - Assert.assertNotNull(modemsInfo); - Assert.assertEquals(1, modemsInfo.size()); - Assert.assertTrue(modemsInfo.contains(descriptor1)); - } - - @Test - public void test_readNextFrame_ok() throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); - task.start(); - this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); - TICCoreFrame frame = - this.createFrame(identifier, TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); - stream.notifyOnData(frame); - this.waitTaskTerminated(task); - Assert.assertNull(task.exception); - Assert.assertNotNull(task.frame); - Assert.assertTrue(task.frame == frame); - } - - @Test - public void test_readNextFrame_error_OTHER_REASON() - throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); - task.start(); - this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); - TICCoreError error = - new TICCoreError(identifier, TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); - stream.notifyOnError(error); - this.waitTaskTerminated(task); - Assert.assertNull(task.frame); - Assert.assertNotNull(task.exception); - Assert.assertTrue(task.exception instanceof TICCoreException); - TICCoreException exception = (TICCoreException) task.exception; - Assert.assertEquals(error.getErrorCode(), exception.getErrorCode()); - Assert.assertEquals(error.getErrorMessage(), exception.getErrorInfo()); - } - - @Test - public void test_readNextFrame_error_STREAM_IDENTIFIER_NOT_FOUND() - throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = - new TICCoreReadNextFrameTask(this.ticCore, new TICIdentifier("2", "COM3", null)); - task.start(); - this.waitTaskRunning(task); - this.waitTaskTerminated(task); - Assert.assertNull(task.frame); - Assert.assertNotNull(task.exception); - Assert.assertTrue(task.exception instanceof TICCoreException); - TICCoreException exception = (TICCoreException) task.exception; - Assert.assertEquals( - TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); - } - - @Test - public void test_readNextFrame_timeout() throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier, 200); - task.start(); - this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); - this.waitTaskTerminated(task); - Assert.assertNull(task.frame); - Assert.assertNotNull(task.exception); - Assert.assertTrue(task.exception instanceof TICCoreException); - TICCoreException exception = (TICCoreException) task.exception; - Assert.assertEquals(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), exception.getErrorCode()); - } - - @Test - public void test_subscribe_any() throws TICCoreException, DataDictionaryException { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - } - - @Test - public void test_unsubscribe_any() throws TICCoreException, DataDictionaryException { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - this.ticCore.unsubscribe(subscriber); - } - - @Test - public void test_subscribe_withIdentifier_ok() throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - } - - @Test - public void test_unsubscribe_withIdentifier_ok() - throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - this.ticCore.unsubscribe(identifier, subscriber); - } - - @Test - public void test_subscribe_withIdentifier_notFound() - throws TICCoreException, DataDictionaryException { - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - TICIdentifier identifier = new TICIdentifier(null, "COM4", null); - try { - this.ticCore.subscribe(identifier, subscriber); - Assert.fail( - "Subscribe should have trhown an exception since stream identifier does not match !"); - } catch (Exception e) { - Assert.assertTrue(e instanceof TICCoreException); - TICCoreException exception = (TICCoreException) e; - Assert.assertEquals( - TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); - } - } - - @Test - public void test_onError_whenUnplugModem() throws TICCoreException, DataDictionaryException { - TICPortDescriptor descriptor1 = - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); - TICIdentifier identifier1 = this.plugModem(descriptor1); - TICIdentifier identifier2 = this.plugModem(descriptor2); - - TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber4 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber5 = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber1); - this.ticCore.subscribe(identifier1, subscriber2); - this.ticCore.subscribe(identifier1, subscriber3); - this.ticCore.subscribe(identifier2, subscriber4); - this.ticCore.subscribe(identifier2, subscriber5); - - this.unplugModem(descriptor1); - this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); - Assert.assertEquals(1, subscriber1.onErrorCalls.size()); - Assert.assertEquals( - TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), - subscriber1.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber2.onErrorCalls, 1); - Assert.assertEquals(1, subscriber2.onErrorCalls.size()); - Assert.assertEquals( - TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), - subscriber2.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber3.onErrorCalls, 1); - Assert.assertEquals(1, subscriber3.onErrorCalls.size()); - Assert.assertEquals( - TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), - subscriber3.onErrorCalls.get(0).error.getErrorCode()); - Assert.assertEquals(0, subscriber4.onErrorCalls.size()); - Assert.assertEquals(0, subscriber5.onErrorCalls.size()); - - this.unplugModem(descriptor2); - this.waitSubscriberNotification(subscriber4.onErrorCalls, 1); - Assert.assertEquals(1, subscriber4.onErrorCalls.size()); - Assert.assertEquals( - TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), - subscriber4.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber5.onErrorCalls, 1); - Assert.assertEquals(1, subscriber5.onErrorCalls.size()); - Assert.assertEquals( - TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), - subscriber5.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); - Assert.assertEquals(2, subscriber1.onErrorCalls.size()); - Assert.assertEquals( - TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), - subscriber1.onErrorCalls.get(1).error.getErrorCode()); - Assert.assertEquals(1, subscriber2.onErrorCalls.size()); - Assert.assertEquals(1, subscriber3.onErrorCalls.size()); - } - - @Test - public void test_onData_any() throws TICCoreException, DataDictionaryException { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreFrame frame1 = - this.createFrame( - stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); - stream1.notifyOnData(frame1); - this.waitSubscriberNotification(subscriber.onDataCalls, 1); - Assert.assertEquals(1, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - - this.plugModem( - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreFrame frame2 = - this.createFrame( - stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); - stream2.notifyOnData(frame2); - this.waitSubscriberNotification(subscriber.onDataCalls, 2); - Assert.assertEquals(2, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - Assert.assertTrue(frame2 == subscriber.onDataCalls.get(1).frame); - } - - @Test - public void test_onData_withIdentifier() throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreFrame frame1 = - this.createFrame( - stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); - stream1.notifyOnData(frame1); - this.waitSubscriberNotification(subscriber.onDataCalls, 1); - Assert.assertEquals(1, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - - this.plugModem( - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.MICHAUD)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreFrame frame2 = - this.createFrame( - stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); - stream2.notifyOnData(frame2); - this.waitSubscriberNotification(subscriber.onDataCalls, 1); - Assert.assertEquals(1, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - } - - @Test - public void test_onError_any() throws TICCoreException, DataDictionaryException { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreError error1 = - new TICCoreError( - stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); - stream1.notifyOnError(error1); - this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - - this.plugModem( - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreError error2 = - new TICCoreError( - stream2.getIdentifier(), - TICCoreErrorCode.OTHER_REASON.getCode(), - "Error reading stream COM4"); - stream2.notifyOnError(error2); - this.waitSubscriberNotification(subscriber.onErrorCalls, 2); - Assert.assertEquals(2, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - Assert.assertTrue(error2 == subscriber.onErrorCalls.get(1).error); - } - - @Test - public void test_onError_withIdentifier() throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreError error1 = - new TICCoreError( - stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); - stream1.notifyOnError(error1); - this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - - this.plugModem( - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreError error2 = - new TICCoreError( - stream2.getIdentifier(), - TICCoreErrorCode.OTHER_REASON.getCode(), - "Error reading stream COM4"); - stream2.notifyOnError(error2); - this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - } - - @Test - public void test_getIdentifiers() throws TICCoreException, DataDictionaryException { - TICIdentifier identifier1 = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - TICIdentifier identifier2 = - this.plugModem( - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - - TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); - - this.ticCore.subscribe(subscriber1); - List indentifierList1 = this.ticCore.getIndentifiers(subscriber1); - Assert.assertNotNull(indentifierList1); - Assert.assertEquals(2, indentifierList1.size()); - Assert.assertTrue(indentifierList1.contains(identifier1)); - Assert.assertTrue(indentifierList1.contains(identifier2)); - - this.ticCore.subscribe(identifier1, subscriber2); - List indentifierList2 = this.ticCore.getIndentifiers(subscriber2); - Assert.assertNotNull(indentifierList2); - Assert.assertEquals(1, indentifierList2.size()); - Assert.assertTrue(indentifierList2.contains(identifier1)); - - this.ticCore.subscribe(identifier2, subscriber3); - List indentifierList3 = this.ticCore.getIndentifiers(subscriber3); - Assert.assertNotNull(indentifierList3); - Assert.assertEquals(1, indentifierList3.size()); - Assert.assertTrue(indentifierList3.contains(identifier2)); - - this.ticCore.unsubscribe(subscriber1); - indentifierList1 = this.ticCore.getIndentifiers(subscriber1); - Assert.assertNotNull(indentifierList1); - Assert.assertEquals(0, indentifierList1.size()); - - this.ticCore.unsubscribe(identifier1, subscriber2); - indentifierList2 = this.ticCore.getIndentifiers(subscriber2); - Assert.assertNotNull(indentifierList2); - Assert.assertEquals(0, indentifierList2.size()); - - this.ticCore.unsubscribe(identifier2, subscriber3); - indentifierList3 = this.ticCore.getIndentifiers(subscriber3); - Assert.assertNotNull(indentifierList3); - Assert.assertEquals(0, indentifierList3.size()); - } - - private TICIdentifier plugModem(TICPortDescriptor descriptor) throws DataDictionaryException { - this.ticPortFinder.addDescriptor(descriptor); - this.waitPlugNotifierUpdate(); - - return new TICIdentifier(descriptor.getPortId(), descriptor.getPortName(), null); - } - - private void unplugModem(TICPortDescriptor descriptor) { - this.ticPortFinder.removeDescriptor(descriptor); - this.waitPlugNotifierUpdate(); - } - - private TICCoreFrame createFrame( - TICIdentifier identifier, TICMode mode, LocalDateTime localDateTime) - throws DataDictionaryException { - DataDictionaryBase content = new DataDictionaryBase(); - if (mode == TICMode.STANDARD) { - content.set("ADSC", identifier.getSerialNumber()); - } else { - content.set("ADCO", identifier.getSerialNumber()); - } - TICCoreFrame frame = new TICCoreFrame(identifier, mode, localDateTime, content); - - return frame; - } - - private void waitPlugNotifierUpdate() { - Time.sleep(2 * this.plugNotifierPeriod); - } - - private void waitTaskRunning(Task task) { - while (!task.isRunning()) { - Time.sleep(50); - } - } - - private void waitTaskTerminated(Task task) { - while (task.isRunning()) { - Time.sleep(50); - } - } - - private void waitReadNextFrameSubsbription() { - Time.sleep(100); - } - - private void waitSubscriberNotification(List onCalls, int expectedSize) { - long begin = System.nanoTime(); - long elapsed = 0; - while (onCalls.size() < expectedSize && elapsed < NOTIFICATION_TIMEOUT) { - Time.sleep(50); - elapsed = (System.nanoTime() - begin) / 1000000; - } - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.io.tic.TICModemType; +import enedis.lab.io.tic.TICPortDescriptor; +import enedis.lab.io.tic.TICPortFinderMock; +import enedis.lab.mock.FunctionCall; +import enedis.lab.protocol.tic.TICMode; +import enedis.lab.types.DataDictionaryException; +import enedis.lab.types.datadictionary.DataDictionaryBase; +import enedis.lab.util.task.Task; +import enedis.lab.util.time.Time; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +@SuppressWarnings("javadoc") +public class TICCoreBaseTest { + private static final int NOTIFICATION_TIMEOUT = 1000; + + private TICPortFinderMock ticPortFinder; + private long plugNotifierPeriod; + private TICCoreBase ticCore; + + @Before + public void startTICCore() throws TICCoreException { + this.ticPortFinder = new TICPortFinderMock(); + this.plugNotifierPeriod = 50; + this.ticCore = + new TICCoreBase( + this.ticPortFinder, + this.plugNotifierPeriod, + TICCoreStreamMock.class, + TICMode.AUTO, + null); + + this.ticCore.start(); + this.waitTaskRunning(this.ticCore); + } + + @After + public void stopTICCore() throws TICCoreException { + this.ticCore.stop(); + TICCoreStreamMock.streams.clear(); + } + + @Test + public void test_getAvailableTICs() throws TICCoreException, DataDictionaryException { + List availableTICs = this.ticCore.getAvailableTICs(); + Assert.assertNotNull(availableTICs); + Assert.assertEquals(0, availableTICs.size()); + + TICPortDescriptor descriptor1 = + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); + TICPortDescriptor descriptor2 = + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); + this.plugModem(descriptor1); + this.plugModem(descriptor2); + + availableTICs = this.ticCore.getAvailableTICs(); + Assert.assertNotNull(availableTICs); + Assert.assertEquals(2, availableTICs.size()); + Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); + Assert.assertTrue(availableTICs.contains(new TICIdentifier("2", "COM4", null))); + + this.unplugModem(descriptor2); + + availableTICs = this.ticCore.getAvailableTICs(); + Assert.assertNotNull(availableTICs); + Assert.assertEquals(1, availableTICs.size()); + Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); + } + + @Test + public void test_getModemsInfo() throws TICCoreException, DataDictionaryException { + List modemsInfo = this.ticCore.getModemsInfo(); + Assert.assertNotNull(modemsInfo); + Assert.assertEquals(0, modemsInfo.size()); + + TICPortDescriptor descriptor1 = + new TICPortDescriptor(null, "COM3", null, null, null, null, TICModemType.MICHAUD); + TICPortDescriptor descriptor2 = + new TICPortDescriptor(null, "COM4", null, null, null, null, TICModemType.TELEINFO); + this.plugModem(descriptor1); + this.plugModem(descriptor2); + + modemsInfo = this.ticCore.getModemsInfo(); + Assert.assertNotNull(modemsInfo); + Assert.assertEquals(2, modemsInfo.size()); + Assert.assertTrue(modemsInfo.contains(descriptor1)); + Assert.assertTrue(modemsInfo.contains(descriptor2)); + + this.unplugModem(descriptor2); + modemsInfo = this.ticCore.getModemsInfo(); + Assert.assertNotNull(modemsInfo); + Assert.assertEquals(1, modemsInfo.size()); + Assert.assertTrue(modemsInfo.contains(descriptor1)); + } + + @Test + public void test_readNextFrame_ok() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); + task.start(); + this.waitTaskRunning(task); + this.waitReadNextFrameSubsbription(); + TICCoreFrame frame = + this.createFrame(identifier, TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + stream.notifyOnData(frame); + this.waitTaskTerminated(task); + Assert.assertNull(task.exception); + Assert.assertNotNull(task.frame); + Assert.assertTrue(task.frame == frame); + } + + @Test + public void test_readNextFrame_error_OTHER_REASON() + throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); + task.start(); + this.waitTaskRunning(task); + this.waitReadNextFrameSubsbription(); + TICCoreError error = + new TICCoreError(identifier, TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + stream.notifyOnError(error); + this.waitTaskTerminated(task); + Assert.assertNull(task.frame); + Assert.assertNotNull(task.exception); + Assert.assertTrue(task.exception instanceof TICCoreException); + TICCoreException exception = (TICCoreException) task.exception; + Assert.assertEquals(error.getErrorCode(), exception.getErrorCode()); + Assert.assertEquals(error.getErrorMessage(), exception.getErrorInfo()); + } + + @Test + public void test_readNextFrame_error_STREAM_IDENTIFIER_NOT_FOUND() + throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = + new TICCoreReadNextFrameTask(this.ticCore, new TICIdentifier("2", "COM3", null)); + task.start(); + this.waitTaskRunning(task); + this.waitTaskTerminated(task); + Assert.assertNull(task.frame); + Assert.assertNotNull(task.exception); + Assert.assertTrue(task.exception instanceof TICCoreException); + TICCoreException exception = (TICCoreException) task.exception; + Assert.assertEquals( + TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); + } + + @Test + public void test_readNextFrame_timeout() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); + Assert.assertEquals(identifier, stream.getIdentifier()); + + TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier, 200); + task.start(); + this.waitTaskRunning(task); + this.waitReadNextFrameSubsbription(); + this.waitTaskTerminated(task); + Assert.assertNull(task.frame); + Assert.assertNotNull(task.exception); + Assert.assertTrue(task.exception instanceof TICCoreException); + TICCoreException exception = (TICCoreException) task.exception; + Assert.assertEquals(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), exception.getErrorCode()); + } + + @Test + public void test_subscribe_any() throws TICCoreException, DataDictionaryException { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + } + + @Test + public void test_unsubscribe_any() throws TICCoreException, DataDictionaryException { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + this.ticCore.unsubscribe(subscriber); + } + + @Test + public void test_subscribe_withIdentifier_ok() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + } + + @Test + public void test_unsubscribe_withIdentifier_ok() + throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + this.ticCore.unsubscribe(identifier, subscriber); + } + + @Test + public void test_subscribe_withIdentifier_notFound() + throws TICCoreException, DataDictionaryException { + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + TICIdentifier identifier = new TICIdentifier(null, "COM4", null); + try { + this.ticCore.subscribe(identifier, subscriber); + Assert.fail( + "Subscribe should have trhown an exception since stream identifier does not match !"); + } catch (Exception e) { + Assert.assertTrue(e instanceof TICCoreException); + TICCoreException exception = (TICCoreException) e; + Assert.assertEquals( + TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); + } + } + + @Test + public void test_onError_whenUnplugModem() throws TICCoreException, DataDictionaryException { + TICPortDescriptor descriptor1 = + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); + TICPortDescriptor descriptor2 = + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); + TICIdentifier identifier1 = this.plugModem(descriptor1); + TICIdentifier identifier2 = this.plugModem(descriptor2); + + TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber4 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber5 = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber1); + this.ticCore.subscribe(identifier1, subscriber2); + this.ticCore.subscribe(identifier1, subscriber3); + this.ticCore.subscribe(identifier2, subscriber4); + this.ticCore.subscribe(identifier2, subscriber5); + + this.unplugModem(descriptor1); + this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); + Assert.assertEquals(1, subscriber1.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber1.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber2.onErrorCalls, 1); + Assert.assertEquals(1, subscriber2.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber2.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber3.onErrorCalls, 1); + Assert.assertEquals(1, subscriber3.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber3.onErrorCalls.get(0).error.getErrorCode()); + Assert.assertEquals(0, subscriber4.onErrorCalls.size()); + Assert.assertEquals(0, subscriber5.onErrorCalls.size()); + + this.unplugModem(descriptor2); + this.waitSubscriberNotification(subscriber4.onErrorCalls, 1); + Assert.assertEquals(1, subscriber4.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber4.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber5.onErrorCalls, 1); + Assert.assertEquals(1, subscriber5.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber5.onErrorCalls.get(0).error.getErrorCode()); + this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); + Assert.assertEquals(2, subscriber1.onErrorCalls.size()); + Assert.assertEquals( + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + subscriber1.onErrorCalls.get(1).error.getErrorCode()); + Assert.assertEquals(1, subscriber2.onErrorCalls.size()); + Assert.assertEquals(1, subscriber3.onErrorCalls.size()); + } + + @Test + public void test_onData_any() throws TICCoreException, DataDictionaryException { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreFrame frame1 = + this.createFrame( + stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + stream1.notifyOnData(frame1); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); + Assert.assertEquals(1, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + + this.plugModem( + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreFrame frame2 = + this.createFrame( + stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); + stream2.notifyOnData(frame2); + this.waitSubscriberNotification(subscriber.onDataCalls, 2); + Assert.assertEquals(2, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + Assert.assertTrue(frame2 == subscriber.onDataCalls.get(1).frame); + } + + @Test + public void test_onData_withIdentifier() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreFrame frame1 = + this.createFrame( + stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + stream1.notifyOnData(frame1); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); + Assert.assertEquals(1, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + + this.plugModem( + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.MICHAUD)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreFrame frame2 = + this.createFrame( + stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); + stream2.notifyOnData(frame2); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); + Assert.assertEquals(1, subscriber.onDataCalls.size()); + Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + } + + @Test + public void test_onError_any() throws TICCoreException, DataDictionaryException { + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); + + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreError error1 = + new TICCoreError( + stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + stream1.notifyOnError(error1); + this.waitSubscriberNotification(subscriber.onErrorCalls, 1); + Assert.assertEquals(1, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + + this.plugModem( + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreError error2 = + new TICCoreError( + stream2.getIdentifier(), + TICCoreErrorCode.OTHER_REASON.getCode(), + "Error reading stream COM4"); + stream2.notifyOnError(error2); + this.waitSubscriberNotification(subscriber.onErrorCalls, 2); + Assert.assertEquals(2, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + Assert.assertTrue(error2 == subscriber.onErrorCalls.get(1).error); + } + + @Test + public void test_onError_withIdentifier() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + + Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); + TICCoreError error1 = + new TICCoreError( + stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + stream1.notifyOnError(error1); + this.waitSubscriberNotification(subscriber.onErrorCalls, 1); + Assert.assertEquals(1, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + + this.plugModem( + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreError error2 = + new TICCoreError( + stream2.getIdentifier(), + TICCoreErrorCode.OTHER_REASON.getCode(), + "Error reading stream COM4"); + stream2.notifyOnError(error2); + this.waitSubscriberNotification(subscriber.onErrorCalls, 1); + Assert.assertEquals(1, subscriber.onErrorCalls.size()); + Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); + } + + @Test + public void test_getIdentifiers() throws TICCoreException, DataDictionaryException { + TICIdentifier identifier1 = + this.plugModem( + new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + TICIdentifier identifier2 = + this.plugModem( + new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + + TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); + TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); + + this.ticCore.subscribe(subscriber1); + List indentifierList1 = this.ticCore.getIndentifiers(subscriber1); + Assert.assertNotNull(indentifierList1); + Assert.assertEquals(2, indentifierList1.size()); + Assert.assertTrue(indentifierList1.contains(identifier1)); + Assert.assertTrue(indentifierList1.contains(identifier2)); + + this.ticCore.subscribe(identifier1, subscriber2); + List indentifierList2 = this.ticCore.getIndentifiers(subscriber2); + Assert.assertNotNull(indentifierList2); + Assert.assertEquals(1, indentifierList2.size()); + Assert.assertTrue(indentifierList2.contains(identifier1)); + + this.ticCore.subscribe(identifier2, subscriber3); + List indentifierList3 = this.ticCore.getIndentifiers(subscriber3); + Assert.assertNotNull(indentifierList3); + Assert.assertEquals(1, indentifierList3.size()); + Assert.assertTrue(indentifierList3.contains(identifier2)); + + this.ticCore.unsubscribe(subscriber1); + indentifierList1 = this.ticCore.getIndentifiers(subscriber1); + Assert.assertNotNull(indentifierList1); + Assert.assertEquals(0, indentifierList1.size()); + + this.ticCore.unsubscribe(identifier1, subscriber2); + indentifierList2 = this.ticCore.getIndentifiers(subscriber2); + Assert.assertNotNull(indentifierList2); + Assert.assertEquals(0, indentifierList2.size()); + + this.ticCore.unsubscribe(identifier2, subscriber3); + indentifierList3 = this.ticCore.getIndentifiers(subscriber3); + Assert.assertNotNull(indentifierList3); + Assert.assertEquals(0, indentifierList3.size()); + } + + private TICIdentifier plugModem(TICPortDescriptor descriptor) throws DataDictionaryException { + this.ticPortFinder.addDescriptor(descriptor); + this.waitPlugNotifierUpdate(); + + return new TICIdentifier(descriptor.getPortId(), descriptor.getPortName(), null); + } + + private void unplugModem(TICPortDescriptor descriptor) { + this.ticPortFinder.removeDescriptor(descriptor); + this.waitPlugNotifierUpdate(); + } + + private TICCoreFrame createFrame( + TICIdentifier identifier, TICMode mode, LocalDateTime localDateTime) + throws DataDictionaryException { + DataDictionaryBase content = new DataDictionaryBase(); + if (mode == TICMode.STANDARD) { + content.set("ADSC", identifier.getSerialNumber()); + } else { + content.set("ADCO", identifier.getSerialNumber()); + } + TICCoreFrame frame = new TICCoreFrame(identifier, mode, localDateTime, content); + + return frame; + } + + private void waitPlugNotifierUpdate() { + Time.sleep(2 * this.plugNotifierPeriod); + } + + private void waitTaskRunning(Task task) { + while (!task.isRunning()) { + Time.sleep(50); + } + } + + private void waitTaskTerminated(Task task) { + while (task.isRunning()) { + Time.sleep(50); + } + } + + private void waitReadNextFrameSubsbription() { + Time.sleep(100); + } + + private void waitSubscriberNotification(List onCalls, int expectedSize) { + long begin = System.nanoTime(); + long elapsed = 0; + while (onCalls.size() < expectedSize && elapsed < NOTIFICATION_TIMEOUT) { + Time.sleep(50); + elapsed = (System.nanoTime() - begin) / 1000000; + } + } +} diff --git a/src/test/java/enedis/tic/core/TICIdentifierTest.java b/src/test/java/enedis/tic/core/TICIdentifierTest.java index 6f7d699..a601433 100644 --- a/src/test/java/enedis/tic/core/TICIdentifierTest.java +++ b/src/test/java/enedis/tic/core/TICIdentifierTest.java @@ -1,167 +1,167 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.types.DataDictionaryException; -import org.junit.Assert; -import org.junit.Test; - -public class TICIdentifierTest { - @Test - public void test_constructor_full() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier("{1-1}", "COM3", "021976551632"); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - Assert.assertEquals("COM3", identifier.getPortName()); - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlyPortId() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - Assert.assertNull(identifier.getPortName()); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlyPortName() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, "COM3", null); - - Assert.assertNull(identifier.getPortId()); - Assert.assertEquals("COM3", identifier.getPortName()); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlySerialNumber() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertNull(identifier.getPortId()); - Assert.assertNull(identifier.getPortName()); - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - } - - @Test(expected = DataDictionaryException.class) - public void test_constructor_empty() throws DataDictionaryException { - new TICIdentifier(null, null, null); - } - - @Test - public void test_setPortId_null_notEmpty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, "021976551632"); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - identifier.setPortId(null); - Assert.assertNull(identifier.getPortId()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setPortId_null_empty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - identifier.setPortId(null); - } - - @Test - public void test_setPortName_null_notEmpty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); - - Assert.assertEquals("COM3", identifier.getPortName()); - identifier.setPortName(null); - Assert.assertNull(identifier.getPortName()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setPortName_null_empty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, "COM3", null); - - Assert.assertEquals("COM3", identifier.getPortName()); - identifier.setPortName(null); - } - - @Test - public void test_setSerialNumber_null_notEmpty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); - - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - identifier.setSerialNumber(null); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setSerialNumber_null_empty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - identifier.setSerialNumber(null); - } - - @Test - public void test_matches_null() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertFalse(identifier.matches(null)); - } - - @Test - public void test_matches_same_serialNumber() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM4", "021976551632"); - - Assert.assertTrue(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_serialNumber() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM3", "021976551638"); - - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_same_portId() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM4", "021976551633"); - TICIdentifier identifier3 = new TICIdentifier("{1-1}", "COM5", null); - - Assert.assertTrue(identifier1.matches(identifier3)); - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_portId() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", null, null); - TICIdentifier identifier2 = new TICIdentifier("{1-7}", null, null); - - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_same_portName() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM3", "021976551633"); - TICIdentifier identifier3 = new TICIdentifier("{1-3}", "COM3", null); - TICIdentifier identifier4 = new TICIdentifier(null, "COM3", null); - - Assert.assertTrue(identifier1.matches(identifier4)); - Assert.assertFalse(identifier1.matches(identifier3)); - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_portName() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier(null, "COM3", null); - TICIdentifier identifier2 = new TICIdentifier(null, "COM8", null); - - Assert.assertFalse(identifier1.matches(identifier2)); - } -} +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package enedis.tic.core; + +import enedis.lab.types.DataDictionaryException; +import org.junit.Assert; +import org.junit.Test; + +public class TICIdentifierTest { + @Test + public void test_constructor_full() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier("{1-1}", "COM3", "021976551632"); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + Assert.assertEquals("COM3", identifier.getPortName()); + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlyPortId() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + Assert.assertNull(identifier.getPortName()); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlyPortName() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, "COM3", null); + + Assert.assertNull(identifier.getPortId()); + Assert.assertEquals("COM3", identifier.getPortName()); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlySerialNumber() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); + + Assert.assertNull(identifier.getPortId()); + Assert.assertNull(identifier.getPortName()); + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + } + + @Test(expected = DataDictionaryException.class) + public void test_constructor_empty() throws DataDictionaryException { + new TICIdentifier(null, null, null); + } + + @Test + public void test_setPortId_null_notEmpty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier("{1-1}", null, "021976551632"); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + identifier.setPortId(null); + Assert.assertNull(identifier.getPortId()); + } + + @Test(expected = DataDictionaryException.class) + public void test_setPortId_null_empty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + identifier.setPortId(null); + } + + @Test + public void test_setPortName_null_notEmpty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); + + Assert.assertEquals("COM3", identifier.getPortName()); + identifier.setPortName(null); + Assert.assertNull(identifier.getPortName()); + } + + @Test(expected = DataDictionaryException.class) + public void test_setPortName_null_empty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, "COM3", null); + + Assert.assertEquals("COM3", identifier.getPortName()); + identifier.setPortName(null); + } + + @Test + public void test_setSerialNumber_null_notEmpty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); + + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + identifier.setSerialNumber(null); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test(expected = DataDictionaryException.class) + public void test_setSerialNumber_null_empty() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); + + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + identifier.setSerialNumber(null); + } + + @Test + public void test_matches_null() throws DataDictionaryException { + TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); + + Assert.assertFalse(identifier.matches(null)); + } + + @Test + public void test_matches_same_serialNumber() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM4", "021976551632"); + + Assert.assertTrue(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_serialNumber() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM3", "021976551638"); + + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_same_portId() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM4", "021976551633"); + TICIdentifier identifier3 = new TICIdentifier("{1-1}", "COM5", null); + + Assert.assertTrue(identifier1.matches(identifier3)); + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_portId() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", null, null); + TICIdentifier identifier2 = new TICIdentifier("{1-7}", null, null); + + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_same_portName() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); + TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM3", "021976551633"); + TICIdentifier identifier3 = new TICIdentifier("{1-3}", "COM3", null); + TICIdentifier identifier4 = new TICIdentifier(null, "COM3", null); + + Assert.assertTrue(identifier1.matches(identifier4)); + Assert.assertFalse(identifier1.matches(identifier3)); + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_portName() throws DataDictionaryException { + TICIdentifier identifier1 = new TICIdentifier(null, "COM3", null); + TICIdentifier identifier2 = new TICIdentifier(null, "COM8", null); + + Assert.assertFalse(identifier1.matches(identifier2)); + } +} From c1d46ddbe2f37b215eaead9be296b2b3965325b3 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Thu, 9 Oct 2025 16:29:41 +0200 Subject: [PATCH 29/59] chore: format with spotless --- .../java/enedis/lab/types/DataArrayList.java | 5 ++-- .../lab/types/DataDictionaryException.java | 20 ++++++++-------- .../java/enedis/lab/types/ExceptionBase.java | 10 ++++---- .../java/enedis/lab/util/MinMaxChecker.java | 11 +++++---- .../java/enedis/lab/util/SystemError.java | 12 ++++++---- src/main/java/enedis/lab/util/SystemLibC.java | 12 ++++++---- .../java/enedis/lab/util/message/Event.java | 12 ++++++---- .../java/enedis/lab/util/message/Message.java | 12 ++++++---- .../enedis/lab/util/message/MessageType.java | 10 ++++---- .../java/enedis/lab/util/message/Request.java | 11 +++++---- .../enedis/lab/util/message/Response.java | 13 ++++++----- .../enedis/lab/util/message/ResponseBase.java | 11 +++++---- .../message/exception/MessageException.java | 19 +++++++-------- .../MessageInvalidContentException.java | 19 +++++++-------- .../MessageInvalidFormatException.java | 13 ++++++----- .../MessageInvalidTypeException.java | 19 +++++++-------- .../MessageKeyNameDoesntExistException.java | 19 +++++++-------- .../MessageKeyTypeDoesntExistException.java | 19 +++++++-------- .../UnsupportedMessageException.java | 13 ++++++----- .../factory/AbstractMessageFactory.java | 13 ++++++----- .../message/factory/BasicMessageFactory.java | 11 +++++---- .../util/message/factory/EventFactory.java | 7 +++--- .../util/message/factory/MessageFactory.java | 23 ++++++++++--------- .../util/message/factory/RequestFactory.java | 15 ++++++------ .../util/message/factory/ResponseFactory.java | 7 +++--- .../lab/util/task/FilteredNotifier.java | 11 +++++---- .../lab/util/task/FilteredNotifierBase.java | 13 ++++++----- .../java/enedis/lab/util/task/Notifier.java | 11 +++++---- .../enedis/lab/util/task/NotifierBase.java | 7 +++--- .../java/enedis/lab/util/task/Subscriber.java | 11 +++++---- src/main/java/enedis/lab/util/task/Task.java | 12 ++++++---- .../java/enedis/lab/util/task/TaskBase.java | 11 +++++---- .../enedis/lab/util/task/TaskPeriodic.java | 11 +++++---- .../task/TaskPeriodicWithSubscribers.java | 7 +++--- src/main/java/enedis/lab/util/time/Time.java | 12 ++++++---- .../tic/core/ReadNextFrameSubscriber.java | 11 +++++---- src/main/java/enedis/tic/core/TICCore.java | 10 ++++---- .../java/enedis/tic/core/TICCoreBase.java | 11 +++++---- .../java/enedis/tic/core/TICCoreError.java | 10 ++++---- .../enedis/tic/core/TICCoreErrorCode.java | 7 +++--- .../java/enedis/tic/core/TICCoreFrame.java | 10 ++++---- .../java/enedis/tic/core/TICCoreStream.java | 11 +++++---- .../enedis/tic/core/TICCoreStreamBase.java | 11 +++++---- .../enedis/tic/core/TICCoreSubscriber.java | 7 +++--- .../java/enedis/tic/core/TICIdentifier.java | 12 ++++++---- 45 files changed, 295 insertions(+), 247 deletions(-) diff --git a/src/main/java/enedis/lab/types/DataArrayList.java b/src/main/java/enedis/lab/types/DataArrayList.java index 39b002d..476dc9a 100644 --- a/src/main/java/enedis/lab/types/DataArrayList.java +++ b/src/main/java/enedis/lab/types/DataArrayList.java @@ -19,7 +19,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.RandomAccess; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -37,8 +36,8 @@ public class DataArrayList extends ArrayList implements DataList { /** * Returns a fixed-size data list backed by the specified array. (Changes to the returned data * list "write through" to the array.) This method acts as bridge between array-based and - * collection-based APIs, in combination with {@link Collection#toArray}. - * + * collection-based APIs, in combination with {@link Collection#toArray}. + * *

    This method also provides a convenient way to create a fixed-size data list initialized to * contain several elements. * diff --git a/src/main/java/enedis/lab/types/DataDictionaryException.java b/src/main/java/enedis/lab/types/DataDictionaryException.java index bf2c966..d78d6f4 100644 --- a/src/main/java/enedis/lab/types/DataDictionaryException.java +++ b/src/main/java/enedis/lab/types/DataDictionaryException.java @@ -8,14 +8,14 @@ package enedis.lab.types; /** - * Exception type used to represent errors occurring when manipulating or interpreting - * data dictionary elements. + * Exception type used to represent errors occurring when manipulating or interpreting data + * dictionary elements. * - *

    This exception typically indicates issues such as invalid data mappings, corrupted - * metadata, or unexpected dictionary structures encountered during runtime.

    + *

    This exception typically indicates issues such as invalid data mappings, corrupted metadata, + * or unexpected dictionary structures encountered during runtime. * - *

    It extends {@link java.lang.Exception}, allowing both checked exception handling and - * detailed error propagation with message and cause information.

    + *

    It extends {@link java.lang.Exception}, allowing both checked exception handling and detailed + * error propagation with message and cause information. * * @author Enedis Smarties team */ @@ -23,9 +23,7 @@ public class DataDictionaryException extends Exception { private static final long serialVersionUID = -7967428428453584771L; - /** - * Creates a new {@code DataDictionaryException} with no detail message or cause. - */ + /** Creates a new {@code DataDictionaryException} with no detail message or cause. */ public DataDictionaryException() { super(); } @@ -59,8 +57,8 @@ public DataDictionaryException(String message, Throwable cause) { } /** - * Creates a new {@code DataDictionaryException} with full control over suppression - * and stack trace writability. + * Creates a new {@code DataDictionaryException} with full control over suppression and stack + * trace writability. * * @param message the detail message describing the error context * @param cause the underlying cause of the exception diff --git a/src/main/java/enedis/lab/types/ExceptionBase.java b/src/main/java/enedis/lab/types/ExceptionBase.java index c97b155..e03d831 100644 --- a/src/main/java/enedis/lab/types/ExceptionBase.java +++ b/src/main/java/enedis/lab/types/ExceptionBase.java @@ -10,12 +10,12 @@ /** * Generic exception base class. * - *

    This class provides a simple mechanism to associate an error code and an informational - * message with an exception. It can be used as a parent class for more specific exception types - * that require standardized error reporting and structured diagnostic data.

    + *

    This class provides a simple mechanism to associate an error code and an informational message + * with an exception. It can be used as a parent class for more specific exception types that + * require standardized error reporting and structured diagnostic data. * - *

    Both the code and info fields are intended to help identify and categorize the source of - * the problem more precisely in distributed or modular systems.

    + *

    Both the code and info fields are intended to help identify and categorize the source of the + * problem more precisely in distributed or modular systems. * * @author Enedis Smarties team */ diff --git a/src/main/java/enedis/lab/util/MinMaxChecker.java b/src/main/java/enedis/lab/util/MinMaxChecker.java index d945ae4..f77745b 100644 --- a/src/main/java/enedis/lab/util/MinMaxChecker.java +++ b/src/main/java/enedis/lab/util/MinMaxChecker.java @@ -10,14 +10,15 @@ /** * Utility class for checking if values are within a specified numeric range. * - *

    This class provides methods to set minimum and maximum bounds and to check whether a given value - * falls within the defined range. It is designed for general-purpose range validation. + *

    This class provides methods to set minimum and maximum bounds and to check whether a given + * value falls within the defined range. It is designed for general-purpose range validation. * *

    Common use cases include: + * *

      - *
    • Validating input values against numeric constraints
    • - *
    • Ensuring values remain within allowed limits
    • - *
    • Supporting configuration or parameter validation
    • + *
    • Validating input values against numeric constraints + *
    • Ensuring values remain within allowed limits + *
    • Supporting configuration or parameter validation *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/SystemError.java b/src/main/java/enedis/lab/util/SystemError.java index ef4ca9c..a61612e 100644 --- a/src/main/java/enedis/lab/util/SystemError.java +++ b/src/main/java/enedis/lab/util/SystemError.java @@ -14,14 +14,16 @@ /** * Utility class for retrieving system error codes and messages. * - *

    This class provides static methods to obtain the last system error code and its associated message, - * supporting multiple operating systems. It is designed for general-purpose error handling and diagnostics. + *

    This class provides static methods to obtain the last system error code and its associated + * message, supporting multiple operating systems. It is designed for general-purpose error handling + * and diagnostics. * *

    Common use cases include: + * *

      - *
    • Retrieving the last system error code
    • - *
    • Obtaining human-readable error messages
    • - *
    • Supporting cross-platform error diagnostics
    • + *
    • Retrieving the last system error code + *
    • Obtaining human-readable error messages + *
    • Supporting cross-platform error diagnostics *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/SystemLibC.java b/src/main/java/enedis/lab/util/SystemLibC.java index 33491b2..db83761 100644 --- a/src/main/java/enedis/lab/util/SystemLibC.java +++ b/src/main/java/enedis/lab/util/SystemLibC.java @@ -13,14 +13,16 @@ /** * Interface for accessing native C library functions via JNA. * - *

    This interface provides access to system-level C library functions, such as retrieving error messages - * associated with error codes. It is designed for general-purpose native integration and diagnostics. + *

    This interface provides access to system-level C library functions, such as retrieving error + * messages associated with error codes. It is designed for general-purpose native integration and + * diagnostics. * *

    Common use cases include: + * *

      - *
    • Obtaining human-readable error messages from system error codes
    • - *
    • Integrating with native system libraries
    • - *
    • Supporting cross-platform diagnostics
    • + *
    • Obtaining human-readable error messages from system error codes + *
    • Integrating with native system libraries + *
    • Supporting cross-platform diagnostics *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/message/Event.java b/src/main/java/enedis/lab/util/message/Event.java index b6eec9f..1fb7b02 100644 --- a/src/main/java/enedis/lab/util/message/Event.java +++ b/src/main/java/enedis/lab/util/message/Event.java @@ -19,14 +19,16 @@ /** * Abstract base class for event messages. * - *

    This class represents event messages. It provides support for event-specific fields such as date/time and - * enforces the accepted message type. Subclasses can define additional event data and behavior. + *

    This class represents event messages. It provides support for event-specific fields such as + * date/time and enforces the accepted message type. Subclasses can define additional event data and + * behavior. * *

    Common use cases include: + * *

      - *
    • Representing system or application events
    • - *
    • Handling event notifications in the message pipeline
    • - *
    • Extending for custom event types
    • + *
    • Representing system or application events + *
    • Handling event notifications in the message pipeline + *
    • Extending for custom event types *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/message/Message.java b/src/main/java/enedis/lab/util/message/Message.java index a5ca362..bdaf81f 100644 --- a/src/main/java/enedis/lab/util/message/Message.java +++ b/src/main/java/enedis/lab/util/message/Message.java @@ -20,14 +20,16 @@ /** * Base class for all messages. * - *

    This class provides the core structure for messages, including type and name fields. It supports construction from maps and data dictionaries, - * and can be extended for specific message types such as requests, responses, and events. + *

    This class provides the core structure for messages, including type and name fields. It + * supports construction from maps and data dictionaries, and can be extended for specific message + * types such as requests, responses, and events. * *

    Common use cases include: + * *

      - *
    • Representing generic messages in the communication pipeline
    • - *
    • Providing a base for request, response, and event messages
    • - *
    • Supporting extensible message structures
    • + *
    • Representing generic messages in the communication pipeline + *
    • Providing a base for request, response, and event messages + *
    • Supporting extensible message structures *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/message/MessageType.java b/src/main/java/enedis/lab/util/message/MessageType.java index 981141d..5d92e22 100644 --- a/src/main/java/enedis/lab/util/message/MessageType.java +++ b/src/main/java/enedis/lab/util/message/MessageType.java @@ -10,13 +10,13 @@ /** * Enumeration of message types. * - *

    This enum defines the supported message types, including events, requests, and responses. It is used to categorize - * messages and control their processing in the communication pipeline. + *

    This enum defines the supported message types, including events, requests, and responses. It + * is used to categorize messages and control their processing in the communication pipeline. * *

      - *
    • {@link #EVENT} - Event messages
    • - *
    • {@link #REQUEST} - Request messages
    • - *
    • {@link #RESPONSE} - Response messages
    • + *
    • {@link #EVENT} - Event messages + *
    • {@link #REQUEST} - Request messages + *
    • {@link #RESPONSE} - Response messages *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/message/Request.java b/src/main/java/enedis/lab/util/message/Request.java index 64a3156..483d322 100644 --- a/src/main/java/enedis/lab/util/message/Request.java +++ b/src/main/java/enedis/lab/util/message/Request.java @@ -17,14 +17,15 @@ /** * Abstract base class for request messages. * - *

    This class represents request messages. It enforces the accepted message type and provides support for request-specific - * fields. Subclasses can define additional request data and behavior. + *

    This class represents request messages. It enforces the accepted message type and provides + * support for request-specific fields. Subclasses can define additional request data and behavior. * *

    Common use cases include: + * *

      - *
    • Representing client requests for data or actions
    • - *
    • Handling request validation and processing
    • - *
    • Extending for custom request types
    • + *
    • Representing client requests for data or actions + *
    • Handling request validation and processing + *
    • Extending for custom request types *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/message/Response.java b/src/main/java/enedis/lab/util/message/Response.java index 1bb4596..1f5274d 100644 --- a/src/main/java/enedis/lab/util/message/Response.java +++ b/src/main/java/enedis/lab/util/message/Response.java @@ -21,15 +21,16 @@ /** * Abstract base class for response messages. * - *

    This class represents response messages. It provides support for response-specific fields such as date/time, - * error code, and error message, and enforces the accepted message type. Subclasses can define - * additional response data and behavior. + *

    This class represents response messages. It provides support for response-specific fields such + * as date/time, error code, and error message, and enforces the accepted message type. Subclasses + * can define additional response data and behavior. * *

    Common use cases include: + * *

      - *
    • Representing server responses to client requests
    • - *
    • Handling error reporting and status information
    • - *
    • Extending for custom response types
    • + *
    • Representing server responses to client requests + *
    • Handling error reporting and status information + *
    • Extending for custom response types *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/message/ResponseBase.java b/src/main/java/enedis/lab/util/message/ResponseBase.java index f26b9d2..3abf06b 100644 --- a/src/main/java/enedis/lab/util/message/ResponseBase.java +++ b/src/main/java/enedis/lab/util/message/ResponseBase.java @@ -21,14 +21,15 @@ * Concrete base class for response messages with data payload. * *

    This class extends {@link Response} to provide support for a data payload field, enabling - * responses to carry additional structured data. It is used for responses that require more - * complex content beyond basic status and error reporting. + * responses to carry additional structured data. It is used for responses that require more complex + * content beyond basic status and error reporting. * *

    Common use cases include: + * *

      - *
    • Returning structured data in response to client requests
    • - *
    • Supporting extensible response formats
    • - *
    • Providing a base for custom response types with data
    • + *
    • Returning structured data in response to client requests + *
    • Supporting extensible response formats + *
    • Providing a base for custom response types with data *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/message/exception/MessageException.java b/src/main/java/enedis/lab/util/message/exception/MessageException.java index d5e47a8..084896b 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageException.java @@ -10,16 +10,17 @@ /** * Exception for signaling errors during message processing in the TIC2WebSocket framework. * - *

    This exception serves as the base class for all message-related errors encountered while parsing, - * validating, or handling messages. - * It enables robust error reporting and propagation throughout the message handling pipeline. + *

    This exception serves as the base class for all message-related errors encountered while + * parsing, validating, or handling messages. It enables robust error reporting and propagation + * throughout the message handling pipeline. * *

    Common use cases include: + * *

      - *
    • Reporting invalid message formats
    • - *
    • Handling missing or unsupported message types
    • - *
    • Signaling errors in message content or structure
    • - *
    • Chaining underlying exceptions for debugging
    • + *
    • Reporting invalid message formats + *
    • Handling missing or unsupported message types + *
    • Signaling errors in message content or structure + *
    • Chaining underlying exceptions for debugging *
    * * @author Enedis Smarties team @@ -40,8 +41,8 @@ public MessageException() { /** * Creates a new MessageException with a detail message and underlying cause. * - *

    This constructor is used when both a descriptive error message and the original cause - * of the error are available, providing complete context for debugging. + *

    This constructor is used when both a descriptive error message and the original cause of the + * error are available, providing complete context for debugging. * * @param message the detail message explaining the error * @param cause the underlying exception that caused this error diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java index e4e34f2..c56d1d9 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java @@ -10,16 +10,17 @@ /** * Exception indicating that a message contains invalid or unexpected content. * - *

    This exception is thrown when the content of a message does not conform to the expected structure, - * contains invalid values, or fails validation during parsing or processing. It is part of the message - * exception hierarchy used to signal specific errors in communication. + *

    This exception is thrown when the content of a message does not conform to the expected + * structure, contains invalid values, or fails validation during parsing or processing. It is part + * of the message exception hierarchy used to signal specific errors in communication. * *

    Common use cases include: + * *

      - *
    • Detecting missing or malformed fields in a message
    • - *
    • Reporting invalid data values or types
    • - *
    • Handling content validation failures
    • - *
    • Chaining underlying exceptions for debugging
    • + *
    • Detecting missing or malformed fields in a message + *
    • Reporting invalid data values or types + *
    • Handling content validation failures + *
    • Chaining underlying exceptions for debugging *
    * * @author Enedis Smarties team @@ -40,8 +41,8 @@ public MessageInvalidContentException() { /** * Creates a new MessageInvalidContentException with a detail message and underlying cause. * - *

    This constructor is used when both a descriptive error message and the original cause - * of the error are available, providing complete context for debugging. + *

    This constructor is used when both a descriptive error message and the original cause of the + * error are available, providing complete context for debugging. * * @param message the detail message explaining the error * @param cause the underlying exception that caused this error diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java index 4ddef57..e5aaad1 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java @@ -15,11 +15,12 @@ * of the message exception hierarchy used to signal specific errors in communication. * *

    Common use cases include: + * *

      - *
    • Detecting malformed or corrupted messages
    • - *
    • Reporting missing or unexpected fields
    • - *
    • Handling parsing failures due to format errors
    • - *
    • Chaining underlying exceptions for debugging
    • + *
    • Detecting malformed or corrupted messages + *
    • Reporting missing or unexpected fields + *
    • Handling parsing failures due to format errors + *
    • Chaining underlying exceptions for debugging *
    * * @author Enedis Smarties team @@ -40,8 +41,8 @@ public MessageInvalidFormatException() { /** * Creates a new MessageInvalidFormatException with a detail message and underlying cause. * - *

    This constructor is used when both a descriptive error message and the original cause - * of the error are available, providing complete context for debugging. + *

    This constructor is used when both a descriptive error message and the original cause of the + * error are available, providing complete context for debugging. * * @param message the detail message explaining the error * @param cause the underlying exception that caused this error diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java index 69b0147..69edef4 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java @@ -10,16 +10,17 @@ /** * Exception indicating that a message has an invalid or unsupported type. * - *

    This exception is thrown when a message specifies a type that is not recognized, not supported, - * or does not match the expected types during parsing or processing. It is part of the message exception - * hierarchy used to signal specific errors in communication. + *

    This exception is thrown when a message specifies a type that is not recognized, not + * supported, or does not match the expected types during parsing or processing. It is part of the + * message exception hierarchy used to signal specific errors in communication. * *

    Common use cases include: + * *

      - *
    • Detecting unknown or unsupported message types
    • - *
    • Reporting type mismatches in message definitions
    • - *
    • Handling validation failures due to type errors
    • - *
    • Chaining underlying exceptions for debugging
    • + *
    • Detecting unknown or unsupported message types + *
    • Reporting type mismatches in message definitions + *
    • Handling validation failures due to type errors + *
    • Chaining underlying exceptions for debugging *
    * * @author Enedis Smarties team @@ -40,8 +41,8 @@ public MessageInvalidTypeException() { /** * Creates a new MessageInvalidTypeException with a detail message and underlying cause. * - *

    This constructor is used when both a descriptive error message and the original cause - * of the error are available, providing complete context for debugging. + *

    This constructor is used when both a descriptive error message and the original cause of the + * error are available, providing complete context for debugging. * * @param message the detail message explaining the error * @param cause the underlying exception that caused this error diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java index 46e6788..266e736 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java @@ -10,16 +10,17 @@ /** * Exception indicating that a required message key name does not exist. * - *

    This exception is thrown when a message references a key name that is missing, undefined, - * or not recognized during parsing or processing. It is part of the message exception hierarchy - * used to signal specific errors in communication. + *

    This exception is thrown when a message references a key name that is missing, undefined, or + * not recognized during parsing or processing. It is part of the message exception hierarchy used + * to signal specific errors in communication. * *

    Common use cases include: + * *

      - *
    • Detecting missing key names in message definitions
    • - *
    • Reporting undefined or unrecognized keys
    • - *
    • Handling validation failures due to absent keys
    • - *
    • Chaining underlying exceptions for debugging
    • + *
    • Detecting missing key names in message definitions + *
    • Reporting undefined or unrecognized keys + *
    • Handling validation failures due to absent keys + *
    • Chaining underlying exceptions for debugging *
    * * @author Enedis Smarties team @@ -40,8 +41,8 @@ public MessageKeyNameDoesntExistException() { /** * Creates a new MessageKeyNameDoesntExistException with a detail message and underlying cause. * - *

    This constructor is used when both a descriptive error message and the original cause - * of the error are available, providing complete context for debugging. + *

    This constructor is used when both a descriptive error message and the original cause of the + * error are available, providing complete context for debugging. * * @param message the detail message explaining the error * @param cause the underlying exception that caused this error diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java index 9b7c2b0..6e20c8a 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java +++ b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java @@ -10,16 +10,17 @@ /** * Exception indicating that a required message key type does not exist. * - *

    This exception is thrown when a message references a key type that is missing, undefined, - * or not recognized during parsing or processing. It is part of the message exception hierarchy - * used to signal specific errors in communication. + *

    This exception is thrown when a message references a key type that is missing, undefined, or + * not recognized during parsing or processing. It is part of the message exception hierarchy used + * to signal specific errors in communication. * *

    Common use cases include: + * *

      - *
    • Detecting missing key types in message definitions
    • - *
    • Reporting undefined or unrecognized key types
    • - *
    • Handling validation failures due to absent key types
    • - *
    • Chaining underlying exceptions for debugging
    • + *
    • Detecting missing key types in message definitions + *
    • Reporting undefined or unrecognized key types + *
    • Handling validation failures due to absent key types + *
    • Chaining underlying exceptions for debugging *
    * * @author Enedis Smarties team @@ -40,8 +41,8 @@ public MessageKeyTypeDoesntExistException() { /** * Creates a new MessageKeyTypeDoesntExistException with a detail message and underlying cause. * - *

    This constructor is used when both a descriptive error message and the original cause - * of the error are available, providing complete context for debugging. + *

    This constructor is used when both a descriptive error message and the original cause of the + * error are available, providing complete context for debugging. * * @param message the detail message explaining the error * @param cause the underlying exception that caused this error diff --git a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java index e967f4d..f9330dc 100644 --- a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java +++ b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java @@ -15,11 +15,12 @@ * signal specific errors in communication. * *

    Common use cases include: + * *

      - *
    • Detecting unsupported message types or operations
    • - *
    • Reporting attempts to use unimplemented features
    • - *
    • Handling validation failures due to unsupported requests
    • - *
    • Chaining underlying exceptions for debugging
    • + *
    • Detecting unsupported message types or operations + *
    • Reporting attempts to use unimplemented features + *
    • Handling validation failures due to unsupported requests + *
    • Chaining underlying exceptions for debugging *
    * * @author Enedis Smarties team @@ -40,8 +41,8 @@ public UnsupportedMessageException() { /** * Creates a new UnsupportedMessageException with a detail message and underlying cause. * - *

    This constructor is used when both a descriptive error message and the original cause - * of the error are available, providing complete context for debugging. + *

    This constructor is used when both a descriptive error message and the original cause of the + * error are available, providing complete context for debugging. * * @param message the detail message explaining the error * @param cause the underlying exception that caused this error diff --git a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java index 37e4070..f39606c 100644 --- a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java @@ -25,11 +25,12 @@ * classes to be registered and decoded dynamically based on their names. * *

    Common use cases include: + * *

      - *
    • Decoding incoming JSON messages into typed objects
    • - *
    • Registering custom message types for extensibility
    • - *
    • Handling errors in message format or content
    • - *
    • Supporting generic message processing pipelines
    • + *
    • Decoding incoming JSON messages into typed objects + *
    • Registering custom message types for extensibility + *
    • Handling errors in message format or content + *
    • Supporting generic message processing pipelines *
    * * @param the type of message handled by this factory @@ -42,8 +43,8 @@ public class AbstractMessageFactory { /** * Creates a new AbstractMessageFactory for the specified message type. * - *

    This constructor initializes the factory for a given message class and prepares the - * internal registry for message type mappings. + *

    This constructor initializes the factory for a given message class and prepares the internal + * registry for message type mappings. * * @param clazz the class of message objects handled by this factory */ diff --git a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java index f2df7cc..13e7205 100644 --- a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java @@ -25,17 +25,18 @@ * specific exceptions for format, type, or key errors. * *

    Common use cases include: + * *

      - *
    • Decoding incoming JSON messages into basic message objects
    • - *
    • Validating message format and required keys
    • - *
    • Handling errors in message type or name extraction
    • - *
    • Supporting generic message processing pipelines
    • + *
    • Decoding incoming JSON messages into basic message objects + *
    • Validating message format and required keys + *
    • Handling errors in message type or name extraction + *
    • Supporting generic message processing pipelines *
    * * @author Enedis Smarties team */ public abstract class BasicMessageFactory { - + /** * Decodes a basic message from its text representation. * diff --git a/src/main/java/enedis/lab/util/message/factory/EventFactory.java b/src/main/java/enedis/lab/util/message/factory/EventFactory.java index 858d939..8aa53b6 100644 --- a/src/main/java/enedis/lab/util/message/factory/EventFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/EventFactory.java @@ -17,10 +17,11 @@ * typically JSON, and supports extensible event handling in the message processing pipeline. * *

    Common use cases include: + * *

      - *
    • Decoding incoming event messages into typed objects
    • - *
    • Registering custom event types for extensibility
    • - *
    • Supporting generic event processing pipelines
    • + *
    • Decoding incoming event messages into typed objects + *
    • Registering custom event types for extensibility + *
    • Supporting generic event processing pipelines *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java index af79a96..4804409 100644 --- a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java @@ -18,16 +18,17 @@ /** * Central factory for decoding and dispatching messages in the TIC2WebSocket framework. * - *

    This class coordinates the decoding of messages by delegating to specialized sub-factories - * for requests, responses, and events. It parses the message type and name, then dispatches to - * the appropriate factory for further decoding and validation. + *

    This class coordinates the decoding of messages by delegating to specialized sub-factories for + * requests, responses, and events. It parses the message type and name, then dispatches to the + * appropriate factory for further decoding and validation. * *

    Common use cases include: + * *

      - *
    • Decoding incoming JSON messages into typed objects
    • - *
    • Coordinating request, response, and event message handling
    • - *
    • Supporting extensible message processing pipelines
    • - *
    • Managing sub-factory references for modularity
    • + *
    • Decoding incoming JSON messages into typed objects + *
    • Coordinating request, response, and event message handling + *
    • Supporting extensible message processing pipelines + *
    • Managing sub-factory references for modularity *
    * * @author Enedis Smarties team @@ -53,8 +54,8 @@ public MessageFactory() { /** * Creates a new MessageFactory with the specified sub-factories. * - *

    This constructor initializes the factory with request, response, and event sub-factories - * for coordinated message decoding. + *

    This constructor initializes the factory with request, response, and event sub-factories for + * coordinated message decoding. * * @param requestFactory the factory for decoding request messages * @param responseFactory the factory for decoding response messages @@ -72,8 +73,8 @@ public MessageFactory( * Decodes and dispatches a message from its text representation. * *

    This method parses the provided text (typically JSON), determines the message type, and - * delegates decoding to the appropriate sub-factory. It throws specific exceptions for unsupported - * message types, invalid formats, or missing keys. + * delegates decoding to the appropriate sub-factory. It throws specific exceptions for + * unsupported message types, invalid formats, or missing keys. * * @param text the text representation of the message (usually JSON) * @return the decoded message object diff --git a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java index 2d06266..ed80d48 100644 --- a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java @@ -12,16 +12,17 @@ /** * Factory for decoding and instantiating request messages. * - *

    This factory is responsible for creating {@link Request} objects from their serialized representations. - * It supports extensible registration of request message types, enabling dynamic decoding and validation - * of incoming request messages. + *

    This factory is responsible for creating {@link Request} objects from their serialized + * representations. It supports extensible registration of request message types, enabling dynamic + * decoding and validation of incoming request messages. * *

    Common use cases include: + * *

      - *
    • Decoding incoming JSON request messages into typed {@link Request} objects
    • - *
    • Registering custom request message classes for extensibility
    • - *
    • Supporting modular request processing pipelines
    • - *
    • Managing request message type mappings for robust decoding
    • + *
    • Decoding incoming JSON request messages into typed {@link Request} objects + *
    • Registering custom request message classes for extensibility + *
    • Supporting modular request processing pipelines + *
    • Managing request message type mappings for robust decoding *
    * *

    The factory pattern allows for decoupled message instantiation, enabling flexible handling of diff --git a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java index 95d1dfc..3923f81 100644 --- a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java +++ b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java @@ -17,10 +17,11 @@ * typically JSON, and supports extensible response handling in the message processing pipeline. * *

    Common use cases include: + * *

      - *
    • Decoding incoming response messages into typed objects
    • - *
    • Registering custom response types for extensibility
    • - *
    • Supporting generic response processing pipelines
    • + *
    • Decoding incoming response messages into typed objects + *
    • Registering custom response types for extensibility + *
    • Supporting generic response processing pipelines *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifier.java b/src/main/java/enedis/lab/util/task/FilteredNotifier.java index 65cea8d..2124192 100644 --- a/src/main/java/enedis/lab/util/task/FilteredNotifier.java +++ b/src/main/java/enedis/lab/util/task/FilteredNotifier.java @@ -13,14 +13,15 @@ /** * Interface for managing filtered notifications to subscribers. * - *

    This interface defines methods for subscribing, unsubscribing, and querying subscribers - * with associated filters. It supports flexible event delivery and observer patterns. + *

    This interface defines methods for subscribing, unsubscribing, and querying subscribers with + * associated filters. It supports flexible event delivery and observer patterns. * *

    Common use cases include: + * *

      - *
    • Selective event delivery based on filters
    • - *
    • Managing filtered lists of subscribers
    • - *
    • Supporting custom notification logic
    • + *
    • Selective event delivery based on filters + *
    • Managing filtered lists of subscribers + *
    • Supporting custom notification logic *
    * * @param the filter type diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java index f09642f..1b99bbd 100644 --- a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java +++ b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java @@ -19,15 +19,16 @@ /** * Generic base implementation for managing filtered notifications to subscribers. * - *

    This class provides mechanisms for subscribing, unsubscribing, and querying subscribers - * with associated filters. It supports both filtered and unfiltered (global) subscriptions, - * enabling flexible event delivery and observer patterns. + *

    This class provides mechanisms for subscribing, unsubscribing, and querying subscribers with + * associated filters. It supports both filtered and unfiltered (global) subscriptions, enabling + * flexible event delivery and observer patterns. * *

    Common use cases include: + * *

      - *
    • Delivering events to subscribers that match specific filters
    • - *
    • Supporting selective notification in observer patterns
    • - *
    • Extending for custom filtering logic
    • + *
    • Delivering events to subscribers that match specific filters + *
    • Supporting selective notification in observer patterns + *
    • Extending for custom filtering logic *
    * * @param the filter type diff --git a/src/main/java/enedis/lab/util/task/Notifier.java b/src/main/java/enedis/lab/util/task/Notifier.java index 3bbbd60..c522a80 100644 --- a/src/main/java/enedis/lab/util/task/Notifier.java +++ b/src/main/java/enedis/lab/util/task/Notifier.java @@ -13,14 +13,15 @@ * Interface for managing subscribers and sending notifications. * *

    This interface defines methods for adding, removing, and querying subscribers, as well as - * retrieving the current set of subscribers. It is used as a generic contract for event-driven - * and observer patterns. + * retrieving the current set of subscribers. It is used as a generic contract for event-driven and + * observer patterns. * *

    Common use cases include: + * *

      - *
    • Managing lists of listeners or observers
    • - *
    • Sending notifications to registered subscribers
    • - *
    • Supporting event-driven architectures
    • + *
    • Managing lists of listeners or observers + *
    • Sending notifications to registered subscribers + *
    • Supporting event-driven architectures *
    * * @param the subscriber type diff --git a/src/main/java/enedis/lab/util/task/NotifierBase.java b/src/main/java/enedis/lab/util/task/NotifierBase.java index 7787f9b..f41a5de 100644 --- a/src/main/java/enedis/lab/util/task/NotifierBase.java +++ b/src/main/java/enedis/lab/util/task/NotifierBase.java @@ -18,10 +18,11 @@ * foundation for event-driven and observer patterns, supporting flexible notification delivery. * *

    Common use cases include: + * *

      - *
    • Managing lists of listeners or observers
    • - *
    • Sending notifications to registered subscribers
    • - *
    • Extending for custom notification logic
    • + *
    • Managing lists of listeners or observers + *
    • Sending notifications to registered subscribers + *
    • Extending for custom notification logic *
    * * @param the subscriber type diff --git a/src/main/java/enedis/lab/util/task/Subscriber.java b/src/main/java/enedis/lab/util/task/Subscriber.java index 4fc3501..b2265eb 100644 --- a/src/main/java/enedis/lab/util/task/Subscriber.java +++ b/src/main/java/enedis/lab/util/task/Subscriber.java @@ -10,14 +10,15 @@ /** * Interface representing a generic event subscriber. * - *

    This interface is used as a contract for objects that can receive notifications or events - * from a notifier. It is commonly used in observer and event-driven patterns. + *

    This interface is used as a contract for objects that can receive notifications or events from + * a notifier. It is commonly used in observer and event-driven patterns. * *

    Common use cases include: + * *

      - *
    • Receiving notifications from event sources
    • - *
    • Implementing observer patterns
    • - *
    • Extending for custom event handling logic
    • + *
    • Receiving notifications from event sources + *
    • Implementing observer patterns + *
    • Extending for custom event handling logic *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/task/Task.java b/src/main/java/enedis/lab/util/task/Task.java index 5c3826d..6549092 100644 --- a/src/main/java/enedis/lab/util/task/Task.java +++ b/src/main/java/enedis/lab/util/task/Task.java @@ -10,14 +10,16 @@ /** * Interface representing a generic asynchronous task. * - *

    This interface defines the contract for executing tasks, typically in a concurrent or event-driven - * environment. Implementations may represent background jobs, scheduled actions, or event handlers. + *

    This interface defines the contract for executing tasks, typically in a concurrent or + * event-driven environment. Implementations may represent background jobs, scheduled actions, or + * event handlers. * *

    Common use cases include: + * *

      - *
    • Running background operations
    • - *
    • Scheduling periodic or delayed actions
    • - *
    • Handling events or notifications
    • + *
    • Running background operations + *
    • Scheduling periodic or delayed actions + *
    • Handling events or notifications *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/task/TaskBase.java b/src/main/java/enedis/lab/util/task/TaskBase.java index 58c762e..9ddfdf4 100644 --- a/src/main/java/enedis/lab/util/task/TaskBase.java +++ b/src/main/java/enedis/lab/util/task/TaskBase.java @@ -14,14 +14,15 @@ /** * Abstract base class representing a generic asynchronous task. * - *

    This class provides a foundation for implementing tasks that can be executed - * asynchronously, typically in concurrent or event-driven environments. + *

    This class provides a foundation for implementing tasks that can be executed asynchronously, + * typically in concurrent or event-driven environments. * *

    Common use cases include: + * *

      - *
    • Running background operations
    • - *
    • Scheduling periodic or delayed actions
    • - *
    • Extending for custom task logic
    • + *
    • Running background operations + *
    • Scheduling periodic or delayed actions + *
    • Extending for custom task logic *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodic.java b/src/main/java/enedis/lab/util/task/TaskPeriodic.java index fea7315..29e0f52 100644 --- a/src/main/java/enedis/lab/util/task/TaskPeriodic.java +++ b/src/main/java/enedis/lab/util/task/TaskPeriodic.java @@ -13,14 +13,15 @@ /** * Abstract class representing a generic periodic asynchronous task. * - *

    This class provides functionality for executing tasks at regular intervals, - * typically used for background operations or scheduled actions. + *

    This class provides functionality for executing tasks at regular intervals, typically used for + * background operations or scheduled actions. * *

    Common use cases include: + * *

      - *
    • Running periodic background jobs
    • - *
    • Scheduling recurring actions
    • - *
    • Extending for custom periodic logic
    • + *
    • Running periodic background jobs + *
    • Scheduling recurring actions + *
    • Extending for custom periodic logic *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java index 2ea9711..b1d5e50 100644 --- a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java +++ b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java @@ -16,10 +16,11 @@ * registered subscribers of events or results. * *

    Common use cases include: + * *

      - *
    • Running periodic background jobs with notifications
    • - *
    • Scheduling recurring actions and informing listeners
    • - *
    • Extending for custom periodic and notification logic
    • + *
    • Running periodic background jobs with notifications + *
    • Scheduling recurring actions and informing listeners + *
    • Extending for custom periodic and notification logic *
    * * @param the subscriber type diff --git a/src/main/java/enedis/lab/util/time/Time.java b/src/main/java/enedis/lab/util/time/Time.java index 1a0feb4..d92b725 100644 --- a/src/main/java/enedis/lab/util/time/Time.java +++ b/src/main/java/enedis/lab/util/time/Time.java @@ -14,14 +14,16 @@ /** * Utility class providing time-related constants and helper methods. * - *

    This class offers constants for time units and static methods for sleeping, formatting timestamps, - * and decorating messages with timestamps. It is designed for general-purpose time management and formatting. + *

    This class offers constants for time units and static methods for sleeping, formatting + * timestamps, and decorating messages with timestamps. It is designed for general-purpose time + * management and formatting. * *

    Common use cases include: + * *

      - *
    • Sleeping for a specified duration
    • - *
    • Formatting timestamps as date/time strings
    • - *
    • Decorating log messages with current date/time
    • + *
    • Sleeping for a specified duration + *
    • Formatting timestamps as date/time strings + *
    • Decorating log messages with current date/time *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java index 94e7518..ef41414 100644 --- a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java +++ b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java @@ -10,14 +10,15 @@ /** * Subscriber implementation for receiving the next frame and error notifications. * - *

    This class listens for frame and error events, storing the latest received frame or error. - * It provides methods to access the received data and to clear its state. + *

    This class listens for frame and error events, storing the latest received frame or error. It + * provides methods to access the received data and to clear its state. * *

    Common use cases include: + * *

      - *
    • Receiving and processing the next available frame
    • - *
    • Handling error notifications
    • - *
    • Resetting subscriber state for reuse
    • + *
    • Receiving and processing the next available frame + *
    • Handling error notifications + *
    • Resetting subscriber state for reuse *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/tic/core/TICCore.java b/src/main/java/enedis/tic/core/TICCore.java index f8c1c70..c74fc1b 100644 --- a/src/main/java/enedis/tic/core/TICCore.java +++ b/src/main/java/enedis/tic/core/TICCore.java @@ -14,13 +14,15 @@ /** * Core interface for managing frame reading and subscriber notifications. * - *

    This interface defines methods for reading frames, managing subscribers, and retrieving modem information. + *

    This interface defines methods for reading frames, managing subscribers, and retrieving modem + * information. * *

    Common use cases include: + * *

      - *
    • Reading frames from available sources
    • - *
    • Managing and notifying subscribers
    • - *
    • Retrieving modem and identifier information
    • + *
    • Reading frames from available sources + *
    • Managing and notifying subscribers + *
    • Retrieving modem and identifier information *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/tic/core/TICCoreBase.java b/src/main/java/enedis/tic/core/TICCoreBase.java index 006a2a9..1ecd8f0 100644 --- a/src/main/java/enedis/tic/core/TICCoreBase.java +++ b/src/main/java/enedis/tic/core/TICCoreBase.java @@ -34,14 +34,15 @@ /** * Core implementation for managing frame reading and subscriber notifications. * - *

    This class provides mechanisms for reading frames, managing streams, handling subscribers, - * and notifying events. It implements the core contract for frame acquisition and event delivery. + *

    This class provides mechanisms for reading frames, managing streams, handling subscribers, and + * notifying events. It implements the core contract for frame acquisition and event delivery. * *

    Common use cases include: + * *

      - *
    • Reading frames from available sources
    • - *
    • Managing and notifying subscribers
    • - *
    • Handling stream lifecycle and modem events
    • + *
    • Reading frames from available sources + *
    • Managing and notifying subscribers + *
    • Handling stream lifecycle and modem events *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/tic/core/TICCoreError.java b/src/main/java/enedis/tic/core/TICCoreError.java index 8466e50..18c40f2 100644 --- a/src/main/java/enedis/tic/core/TICCoreError.java +++ b/src/main/java/enedis/tic/core/TICCoreError.java @@ -22,13 +22,15 @@ * Class representing a core error with identifier, code, message, and optional data. * *

    This class provides mechanisms for constructing, accessing, and managing error information - * including error codes, messages, and associated data. It is designed for general-purpose error handling. + * including error codes, messages, and associated data. It is designed for general-purpose error + * handling. * *

    Common use cases include: + * *

      - *
    • Representing errors with structured data
    • - *
    • Managing error codes and messages
    • - *
    • Associating additional data with errors
    • + *
    • Representing errors with structured data + *
    • Managing error codes and messages + *
    • Associating additional data with errors *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/tic/core/TICCoreErrorCode.java b/src/main/java/enedis/tic/core/TICCoreErrorCode.java index 287bbcd..c4baf36 100644 --- a/src/main/java/enedis/tic/core/TICCoreErrorCode.java +++ b/src/main/java/enedis/tic/core/TICCoreErrorCode.java @@ -14,10 +14,11 @@ * It provides a structured way to manage and interpret error states. * *

    Common use cases include: + * *

      - *
    • Representing error states in core logic
    • - *
    • Supporting diagnostics and error reporting
    • - *
    • Mapping error codes to messages
    • + *
    • Representing error states in core logic + *
    • Supporting diagnostics and error reporting + *
    • Mapping error codes to messages *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/tic/core/TICCoreFrame.java b/src/main/java/enedis/tic/core/TICCoreFrame.java index 0908a14..137d0d5 100644 --- a/src/main/java/enedis/tic/core/TICCoreFrame.java +++ b/src/main/java/enedis/tic/core/TICCoreFrame.java @@ -24,13 +24,15 @@ * Class representing a core frame with identifier, mode, capture time, and content. * *

    This class provides mechanisms for constructing, accessing, and managing frame information - * including identifiers, modes, timestamps, and associated content. It is designed for general-purpose frame handling. + * including identifiers, modes, timestamps, and associated content. It is designed for + * general-purpose frame handling. * *

    Common use cases include: + * *

      - *
    • Representing frames with structured data
    • - *
    • Managing frame identifiers and modes
    • - *
    • Associating content and timestamps with frames
    • + *
    • Representing frames with structured data + *
    • Managing frame identifiers and modes + *
    • Associating content and timestamps with frames *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/tic/core/TICCoreStream.java b/src/main/java/enedis/tic/core/TICCoreStream.java index 702049f..c9fa0b2 100644 --- a/src/main/java/enedis/tic/core/TICCoreStream.java +++ b/src/main/java/enedis/tic/core/TICCoreStream.java @@ -13,14 +13,15 @@ /** * Interface for managing core streams, frame acquisition, and subscriber notifications. * - *

    This interface defines methods for accessing stream identifiers and managing subscribers. - * It provides a contract for implementations that handle frame delivery and event notification. + *

    This interface defines methods for accessing stream identifiers and managing subscribers. It + * provides a contract for implementations that handle frame delivery and event notification. * *

    Common use cases include: + * *

      - *
    • Acquiring frames from streams
    • - *
    • Managing and notifying subscribers
    • - *
    • Supporting event-driven stream operations
    • + *
    • Acquiring frames from streams + *
    • Managing and notifying subscribers + *
    • Supporting event-driven stream operations *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/tic/core/TICCoreStreamBase.java b/src/main/java/enedis/tic/core/TICCoreStreamBase.java index 97435e2..43036db 100644 --- a/src/main/java/enedis/tic/core/TICCoreStreamBase.java +++ b/src/main/java/enedis/tic/core/TICCoreStreamBase.java @@ -39,14 +39,15 @@ /** * Core stream implementation for frame acquisition and subscriber notifications. * - *

    This class provides mechanisms for managing streams, acquiring frames, handling errors, - * and notifying subscribers. It implements the contract for event-driven stream operations. + *

    This class provides mechanisms for managing streams, acquiring frames, handling errors, and + * notifying subscribers. It implements the contract for event-driven stream operations. * *

    Common use cases include: + * *

      - *
    • Acquiring frames from streams
    • - *
    • Managing and notifying subscribers
    • - *
    • Handling errors and stream lifecycle
    • + *
    • Acquiring frames from streams + *
    • Managing and notifying subscribers + *
    • Handling errors and stream lifecycle *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/tic/core/TICCoreSubscriber.java b/src/main/java/enedis/tic/core/TICCoreSubscriber.java index 87f96bb..8d17de3 100644 --- a/src/main/java/enedis/tic/core/TICCoreSubscriber.java +++ b/src/main/java/enedis/tic/core/TICCoreSubscriber.java @@ -16,10 +16,11 @@ * for objects that handle event-driven notifications in core operations. * *

    Common use cases include: + * *

      - *
    • Receiving frame notifications
    • - *
    • Handling error events
    • - *
    • Implementing event-driven logic
    • + *
    • Receiving frame notifications + *
    • Handling error events + *
    • Implementing event-driven logic *
    * * @author Enedis Smarties team diff --git a/src/main/java/enedis/tic/core/TICIdentifier.java b/src/main/java/enedis/tic/core/TICIdentifier.java index 491c55c..8136e17 100644 --- a/src/main/java/enedis/tic/core/TICIdentifier.java +++ b/src/main/java/enedis/tic/core/TICIdentifier.java @@ -19,14 +19,16 @@ /** * Class representing a core identifier with port ID, port name, and serial number. * - *

    This class provides mechanisms for constructing, accessing, and managing identifier information - * including port IDs, port names, and serial numbers. It is designed for general-purpose identifier handling. + *

    This class provides mechanisms for constructing, accessing, and managing identifier + * information including port IDs, port names, and serial numbers. It is designed for + * general-purpose identifier handling. * *

    Common use cases include: + * *

      - *
    • Representing identifiers with structured data
    • - *
    • Matching identifiers for streams and devices
    • - *
    • Managing port and serial information
    • + *
    • Representing identifiers with structured data + *
    • Matching identifiers for streams and devices + *
    • Managing port and serial information *
    * * @author Enedis Smarties team From 24cf84cc0ac1896ad89a35577daee7697b633e6b Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Thu, 9 Oct 2025 16:50:20 +0200 Subject: [PATCH 30/59] chore: format enedis.tic.service.client package --- .../service/client/TIC2WebSocketClient.java | 59 ++++++++++++--- .../client/TIC2WebSocketClientPool.java | 63 +++++++++++----- .../client/TIC2WebSocketClientPoolBase.java | 72 +++++++++++++++++-- 3 files changed, 162 insertions(+), 32 deletions(-) diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java index e61724d..6af100c 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java @@ -20,17 +20,42 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -/** TIC2WebSocket client */ +/** + * WebSocket client for TIC2WebSocket service. + * + *

    This class implements {@link TICCoreSubscriber} to handle TIC data and error events, + * forwarding them to the WebSocket channel using the provided {@link EventSender}. + * + *

    Key features include: + * + *

      + *
    • Receives TIC data frames and error notifications + *
    • Forwards events to the associated Netty WebSocket channel + *
    • Handles event serialization and transmission + *
    + * + * @author Enedis Smarties team + * @see TICCoreSubscriber + * @see EventSender + * @see Channel + */ public class TIC2WebSocketClient implements TICCoreSubscriber { - private Logger logger; - private Channel channel; - private EventSender eventSender; + /** Logger for client events and errors. */ + private final Logger logger; + + /** Associated Netty WebSocket channel for communication. */ + private final Channel channel; + + /** Event sender for dispatching TIC events to the channel. */ + private final EventSender eventSender; /** - * Constructor + * Constructs a new TIC2WebSocketClient instance. + * + *

    Initializes the client with the specified WebSocket channel and event sender. * - * @param channel - * @param eventSender + * @param channel the Netty WebSocket channel for communication + * @param eventSender the event sender responsible for dispatching events */ public TIC2WebSocketClient(Channel channel, EventSender eventSender) { super(); @@ -39,6 +64,14 @@ public TIC2WebSocketClient(Channel channel, EventSender eventSender) { this.eventSender = eventSender; } + /** + * Handles incoming TIC data frames. + * + *

    This method is called when a new TIC data frame is received. It creates an {@link + * EventOnTICData} event and sends it to the WebSocket channel. + * + * @param frame the TIC data frame received + */ @Override public void onData(TICCoreFrame frame) { try { @@ -49,6 +82,14 @@ public void onData(TICCoreFrame frame) { } } + /** + * Handles TIC error notifications. + * + *

    This method is called when a TIC error occurs. It creates an {@link EventOnError} event and + * sends it to the WebSocket channel. + * + * @param error the TIC error detected + */ @Override public void onError(TICCoreError error) { try { @@ -60,9 +101,9 @@ public void onError(TICCoreError error) { } /** - * Get client websocket channel + * Returns the associated WebSocket channel for this client. * - * @return websocket channel + * @return the Netty WebSocket channel */ public Channel getChannel() { return this.channel; diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java index 15f2895..6d681db 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java @@ -11,37 +11,66 @@ import io.netty.channel.Channel; import java.util.Optional; -/** TIC2WebSocket client pool interface */ +/** + * Pool interface for managing TIC2WebSocketClient instances. + * + *

    This interface defines methods for creating, retrieving, checking, and removing {@link + * TIC2WebSocketClient} objects associated with Netty WebSocket channels. It provides centralized + * management of client lifecycles and lookup by channel identifier. + * + *

    Key features include: + * + *

      + *
    • Client creation and registration + *
    • Client lookup by channel ID + *
    • Existence checks for active clients + *
    • Client removal and cleanup + *
    + * + * @author Enedis Smarties team + * @see TIC2WebSocketClient + * @see Channel + * @see EventSender + */ public interface TIC2WebSocketClientPool { /** - * Get client from channel id + * Retrieves the client associated with the specified channel ID. * - * @param channelId - * @return client + *

    Returns an {@link Optional} containing the client if found, or empty if no client exists for + * the given channel ID. + * + * @param channelId the unique identifier of the WebSocket channel + * @return an Optional containing the client, or empty if not found */ - public Optional getClient(String channelId); + Optional getClient(String channelId); /** - * Check if a client with the given channel id exists + * Checks if a client exists for the specified channel ID. + * + *

    Returns true if a client is registered for the given channel ID, false otherwise. * - * @param channelId - * @return true if a client with the given channel id exists + * @param channelId the unique identifier of the WebSocket channel + * @return true if a client exists, false otherwise */ - public boolean exists(String channelId); + boolean exists(String channelId); /** - * Create a new client + * Creates and registers a new client for the specified channel and event sender. * - * @param channel - * @param sender - * @return new client + *

    Initializes a new {@link TIC2WebSocketClient} and associates it with the given channel. + * + * @param channel the Netty WebSocket channel for communication + * @param sender the event sender responsible for dispatching events + * @return the newly created client instance */ - public TIC2WebSocketClient createClient(Channel channel, EventSender sender); + TIC2WebSocketClient createClient(Channel channel, EventSender sender); /** - * Remove client with the given channel id + * Removes the client associated with the specified channel ID. + * + *

    Unregisters and cleans up the client for the given channel ID, if present. * - * @param channelId + * @param channelId the unique identifier of the WebSocket channel */ - public void remove(String channelId); + void remove(String channelId); } diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java index efcb142..ee30943 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java +++ b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java @@ -13,30 +13,74 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; -/** Client pool */ +/** + * Default implementation of the TIC2WebSocket client pool. + * + *

    This class manages a thread-safe set of {@link TIC2WebSocketClient} instances, providing + * creation, lookup, existence checks, and removal by channel ID. It implements the {@link + * TIC2WebSocketClientPool} interface for centralized client management in the WebSocket service. + * + *

    Key features include: + * + *

      + *
    • Thread-safe client storage using {@link CopyOnWriteArraySet} + *
    • Efficient lookup and registration by channel ID + *
    • Argument validation for client creation + *
    + * + * @author Enedis Smarties team + * @see TIC2WebSocketClientPool + * @see TIC2WebSocketClient + * @see Channel + * @see EventSender + */ public class TIC2WebSocketClientPoolBase implements TIC2WebSocketClientPool { - private Set clients; + /** Thread-safe set of registered clients. */ + private final Set clients; - /** Default constructor */ + /** Constructs a new client pool with an empty set of clients. */ public TIC2WebSocketClientPoolBase() { super(); - this.clients = new CopyOnWriteArraySet(); + this.clients = new CopyOnWriteArraySet<>(); } + /** + * Retrieves the client associated with the specified channel ID. + * + *

    Searches the pool for a client whose channel matches the given ID. + * + * @param channelId the unique identifier of the WebSocket channel + * @return an Optional containing the client, or empty if not found + */ @Override public Optional getClient(String channelId) { - // @formatter:off return this.clients.stream() .filter(c -> c.getChannel().id().asLongText().equals(channelId)) .findAny(); - // @formatter:on } + /** + * Checks if a client exists for the specified channel ID. + * + * @param channelId the unique identifier of the WebSocket channel + * @return true if a client exists, false otherwise + */ @Override public boolean exists(String channelId) { return this.getClient(channelId).isPresent(); } + /** + * Creates and registers a new client for the specified channel and event sender. + * + *

    If a client for the channel already exists, returns the existing client. Otherwise, creates + * a new client, registers it, and returns it. + * + * @param channel the Netty WebSocket channel for communication + * @param sender the event sender responsible for dispatching events + * @return the newly created or existing client instance + * @throws IllegalArgumentException if channel or sender is null + */ @Override public TIC2WebSocketClient createClient(Channel channel, EventSender sender) { this.checkArguments(channel, sender); @@ -52,6 +96,13 @@ public TIC2WebSocketClient createClient(Channel channel, EventSender sender) { } } + /** + * Removes the client associated with the specified channel ID. + * + *

    Unregisters and cleans up the client for the given channel ID, if present. + * + * @param channelId the unique identifier of the WebSocket channel + */ @Override public void remove(String channelId) { Optional client = this.getClient(channelId); @@ -60,6 +111,15 @@ public void remove(String channelId) { } } + /** + * Validates arguments for client creation. + * + *

    Throws an exception if either argument is null. + * + * @param channel the Netty WebSocket channel + * @param sender the event sender + * @throws IllegalArgumentException if channel or sender is null + */ private void checkArguments(Channel channel, EventSender sender) { if (channel == null || sender == null) { throw new IllegalArgumentException( From a380213e0a1370a6d80156da4d34331cec4ba33c Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Thu, 9 Oct 2025 16:59:06 +0200 Subject: [PATCH 31/59] chore: format enedis.tic.service.config package --- .../config/TIC2WebSocketConfiguration.java | 125 +++++++++++++----- 1 file changed, 94 insertions(+), 31 deletions(-) diff --git a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java index 9d7a13c..66a37ca 100644 --- a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java +++ b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java @@ -21,35 +21,68 @@ import java.util.Map; /** - * TIC2WebSocketConfiguration class + * Configuration class for TIC2WebSocket service. * - *

    Generated + *

    This class manages configuration parameters for the TIC2WebSocket server, including server + * port, TIC mode, and the list of TIC port names. It provides validation and conversion logic for + * each parameter, ensuring correct types and values. + * + *

    Key features include: + * + *

      + *
    • Validation of server port range and TIC port names + *
    • Support for multiple construction methods (map, DataDictionary, explicit values) + *
    • Integration with the configuration base and key descriptor system + *
    + * + * @author Enedis Smarties team + * @see ConfigurationBase + * @see TICMode */ public class TIC2WebSocketConfiguration extends ConfigurationBase { + /** Key for server port configuration. */ protected static final String KEY_SERVER_PORT = "serverPort"; + + /** Key for TIC mode configuration. */ protected static final String KEY_TIC_MODE = "ticMode"; + + /** Key for TIC port names configuration. */ protected static final String KEY_TIC_PORT_NAMES = "ticPortNames"; + /** Minimum allowed server port value. */ private static final Number SERVER_PORT_MIN = 1; + + /** Maximum allowed server port value. */ private static final Number SERVER_PORT_MAX = 65535; + + /** Minimum number of TIC port names required. */ private static final int TIC_PORT_NAMES_MIN_SIZE = 1; - private List> keys = new ArrayList>(); + /** List of key descriptors for configuration parameters. */ + private final List> keys = new ArrayList<>(); + /** Key descriptor for server port. */ protected KeyDescriptorNumberMinMax kServerPort; + + /** Key descriptor for TIC mode. */ protected KeyDescriptorEnum kTicMode; + + /** Key descriptor for TIC port names. */ protected KeyDescriptorListMinMaxSize kTicPortNames; + /** Constructs a configuration with default values and loads key descriptors. */ protected TIC2WebSocketConfiguration() { super(); this.loadKeyDescriptors(); } /** - * Constructor using map + * Constructs a configuration from a map of values. + * + *

    Initializes the configuration using a map of key-value pairs. * - * @param map - * @throws DataDictionaryException + * @param map the map containing configuration parameters + * @throws DataDictionaryException if validation fails */ public TIC2WebSocketConfiguration(Map map) throws DataDictionaryException { this(); @@ -57,10 +90,12 @@ public TIC2WebSocketConfiguration(Map map) throws DataDictionary } /** - * Constructor using datadictionary + * Constructs a configuration from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + *

    Copies configuration values from the provided DataDictionary. + * + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public TIC2WebSocketConfiguration(DataDictionary other) throws DataDictionaryException { this(); @@ -68,7 +103,7 @@ public TIC2WebSocketConfiguration(DataDictionary other) throws DataDictionaryExc } /** - * Constructor setting configuration name/file and parameters to default values + * Constructs a configuration with a specified name and file, using default parameter values. * * @param name the configuration name * @param file the configuration file @@ -79,12 +114,14 @@ public TIC2WebSocketConfiguration(String name, File file) { } /** - * Constructor setting parameters to specific values + * Constructs a configuration with explicit parameter values. + * + *

    Initializes the configuration with the specified server port, TIC mode, and TIC port names. * - * @param serverPort - * @param ticMode - * @param ticPortNames - * @throws DataDictionaryException + * @param serverPort the server port number + * @param ticMode the TIC mode + * @param ticPortNames the list of TIC port names + * @throws DataDictionaryException if validation fails */ public TIC2WebSocketConfiguration(Number serverPort, TICMode ticMode, List ticPortNames) throws DataDictionaryException { @@ -98,7 +135,7 @@ public TIC2WebSocketConfiguration(Number serverPort, TICMode ticMode, List getTicPortNames() { @@ -126,43 +163,63 @@ public List getTicPortNames() { } /** - * Set server port + * Sets the server port value. * - * @param serverPort - * @throws DataDictionaryException + * @param serverPort the server port number + * @throws DataDictionaryException if validation fails */ public void setServerPort(Number serverPort) throws DataDictionaryException { this.setServerPort((Object) serverPort); } /** - * Set tic mode + * Sets the TIC mode value. * - * @param ticMode - * @throws DataDictionaryException + * @param ticMode the TIC mode + * @throws DataDictionaryException if validation fails */ public void setTicMode(TICMode ticMode) throws DataDictionaryException { this.setTicMode((Object) ticMode); } /** - * Set tic port names + * Sets the list of TIC port names. * - * @param ticPortNames - * @throws DataDictionaryException + * @param ticPortNames the list of TIC port names + * @throws DataDictionaryException if validation fails */ public void setTicPortNames(List ticPortNames) throws DataDictionaryException { this.setTicPortNames((Object) ticPortNames); } + /** + * Internal setter for server port, with conversion and validation. + * + * @param serverPort the server port value (Object or Number) + * @throws DataDictionaryException if validation fails + */ protected void setServerPort(Object serverPort) throws DataDictionaryException { this.data.put(KEY_SERVER_PORT, this.kServerPort.convert(serverPort)); } + /** + * Internal setter for TIC mode, with conversion and validation. + * + * @param ticMode the TIC mode value (Object or TICMode) + * @throws DataDictionaryException if validation fails + */ protected void setTicMode(Object ticMode) throws DataDictionaryException { this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); } + /** + * Internal setter for TIC port names, with conversion and validation. + * + *

    Checks for null, empty, and duplicate port names. + * + * @param ticPortNames the list or object representing TIC port names + * @throws DataDictionaryException if validation fails + */ protected void setTicPortNames(Object ticPortNames) throws DataDictionaryException { List portNames = this.kTicPortNames.convert(ticPortNames); for (int i = 0; i < portNames.size(); i++) { @@ -201,17 +258,23 @@ protected void setTicPortNames(Object ticPortNames) throws DataDictionaryExcepti this.data.put(KEY_TIC_PORT_NAMES, portNames); } + /** + * Loads key descriptors for configuration parameters. + * + *

    Initializes descriptors for server port, TIC mode, and TIC port names, and adds them to the + * configuration. + */ private void loadKeyDescriptors() { try { this.kServerPort = new KeyDescriptorNumberMinMax(KEY_SERVER_PORT, true, SERVER_PORT_MIN, SERVER_PORT_MAX); this.keys.add(this.kServerPort); - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, false, TICMode.class); + this.kTicMode = new KeyDescriptorEnum<>(KEY_TIC_MODE, false, TICMode.class); this.keys.add(this.kTicMode); this.kTicPortNames = - new KeyDescriptorListMinMaxSize( + new KeyDescriptorListMinMaxSize<>( KEY_TIC_PORT_NAMES, false, String.class, TIC_PORT_NAMES_MIN_SIZE, null); this.keys.add(this.kTicPortNames); From 467882477881564e078842641ac189a6141663ba Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Thu, 9 Oct 2025 17:07:53 +0200 Subject: [PATCH 32/59] chore: format enedis.tic.service.endpoint package --- .../tic/service/endpoint/EventSender.java | 30 ++++++++-- .../TIC2WebSocketEndPointErrorCode.java | 56 +++++++++++++------ .../TIC2WebSocketEndPointException.java | 49 ++++++++++------ 3 files changed, 96 insertions(+), 39 deletions(-) diff --git a/src/main/java/enedis/tic/service/endpoint/EventSender.java b/src/main/java/enedis/tic/service/endpoint/EventSender.java index 6e10ed8..2fd05c8 100644 --- a/src/main/java/enedis/tic/service/endpoint/EventSender.java +++ b/src/main/java/enedis/tic/service/endpoint/EventSender.java @@ -10,13 +10,33 @@ import enedis.lab.util.message.Event; import io.netty.channel.Channel; -/** Event sender interface */ +/** + * Interface for sending event messages to a WebSocket channel. + * + *

    This interface defines the contract for dispatching {@link Event} objects to a specified Netty + * {@link Channel}. Implementations are responsible for serializing and transmitting event data over + * the channel. + * + *

    Key features include: + * + *

      + *
    • Abstracts event delivery to WebSocket clients + *
    • Supports custom event serialization and transmission strategies + *
    + * + * @author Enedis Smarties team + * @see Event + * @see Channel + */ public interface EventSender { /** - * Send event + * Sends an event message to the specified WebSocket channel. * - * @param channel - * @param event + *

    Implementations should serialize the event and transmit it to the client associated with the + * given channel. + * + * @param channel the Netty WebSocket channel to send the event to + * @param event the event message to be sent */ - public void sendEvent(Channel channel, Event event); + void sendEvent(Channel channel, Event event); } diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java index f6bc8f9..3933706 100644 --- a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java +++ b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java @@ -7,43 +7,65 @@ package enedis.tic.service.endpoint; -/** End point error codes list */ +/** + * Error codes for TIC2WebSocket endpoint operations. + * + *

    This enumeration defines error codes used to represent the result of endpoint operations, + * including message validation, subscription management, and internal errors. Each code is + * associated with a unique integer value for protocol communication. + * + *

    Key features include: + * + *

      + *
    • Success and error codes for message handling + *
    • Subscription and unsubscription error codes + *
    • Timeout and identifier errors + *
    + * + * @author Enedis Smarties team + */ public enum TIC2WebSocketEndPointErrorCode { - /** Success code */ + /** Operation completed successfully. */ NO_ERROR(0), - /** Unvalid message */ + /** Message format is invalid. */ INVALID_MESSAGE_FORMAT(-1), - /** Message type missing */ + /** Message type is missing. */ MESSAGE_TYPE_MISSING(-2), - /** Message name missing */ + /** Message name is missing. */ MESSAGE_NAME_MISSING(-3), - /** Message type invalid */ + /** Message type is invalid. */ MESSAGE_TYPE_INVALID(-4), - /** Unsupported message */ + /** Message is not supported. */ UNSUPPORTED_MESSAGE(-5), - /** Invalid messge content */ + /** Message content is invalid. */ INVALID_MESSAGE_CONTENT(-6), - /** Internal error */ + /** Internal server error occurred. */ INTERNAL_ERROR(-7), - /** Subscription fail */ + /** Subscription request failed. */ SUBSCRIPTION_FAIL(-10), - /** Unsubscription fail */ + /** Unsubscription request failed. */ UNSUBSCRIPTION_FAIL(-11), - /** TIC identifier not found */ + /** TIC identifier not found. */ IDENTIFIER_NOT_FOUND(-12), - /** Read TIC timeout */ + /** TIC read operation timed out. */ READ_TIMEOUT(-13); - private int value; + /** Integer value associated with the error code. */ + private final int value; - private TIC2WebSocketEndPointErrorCode(int value) { + /** + * Constructs an error code with the specified integer value. + * + * @param value the integer value for the error code + */ + TIC2WebSocketEndPointErrorCode(int value) { this.value = value; } /** - * Return error code + * Returns the integer value associated with this error code. * - * @return code + * @return the error code value */ public int value() { return this.value; diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java index 442fe10..5a68bb6 100644 --- a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java +++ b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java @@ -8,19 +8,34 @@ package enedis.tic.service.endpoint; /** - * @author Antoine + * Exception class for TIC2WebSocket endpoint errors. + * + *

    This exception encapsulates an error code from {@link TIC2WebSocketEndPointErrorCode}, + * providing detailed context for endpoint operation failures. It supports multiple constructors for + * message and cause chaining. + * + *

    Key features include: + * + *

      + *
    • Associates an error code with each exception + *
    • Supports message and cause chaining for diagnostics + *
    + * + * @author Enedis Smarties team + * @see TIC2WebSocketEndPointErrorCode */ -/** TIC2WebSocket end point Exception */ public class TIC2WebSocketEndPointException extends Exception { + /** Serial version UID for serialization. */ private static final long serialVersionUID = -2263755971102386572L; - private TIC2WebSocketEndPointErrorCode code; + /** Error code associated with this exception. */ + private final TIC2WebSocketEndPointErrorCode code; /** - * Default constructor + * Constructs an exception with the specified error code. * - * @param code + * @param code the error code */ public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code) { super(); @@ -28,11 +43,11 @@ public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code) { } /** - * Constructor using message and cause + * Constructs an exception with the specified error code, message, and cause. * - * @param code - * @param message - * @param cause + * @param code the error code + * @param message the detail message + * @param cause the cause of the exception */ public TIC2WebSocketEndPointException( TIC2WebSocketEndPointErrorCode code, String message, Throwable cause) { @@ -41,10 +56,10 @@ public TIC2WebSocketEndPointException( } /** - * Constructor using message + * Constructs an exception with the specified error code and message. * - * @param code - * @param message + * @param code the error code + * @param message the detail message */ public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, String message) { super(message); @@ -52,10 +67,10 @@ public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, Strin } /** - * Constructor using cause + * Constructs an exception with the specified error code and cause. * - * @param code - * @param cause + * @param code the error code + * @param cause the cause of the exception */ public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, Throwable cause) { super(cause); @@ -63,9 +78,9 @@ public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, Throw } /** - * Get error code + * Returns the error code associated with this exception. * - * @return error code + * @return the endpoint error code */ public TIC2WebSocketEndPointErrorCode getCode() { return this.code; From d5bfd7021895806a0802911538e0f474a3b04429 Mon Sep 17 00:00:00 2001 From: Mathieu SABARTHES Date: Thu, 9 Oct 2025 18:44:31 +0200 Subject: [PATCH 33/59] chore: format enedis.tic.service.message package --- .../tic/service/message/EventOnError.java | 72 +++++++++++++----- .../tic/service/message/EventOnTICData.java | 72 +++++++++++++----- .../message/RequestGetAvailableTICs.java | 41 ++++++---- .../service/message/RequestGetModemsInfo.java | 43 ++++++++--- .../tic/service/message/RequestReadTIC.java | 70 ++++++++++++----- .../service/message/RequestSubscribeTIC.java | 70 ++++++++++++----- .../message/RequestUnsubscribeTIC.java | 70 ++++++++++++----- .../message/ResponseGetAvailableTICs.java | 76 +++++++++++++------ .../message/ResponseGetModemsInfo.java | 76 +++++++++++++------ .../tic/service/message/ResponseReadTIC.java | 75 ++++++++++++------ .../service/message/ResponseSubscribeTIC.java | 55 +++++++++----- .../message/ResponseUnsubscribeTIC.java | 55 +++++++++----- .../factory/TIC2WebSocketEventFactory.java | 21 ++++- .../factory/TIC2WebSocketMessageFactory.java | 26 ++++++- .../factory/TIC2WebSocketRequestFactory.java | 25 +++++- .../factory/TIC2WebSocketResponseFactory.java | 25 +++++- 16 files changed, 633 insertions(+), 239 deletions(-) diff --git a/src/main/java/enedis/tic/service/message/EventOnError.java b/src/main/java/enedis/tic/service/message/EventOnError.java index 7c97d2e..b0364ba 100644 --- a/src/main/java/enedis/tic/service/message/EventOnError.java +++ b/src/main/java/enedis/tic/service/message/EventOnError.java @@ -19,32 +19,48 @@ import java.util.Map; /** - * EventOnError class + * Event message representing an error in the TIC2WebSocket protocol. * - *

    Generated + *

    This class encapsulates error information using a {@link TICCoreError} object and provides + * constructors for various initialization scenarios. It integrates with the event messaging system + * for error notification and handling. + * + *

    Key features include: + * + *

      + *
    • Encapsulates error data for protocol events + *
    • Supports construction from map, DataDictionary, or explicit values + *
    • Validates and manages error data using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Event + * @see TICCoreError */ public class EventOnError extends Event { + /** Key for error data in the event. */ protected static final String KEY_DATA = "data"; - /** Message name */ + /** Message name for error events. */ public static final String NAME = "OnError"; private List> keys = new ArrayList>(); + /** Key descriptor for error data. */ protected KeyDescriptorDataDictionary kData; + /** Constructs an error event with default values and loads key descriptors. */ protected EventOnError() { super(); this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); } /** - * Constructor using map + * Constructs an error event from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing event parameters + * @throws DataDictionaryException if validation fails */ public EventOnError(Map map) throws DataDictionaryException { this(); @@ -52,10 +68,10 @@ public EventOnError(Map map) throws DataDictionaryException { } /** - * Constructor using datadictionary + * Constructs an error event from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public EventOnError(DataDictionary other) throws DataDictionaryException { this(); @@ -63,21 +79,24 @@ public EventOnError(DataDictionary other) throws DataDictionaryException { } /** - * Constructor setting parameters to specific values + * Constructs an error event with explicit date/time and error data. * - * @param dateTime - * @param data - * @throws DataDictionaryException + * @param dateTime the event timestamp + * @param data the error data + * @throws DataDictionaryException if validation fails */ public EventOnError(LocalDateTime dateTime, TICCoreError data) throws DataDictionaryException { this(); - this.setDateTime(dateTime); this.setData(data); - this.checkAndUpdate(); } + /** + * Updates optional parameters for the event, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { @@ -87,28 +106,39 @@ protected void updateOptionalParameters() throws DataDictionaryException { } /** - * Get data + * Returns the error data associated with this event. * - * @return the data + * @return the error data */ public TICCoreError getData() { return (TICCoreError) this.data.get(KEY_DATA); } /** - * Set data + * Sets the error data for this event. * - * @param data - * @throws DataDictionaryException + * @param data the error data + * @throws DataDictionaryException if validation fails */ public void setData(TICCoreError data) throws DataDictionaryException { this.setData((Object) data); } + /** + * Internal setter for error data, with conversion and validation. + * + * @param data the error data (Object or TICCoreError) + * @throws DataDictionaryException if validation fails + */ protected void setData(Object data) throws DataDictionaryException { this.data.put(KEY_DATA, this.kData.convert(data)); } + /** + * Loads key descriptors for event parameters. + * + *

    Initializes the descriptor for error data and adds it to the event. + */ private void loadKeyDescriptors() { try { this.kData = diff --git a/src/main/java/enedis/tic/service/message/EventOnTICData.java b/src/main/java/enedis/tic/service/message/EventOnTICData.java index 52ed6e1..efe7f20 100644 --- a/src/main/java/enedis/tic/service/message/EventOnTICData.java +++ b/src/main/java/enedis/tic/service/message/EventOnTICData.java @@ -19,32 +19,48 @@ import java.util.Map; /** - * EventOnTICData class + * Event message representing TIC data in the TIC2WebSocket protocol. * - *

    Generated + *

    This class encapsulates TIC data using a {@link TICCoreFrame} object and provides constructors + * for various initialization scenarios. It integrates with the event messaging system for data + * notification and handling. + * + *

    Key features include: + * + *

      + *
    • Encapsulates TIC data for protocol events + *
    • Supports construction from map, DataDictionary, or explicit values + *
    • Validates and manages TIC data using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Event + * @see TICCoreFrame */ public class EventOnTICData extends Event { + /** Key for TIC data in the event. */ protected static final String KEY_DATA = "data"; - /** Message name */ + /** Message name for TIC data events. */ public static final String NAME = "OnTICData"; private List> keys = new ArrayList>(); + /** Key descriptor for TIC data. */ protected KeyDescriptorDataDictionary kData; + /** Constructs a TIC data event with default values and loads key descriptors. */ protected EventOnTICData() { super(); this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); } /** - * Constructor using map + * Constructs a TIC data event from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing event parameters + * @throws DataDictionaryException if validation fails */ public EventOnTICData(Map map) throws DataDictionaryException { this(); @@ -52,10 +68,10 @@ public EventOnTICData(Map map) throws DataDictionaryException { } /** - * Constructor using datadictionary + * Constructs a TIC data event from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public EventOnTICData(DataDictionary other) throws DataDictionaryException { this(); @@ -63,21 +79,24 @@ public EventOnTICData(DataDictionary other) throws DataDictionaryException { } /** - * Constructor setting parameters to specific values + * Constructs a TIC data event with explicit date/time and TIC data. * - * @param dateTime - * @param data - * @throws DataDictionaryException + * @param dateTime the event timestamp + * @param data the TIC data + * @throws DataDictionaryException if validation fails */ public EventOnTICData(LocalDateTime dateTime, TICCoreFrame data) throws DataDictionaryException { this(); - this.setDateTime(dateTime); this.setData(data); - this.checkAndUpdate(); } + /** + * Updates optional parameters for the event, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { @@ -87,28 +106,39 @@ protected void updateOptionalParameters() throws DataDictionaryException { } /** - * Get data + * Returns the TIC data associated with this event. * - * @return the data + * @return the TIC data */ public TICCoreFrame getData() { return (TICCoreFrame) this.data.get(KEY_DATA); } /** - * Set data + * Sets the TIC data for this event. * - * @param data - * @throws DataDictionaryException + * @param data the TIC data + * @throws DataDictionaryException if validation fails */ public void setData(TICCoreFrame data) throws DataDictionaryException { this.setData((Object) data); } + /** + * Internal setter for TIC data, with conversion and validation. + * + * @param data the TIC data (Object or TICCoreFrame) + * @throws DataDictionaryException if validation fails + */ protected void setData(Object data) throws DataDictionaryException { this.data.put(KEY_DATA, this.kData.convert(data)); } + /** + * Loads key descriptors for event parameters. + * + *

    Initializes the descriptor for TIC data and adds it to the event. + */ private void loadKeyDescriptors() { try { this.kData = diff --git a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java index 31e2754..4ddb217 100644 --- a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java +++ b/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java @@ -13,32 +13,42 @@ import java.util.Map; /** - * RequestGetAvailableTICs class + * Request message for retrieving available TICs. * - *

    Generated + *

    This class represents a request to obtain the list of available TICs. It provides constructors + * for various initialization scenarios and integrates with the request messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates request for available TICs + *
    • Supports construction from map, DataDictionary, or default values + *
    • Validates and manages request parameters + *
    + * + * @author Enedis Smarties team + * @see Request */ public class RequestGetAvailableTICs extends Request { - /** Message name */ + /** Message name for this request. */ public static final String NAME = "GetAvailableTICs"; /** - * Default constructor + * Constructs a request for available TICs with default values. * - * @throws DataDictionaryException + * @throws DataDictionaryException if validation fails */ public RequestGetAvailableTICs() throws DataDictionaryException { super(); - this.kName.setAcceptedValues(NAME); - this.checkAndUpdate(); } /** - * Constructor using map + * Constructs a request for available TICs from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing request parameters + * @throws DataDictionaryException if validation fails */ public RequestGetAvailableTICs(Map map) throws DataDictionaryException { this(); @@ -46,16 +56,21 @@ public RequestGetAvailableTICs(Map map) throws DataDictionaryExc } /** - * Constructor using datadictionary + * Constructs a request for available TICs from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public RequestGetAvailableTICs(DataDictionary other) throws DataDictionaryException { this(); this.copy(other); } + /** + * Updates optional parameters for the request, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { diff --git a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java b/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java index 18a4b26..0d435f3 100644 --- a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java +++ b/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java @@ -13,27 +13,43 @@ import java.util.Map; /** - * RequestGetModemsInfo class + * Request message for retrieving modem information. * - *

    Generated + *

    This class represents a request to obtain information about available modems. It provides + * constructors for various initialization scenarios and integrates with the request messaging + * system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates request for modem information + *
    • Supports construction from map, DataDictionary, or default values + *
    • Validates and manages request parameters + *
    + * + * @author Enedis Smarties team + * @see Request */ public class RequestGetModemsInfo extends Request { - /** Message name */ + /** Message name for this request. */ public static final String NAME = "GetModemsInfo"; + /** + * Constructs a request for modem information with default values. + * + * @throws DataDictionaryException if validation fails + */ public RequestGetModemsInfo() throws DataDictionaryException { super(); - this.kName.setAcceptedValues(NAME); - this.checkAndUpdate(); } /** - * Constructor using map + * Constructs a request for modem information from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing request parameters + * @throws DataDictionaryException if validation fails */ public RequestGetModemsInfo(Map map) throws DataDictionaryException { this(); @@ -41,16 +57,21 @@ public RequestGetModemsInfo(Map map) throws DataDictionaryExcept } /** - * Constructor using datadictionary + * Constructs a request for modem information from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public RequestGetModemsInfo(DataDictionary other) throws DataDictionaryException { this(); this.copy(other); } + /** + * Updates optional parameters for the request, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { diff --git a/src/main/java/enedis/tic/service/message/RequestReadTIC.java b/src/main/java/enedis/tic/service/message/RequestReadTIC.java index 45d2d00..7857888 100644 --- a/src/main/java/enedis/tic/service/message/RequestReadTIC.java +++ b/src/main/java/enedis/tic/service/message/RequestReadTIC.java @@ -18,32 +18,48 @@ import java.util.Map; /** - * RequestReadTIC class + * Request message for reading TIC data in the TIC2WebSocket protocol. * - *

    Generated + *

    This class represents a request to read TIC data for a specific identifier. It provides + * constructors for various initialization scenarios and integrates with the request messaging + * system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates request for reading TIC data + *
    • Supports construction from map, DataDictionary, or explicit identifier + *
    • Validates and manages request parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Request + * @see TICIdentifier */ public class RequestReadTIC extends Request { + /** Key for TIC identifier data in the request. */ protected static final String KEY_DATA = "data"; - /** Message name */ + /** Message name for this request. */ public static final String NAME = "ReadTIC"; private List> keys = new ArrayList>(); + /** Key descriptor for TIC identifier data. */ protected KeyDescriptorDataDictionary kData; + /** Constructs a request for reading TIC data with default values. */ protected RequestReadTIC() { super(); this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); } /** - * Constructor using map + * Constructs a request for reading TIC data from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing request parameters + * @throws DataDictionaryException if validation fails */ public RequestReadTIC(Map map) throws DataDictionaryException { this(); @@ -51,10 +67,10 @@ public RequestReadTIC(Map map) throws DataDictionaryException { } /** - * Constructor using datadictionary + * Constructs a request for reading TIC data from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public RequestReadTIC(DataDictionary other) throws DataDictionaryException { this(); @@ -62,19 +78,22 @@ public RequestReadTIC(DataDictionary other) throws DataDictionaryException { } /** - * Constructor setting parameters to specific values + * Constructs a request for reading TIC data with a specific identifier. * - * @param data - * @throws DataDictionaryException + * @param data the TIC identifier + * @throws DataDictionaryException if validation fails */ public RequestReadTIC(TICIdentifier data) throws DataDictionaryException { this(); - this.setData(data); - this.checkAndUpdate(); } + /** + * Updates optional parameters for the request, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { @@ -84,28 +103,39 @@ protected void updateOptionalParameters() throws DataDictionaryException { } /** - * Get data + * Returns the TIC identifier data associated with this request. * - * @return the data + * @return the TIC identifier */ public TICIdentifier getData() { return (TICIdentifier) this.data.get(KEY_DATA); } /** - * Set data + * Sets the TIC identifier data for this request. * - * @param data - * @throws DataDictionaryException + * @param data the TIC identifier + * @throws DataDictionaryException if validation fails */ public void setData(TICIdentifier data) throws DataDictionaryException { this.setData((Object) data); } + /** + * Internal setter for TIC identifier data, with conversion and validation. + * + * @param data the TIC identifier (Object or TICIdentifier) + * @throws DataDictionaryException if validation fails + */ protected void setData(Object data) throws DataDictionaryException { this.data.put(KEY_DATA, this.kData.convert(data)); } + /** + * Loads key descriptors for request parameters. + * + *

    Initializes the descriptor for TIC identifier data and adds it to the request. + */ private void loadKeyDescriptors() { try { this.kData = diff --git a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java index e5a5e08..d617edf 100644 --- a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java @@ -18,32 +18,48 @@ import java.util.Map; /** - * RequestSubscribeTIC class + * Request message for subscribing to TIC data. * - *

    Generated + *

    This class represents a request to subscribe to TIC data for one or more identifiers. It + * provides constructors for various initialization scenarios and integrates with the request + * messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates request for TIC data subscription + *
    • Supports construction from map, DataDictionary, or explicit identifier list + *
    • Validates and manages request parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Request + * @see TICIdentifier */ public class RequestSubscribeTIC extends Request { + /** Key for TIC identifier data in the request. */ protected static final String KEY_DATA = "data"; - /** Message name */ + /** Message name for this request. */ public static final String NAME = "SubscribeTIC"; private List> keys = new ArrayList>(); + /** Key descriptor for TIC identifier data. */ protected KeyDescriptorList kData; + /** Constructs a request for subscribing to TIC data with default values. */ protected RequestSubscribeTIC() { super(); this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); } /** - * Constructor using map + * Constructs a request for subscribing to TIC data from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing request parameters + * @throws DataDictionaryException if validation fails */ public RequestSubscribeTIC(Map map) throws DataDictionaryException { this(); @@ -51,10 +67,10 @@ public RequestSubscribeTIC(Map map) throws DataDictionaryExcepti } /** - * Constructor using datadictionary + * Constructs a request for subscribing to TIC data from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public RequestSubscribeTIC(DataDictionary other) throws DataDictionaryException { this(); @@ -62,19 +78,22 @@ public RequestSubscribeTIC(DataDictionary other) throws DataDictionaryException } /** - * Constructor setting parameters to specific values + * Constructs a request for subscribing to TIC data with a specific list of identifiers. * - * @param data - * @throws DataDictionaryException + * @param data the list of TIC identifiers + * @throws DataDictionaryException if validation fails */ public RequestSubscribeTIC(List data) throws DataDictionaryException { this(); - this.setData(data); - this.checkAndUpdate(); } + /** + * Updates optional parameters for the request, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { @@ -84,9 +103,9 @@ protected void updateOptionalParameters() throws DataDictionaryException { } /** - * Get data + * Returns the list of TIC identifier data associated with this request. * - * @return the data + * @return the list of TIC identifiers */ @SuppressWarnings("unchecked") public List getData() { @@ -94,19 +113,30 @@ public List getData() { } /** - * Set data + * Sets the list of TIC identifier data for this request. * - * @param data - * @throws DataDictionaryException + * @param data the list of TIC identifiers + * @throws DataDictionaryException if validation fails */ public void setData(List data) throws DataDictionaryException { this.setData((Object) data); } + /** + * Internal setter for TIC identifier data, with conversion and validation. + * + * @param data the TIC identifier list (Object or List) + * @throws DataDictionaryException if validation fails + */ protected void setData(Object data) throws DataDictionaryException { this.data.put(KEY_DATA, this.kData.convert(data)); } + /** + * Loads key descriptors for request parameters. + * + *

    Initializes the descriptor for TIC identifier data and adds it to the request. + */ private void loadKeyDescriptors() { try { this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); diff --git a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java index e15be86..b40665b 100644 --- a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java @@ -18,32 +18,48 @@ import java.util.Map; /** - * RequestUnsubscribeTIC class + * Request message for unsubscribing from TIC data in the TIC2WebSocket protocol. * - *

    Generated + *

    This class represents a request to unsubscribe from TIC data for one or more identifiers. It + * provides constructors for various initialization scenarios and integrates with the request + * messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates request for TIC data unsubscription + *
    • Supports construction from map, DataDictionary, or explicit identifier list + *
    • Validates and manages request parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Request + * @see TICIdentifier */ public class RequestUnsubscribeTIC extends Request { + /** Key for TIC identifier data in the request. */ protected static final String KEY_DATA = "data"; - /** Message name */ + /** Message name for this request. */ public static final String NAME = "UnsubscribeTIC"; private List> keys = new ArrayList>(); + /** Key descriptor for TIC identifier data. */ protected KeyDescriptorList kData; + /** Constructs a request for unsubscribing from TIC data with default values. */ protected RequestUnsubscribeTIC() { super(); this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); } /** - * Constructor using map + * Constructs a request for unsubscribing from TIC data from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing request parameters + * @throws DataDictionaryException if validation fails */ public RequestUnsubscribeTIC(Map map) throws DataDictionaryException { this(); @@ -51,10 +67,10 @@ public RequestUnsubscribeTIC(Map map) throws DataDictionaryExcep } /** - * Constructor using datadictionary + * Constructs a request for unsubscribing from TIC data from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public RequestUnsubscribeTIC(DataDictionary other) throws DataDictionaryException { this(); @@ -62,19 +78,22 @@ public RequestUnsubscribeTIC(DataDictionary other) throws DataDictionaryExceptio } /** - * Constructor setting parameters to specific values + * Constructs a request for unsubscribing from TIC data with a specific list of identifiers. * - * @param data - * @throws DataDictionaryException + * @param data the list of TIC identifiers + * @throws DataDictionaryException if validation fails */ public RequestUnsubscribeTIC(List data) throws DataDictionaryException { this(); - this.setData(data); - this.checkAndUpdate(); } + /** + * Updates optional parameters for the request, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { @@ -84,9 +103,9 @@ protected void updateOptionalParameters() throws DataDictionaryException { } /** - * Get data + * Returns the list of TIC identifier data associated with this request. * - * @return the data + * @return the list of TIC identifiers */ @SuppressWarnings("unchecked") public List getData() { @@ -94,19 +113,30 @@ public List getData() { } /** - * Set data + * Sets the list of TIC identifier data for this request. * - * @param data - * @throws DataDictionaryException + * @param data the list of TIC identifiers + * @throws DataDictionaryException if validation fails */ public void setData(List data) throws DataDictionaryException { this.setData((Object) data); } + /** + * Internal setter for TIC identifier data, with conversion and validation. + * + * @param data the TIC identifier list (Object or List) + * @throws DataDictionaryException if validation fails + */ protected void setData(Object data) throws DataDictionaryException { this.data.put(KEY_DATA, this.kData.convert(data)); } + /** + * Loads key descriptors for request parameters. + * + *

    Initializes the descriptor for TIC identifier data and adds it to the request. + */ private void loadKeyDescriptors() { try { this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); diff --git a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java index 963ca4a..18a1480 100644 --- a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java +++ b/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java @@ -19,32 +19,48 @@ import java.util.Map; /** - * ResponseGetAvailableTICs class + * Response message for available TIC identifiers. * - *

    Generated + *

    This class represents a response containing the list of available TIC identifiers. It provides + * constructors for various initialization scenarios and integrates with the response messaging + * system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates response for available TIC identifiers + *
    • Supports construction from map, DataDictionary, or explicit parameter list + *
    • Validates and manages response parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Response + * @see TICIdentifier */ public class ResponseGetAvailableTICs extends Response { + /** Key for TIC identifier data in the response. */ protected static final String KEY_DATA = "data"; - /** Message name */ + /** Message name for this response. */ public static final String NAME = "GetAvailableTICs"; private List> keys = new ArrayList>(); + /** Key descriptor for TIC identifier data. */ protected KeyDescriptorList kData; + /** Constructs a response for available TIC identifiers with default values. */ protected ResponseGetAvailableTICs() { super(); this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); } /** - * Constructor using map + * Constructs a response for available TIC identifiers from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing response parameters + * @throws DataDictionaryException if validation fails */ public ResponseGetAvailableTICs(Map map) throws DataDictionaryException { this(); @@ -52,10 +68,10 @@ public ResponseGetAvailableTICs(Map map) throws DataDictionaryEx } /** - * Constructor using datadictionary + * Constructs a response for available TIC identifiers from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public ResponseGetAvailableTICs(DataDictionary other) throws DataDictionaryException { this(); @@ -63,27 +79,30 @@ public ResponseGetAvailableTICs(DataDictionary other) throws DataDictionaryExcep } /** - * Constructor setting parameters to specific values + * Constructs a response for available TIC identifiers with explicit parameters. * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + * @param data the list of available TIC identifiers + * @throws DataDictionaryException if validation fails */ public ResponseGetAvailableTICs( LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException { this(); - this.setDateTime(dateTime); this.setErrorCode(errorCode); this.setErrorMessage(errorMessage); this.setData(data); - this.checkAndUpdate(); } + /** + * Updates optional parameters for the response, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { @@ -93,9 +112,9 @@ protected void updateOptionalParameters() throws DataDictionaryException { } /** - * Get data + * Returns the list of TIC identifier data associated with this response. * - * @return the data + * @return the list of TIC identifiers */ @SuppressWarnings("unchecked") public List getData() { @@ -103,19 +122,30 @@ public List getData() { } /** - * Set data + * Sets the list of TIC identifier data for this response. * - * @param data - * @throws DataDictionaryException + * @param data the list of TIC identifiers + * @throws DataDictionaryException if validation fails */ public void setData(List data) throws DataDictionaryException { this.setData((Object) data); } + /** + * Internal setter for TIC identifier data, with conversion and validation. + * + * @param data the TIC identifier list (Object or List) + * @throws DataDictionaryException if validation fails + */ protected void setData(Object data) throws DataDictionaryException { this.data.put(KEY_DATA, this.kData.convert(data)); } + /** + * Loads key descriptors for response parameters. + * + *

    Initializes the descriptor for TIC identifier data and adds it to the response. + */ private void loadKeyDescriptors() { try { this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); diff --git a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java b/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java index dea46f5..b04258b 100644 --- a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java +++ b/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java @@ -19,32 +19,48 @@ import java.util.Map; /** - * ResponseGetModemsInfo class + * Response message for modem information. * - *

    Generated + *

    This class represents a response containing the list of modem port descriptors. It provides + * constructors for various initialization scenarios and integrates with the response messaging + * system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates response for modem information + *
    • Supports construction from map, DataDictionary, or explicit parameter list + *
    • Validates and manages response parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Response + * @see TICPortDescriptor */ public class ResponseGetModemsInfo extends Response { + /** Key for modem port descriptor data in the response. */ protected static final String KEY_DATA = "data"; - /** Message name */ + /** Message name for this response. */ public static final String NAME = "GetModemsInfo"; private List> keys = new ArrayList>(); + /** Key descriptor for modem port descriptor data. */ protected KeyDescriptorList kData; + /** Constructs a response for modem information with default values. */ protected ResponseGetModemsInfo() { super(); this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); } /** - * Constructor using map + * Constructs a response for modem information from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing response parameters + * @throws DataDictionaryException if validation fails */ public ResponseGetModemsInfo(Map map) throws DataDictionaryException { this(); @@ -52,10 +68,10 @@ public ResponseGetModemsInfo(Map map) throws DataDictionaryExcep } /** - * Constructor using datadictionary + * Constructs a response for modem information from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public ResponseGetModemsInfo(DataDictionary other) throws DataDictionaryException { this(); @@ -63,27 +79,30 @@ public ResponseGetModemsInfo(DataDictionary other) throws DataDictionaryExceptio } /** - * Constructor setting parameters to specific values + * Constructs a response for modem information with explicit parameters. * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + * @param data the list of modem port descriptors + * @throws DataDictionaryException if validation fails */ public ResponseGetModemsInfo( LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException { this(); - this.setDateTime(dateTime); this.setErrorCode(errorCode); this.setErrorMessage(errorMessage); this.setData(data); - this.checkAndUpdate(); } + /** + * Updates optional parameters for the response, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { @@ -93,9 +112,9 @@ protected void updateOptionalParameters() throws DataDictionaryException { } /** - * Get data + * Returns the list of modem port descriptor data associated with this response. * - * @return the data + * @return the list of modem port descriptors */ @SuppressWarnings("unchecked") public List getData() { @@ -103,19 +122,30 @@ public List getData() { } /** - * Set data + * Sets the list of modem port descriptor data for this response. * - * @param data - * @throws DataDictionaryException + * @param data the list of modem port descriptors + * @throws DataDictionaryException if validation fails */ public void setData(List data) throws DataDictionaryException { this.setData((Object) data); } + /** + * Internal setter for modem port descriptor data, with conversion and validation. + * + * @param data the modem port descriptor list (Object or List) + * @throws DataDictionaryException if validation fails + */ protected void setData(Object data) throws DataDictionaryException { this.data.put(KEY_DATA, this.kData.convert(data)); } + /** + * Loads key descriptors for response parameters. + * + *

    Initializes the descriptor for modem port descriptor data and adds it to the response. + */ private void loadKeyDescriptors() { try { this.kData = diff --git a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java b/src/main/java/enedis/tic/service/message/ResponseReadTIC.java index 326f75c..4f2a5c2 100644 --- a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java +++ b/src/main/java/enedis/tic/service/message/ResponseReadTIC.java @@ -19,32 +19,47 @@ import java.util.Map; /** - * ResponseReadTIC class + * Response message for TIC frame data. * - *

    Generated + *

    This class represents a response containing a TIC frame. It provides constructors for various + * initialization scenarios and integrates with the response messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates response for TIC frame data + *
    • Supports construction from map, DataDictionary, or explicit parameter list + *
    • Validates and manages response parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Response + * @see TICCoreFrame */ public class ResponseReadTIC extends Response { + /** Key for TIC frame data in the response. */ protected static final String KEY_DATA = "data"; - /** Message name */ + /** Message name for this response. */ public static final String NAME = "ReadTIC"; private List> keys = new ArrayList>(); + /** Key descriptor for TIC frame data. */ protected KeyDescriptorDataDictionary kData; + /** Constructs a response for TIC frame data with default values. */ protected ResponseReadTIC() { super(); this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); } /** - * Constructor using map + * Constructs a response for TIC frame data from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing response parameters + * @throws DataDictionaryException if validation fails */ public ResponseReadTIC(Map map) throws DataDictionaryException { this(); @@ -52,10 +67,10 @@ public ResponseReadTIC(Map map) throws DataDictionaryException { } /** - * Constructor using datadictionary + * Constructs a response for TIC frame data from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public ResponseReadTIC(DataDictionary other) throws DataDictionaryException { this(); @@ -63,27 +78,30 @@ public ResponseReadTIC(DataDictionary other) throws DataDictionaryException { } /** - * Constructor setting parameters to specific values + * Constructs a response for TIC frame data with explicit parameters. * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + * @param data the TIC frame data + * @throws DataDictionaryException if validation fails */ public ResponseReadTIC( LocalDateTime dateTime, Number errorCode, String errorMessage, TICCoreFrame data) throws DataDictionaryException { this(); - this.setDateTime(dateTime); this.setErrorCode(errorCode); this.setErrorMessage(errorMessage); this.setData(data); - this.checkAndUpdate(); } + /** + * Updates optional parameters for the response, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { @@ -93,28 +111,39 @@ protected void updateOptionalParameters() throws DataDictionaryException { } /** - * Get data + * Returns the TIC frame data associated with this response. * - * @return the data + * @return the TIC frame data */ public TICCoreFrame getData() { return (TICCoreFrame) this.data.get(KEY_DATA); } /** - * Set data + * Sets the TIC frame data for this response. * - * @param data - * @throws DataDictionaryException + * @param data the TIC frame data + * @throws DataDictionaryException if validation fails */ public void setData(TICCoreFrame data) throws DataDictionaryException { this.setData((Object) data); } + /** + * Internal setter for TIC frame data, with conversion and validation. + * + * @param data the TIC frame data (Object or TICCoreFrame) + * @throws DataDictionaryException if validation fails + */ protected void setData(Object data) throws DataDictionaryException { this.data.put(KEY_DATA, this.kData.convert(data)); } + /** + * Loads key descriptors for response parameters. + * + *

    Initializes the descriptor for TIC frame data and adds it to the response. + */ private void loadKeyDescriptors() { try { this.kData = diff --git a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java index 93946ab..391e617 100644 --- a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java @@ -17,28 +17,40 @@ import java.util.Map; /** - * ResponseSubscribeTIC class + * Response message for TIC subscription. * - *

    Generated + *

    This class represents a response to a TIC subscription request. It provides constructors for + * various initialization scenarios and integrates with the response messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates response for TIC subscription + *
    • Supports construction from map, DataDictionary, or explicit parameter list + *
    • Validates and manages response parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Response */ public class ResponseSubscribeTIC extends Response { - /** Message name */ + /** Message name for this response. */ public static final String NAME = "SubscribeTIC"; private List> keys = new ArrayList>(); + /** Constructs a response for TIC subscription with default values. */ protected ResponseSubscribeTIC() { super(); this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); } /** - * Constructor using map + * Constructs a response for TIC subscription from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing response parameters + * @throws DataDictionaryException if validation fails */ public ResponseSubscribeTIC(Map map) throws DataDictionaryException { this(); @@ -46,10 +58,10 @@ public ResponseSubscribeTIC(Map map) throws DataDictionaryExcept } /** - * Constructor using datadictionary + * Constructs a response for TIC subscription from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public ResponseSubscribeTIC(DataDictionary other) throws DataDictionaryException { this(); @@ -57,24 +69,27 @@ public ResponseSubscribeTIC(DataDictionary other) throws DataDictionaryException } /** - * Constructor setting parameters to specific values + * Constructs a response for TIC subscription with explicit parameters. * - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + * @throws DataDictionaryException if validation fails */ public ResponseSubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException { this(); - this.setDateTime(dateTime); this.setErrorCode(errorCode); this.setErrorMessage(errorMessage); - this.checkAndUpdate(); } + /** + * Updates optional parameters for the response, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { @@ -83,9 +98,13 @@ protected void updateOptionalParameters() throws DataDictionaryException { super.updateOptionalParameters(); } + /** + * Loads key descriptors for response parameters. + * + *

    Initializes the descriptors and adds them to the response. + */ private void loadKeyDescriptors() { try { - this.addAllKeyDescriptor(this.keys); } catch (DataDictionaryException e) { throw new RuntimeException(e.getMessage(), e); diff --git a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java index 9516f8a..be29ca3 100644 --- a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java +++ b/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java @@ -17,28 +17,40 @@ import java.util.Map; /** - * ResponseUnsubscribeTIC class + * Response message for TIC unsubscription. * - *

    Generated + *

    This class represents a response to a TIC unsubscription request. It provides constructors for + * various initialization scenarios and integrates with the response messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates response for TIC unsubscription + *
    • Supports construction from map, DataDictionary, or explicit parameter list + *
    • Validates and manages response parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Response */ public class ResponseUnsubscribeTIC extends Response { - /** Message name */ + /** Message name for this response. */ public static final String NAME = "UnsubscribeTIC"; private List> keys = new ArrayList>(); + /** Constructs a response for TIC unsubscription with default values. */ protected ResponseUnsubscribeTIC() { super(); this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); } /** - * Constructor using map + * Constructs a response for TIC unsubscription from a map of values. * - * @param map - * @throws DataDictionaryException + * @param map the map containing response parameters + * @throws DataDictionaryException if validation fails */ public ResponseUnsubscribeTIC(Map map) throws DataDictionaryException { this(); @@ -46,10 +58,10 @@ public ResponseUnsubscribeTIC(Map map) throws DataDictionaryExce } /** - * Constructor using datadictionary + * Constructs a response for TIC unsubscription from another DataDictionary instance. * - * @param other - * @throws DataDictionaryException + * @param other the DataDictionary to copy from + * @throws DataDictionaryException if validation fails */ public ResponseUnsubscribeTIC(DataDictionary other) throws DataDictionaryException { this(); @@ -57,24 +69,27 @@ public ResponseUnsubscribeTIC(DataDictionary other) throws DataDictionaryExcepti } /** - * Constructor setting parameters to specific values + * Constructs a response for TIC unsubscription with explicit parameters. * - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + * @throws DataDictionaryException if validation fails */ public ResponseUnsubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException { this(); - this.setDateTime(dateTime); this.setErrorCode(errorCode); this.setErrorMessage(errorMessage); - this.checkAndUpdate(); } + /** + * Updates optional parameters for the response, ensuring the name is set. + * + * @throws DataDictionaryException if validation fails + */ @Override protected void updateOptionalParameters() throws DataDictionaryException { if (!this.exists(KEY_NAME)) { @@ -83,9 +98,13 @@ protected void updateOptionalParameters() throws DataDictionaryException { super.updateOptionalParameters(); } + /** + * Loads key descriptors for response parameters. + * + *

    Initializes the descriptors and adds them to the response. + */ private void loadKeyDescriptors() { try { - this.addAllKeyDescriptor(this.keys); } catch (DataDictionaryException e) { throw new RuntimeException(e.getMessage(), e); diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java index 75f460c..5b618bc 100644 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java @@ -11,9 +11,26 @@ import enedis.tic.service.message.EventOnError; import enedis.tic.service.message.EventOnTICData; -/** TIC2WebSocket request factory */ +/** + * Factory for creating TIC2WebSocket event messages. + * + *

    This class extends {@link EventFactory} to register and instantiate event message types + * specific to the TIC2WebSocket protocol, including TIC data and error events. + * + *

    Key features include: + * + *

      + *
    • Registers {@link EventOnTICData} and {@link EventOnError} message classes + *
    • Supports dynamic event creation by message name + *
    + * + * @author Enedis Smarties team + * @see EventFactory + * @see EventOnTICData + * @see EventOnError + */ public class TIC2WebSocketEventFactory extends EventFactory { - /** Default constructor */ + /** Constructs a new event factory and registers TIC2WebSocket event message types. */ public TIC2WebSocketEventFactory() { super(); this.addMessageClass(EventOnTICData.NAME, EventOnTICData.class); diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java index 31c639f..5d583af 100644 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java @@ -9,9 +9,31 @@ import enedis.lab.util.message.factory.MessageFactory; -/** TIC2WebSocket message factory */ +/** + * Factory for creating TIC2WebSocket protocol messages. + * + *

    This class extends {@link MessageFactory} to register and instantiate request, response, and + * event message types specific to the TIC2WebSocket protocol. It aggregates the protocol's request, + * response, and event factories for unified message creation. + * + *

    Key features include: + * + *

      + *
    • Centralized registration of request, response, and event factories + *
    • Supports dynamic message creation for all protocol message types + *
    + * + * @author Enedis Smarties team + * @see MessageFactory + * @see TIC2WebSocketRequestFactory + * @see TIC2WebSocketResponseFactory + * @see TIC2WebSocketEventFactory + */ public class TIC2WebSocketMessageFactory extends MessageFactory { - /** Default constructor */ + /** + * Constructs a new message factory and registers TIC2WebSocket request, response, and event + * factories. + */ public TIC2WebSocketMessageFactory() { super( new TIC2WebSocketRequestFactory(), diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java index dc56b3f..059cf26 100644 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java @@ -14,9 +14,30 @@ import enedis.tic.service.message.RequestSubscribeTIC; import enedis.tic.service.message.RequestUnsubscribeTIC; -/** TIC2WebSocket request factory */ +/** + * Factory for creating TIC2WebSocket request messages. + * + *

    This class extends {@link RequestFactory} to register and instantiate request message types + * specific to the TIC2WebSocket protocol, including requests for TICs, modems, subscriptions, and + * reads. + * + *

    Key features include: + * + *

      + *
    • Registers all supported TIC2WebSocket request message classes + *
    • Supports dynamic request creation by message name + *
    + * + * @author Enedis Smarties team + * @see RequestFactory + * @see RequestGetAvailableTICs + * @see RequestGetModemsInfo + * @see RequestReadTIC + * @see RequestSubscribeTIC + * @see RequestUnsubscribeTIC + */ public class TIC2WebSocketRequestFactory extends RequestFactory { - /** Default constructor */ + /** Constructs a new request factory and registers TIC2WebSocket request message types. */ public TIC2WebSocketRequestFactory() { super(); this.addMessageClass(RequestGetAvailableTICs.NAME, RequestGetAvailableTICs.class); diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java index 0ae7b41..ea26094 100644 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java +++ b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java @@ -14,9 +14,30 @@ import enedis.tic.service.message.ResponseSubscribeTIC; import enedis.tic.service.message.ResponseUnsubscribeTIC; -/** TIC2WebSocket request factory */ +/** + * Factory for creating TIC2WebSocket response messages. + * + *

    This class extends {@link ResponseFactory} to register and instantiate response message types + * specific to the TIC2WebSocket protocol, including responses for TICs, modems, subscriptions, and + * reads. + * + *

    Key features include: + * + *

      + *
    • Registers all supported TIC2WebSocket response message classes + *
    • Supports dynamic response creation by message name + *
    + * + * @author Enedis Smarties team + * @see ResponseFactory + * @see ResponseGetAvailableTICs + * @see ResponseGetModemsInfo + * @see ResponseReadTIC + * @see ResponseSubscribeTIC + * @see ResponseUnsubscribeTIC + */ public class TIC2WebSocketResponseFactory extends ResponseFactory { - /** Default constructor */ + /** Constructs a new response factory and registers TIC2WebSocket response message types. */ public TIC2WebSocketResponseFactory() { super(); this.addMessageClass(ResponseGetAvailableTICs.NAME, ResponseGetAvailableTICs.class); From e4912218059b6235d4fb368be8109ebf0aca7050 Mon Sep 17 00:00:00 2001 From: MathieuSabarthes Date: Tue, 14 Oct 2025 15:23:21 +0200 Subject: [PATCH 34/59] chore: format tic.service package --- .../tic/service/TIC2WebSocketApplication.java | 84 ++++++++++--- .../TIC2WebSocketApplicationErrorCode.java | 40 ++++-- .../tic/service/TIC2WebSocketCommandLine.java | 85 +++++++++---- .../TIC2WebSocketChannelInitializer.java | 24 +++- .../service/netty/TIC2WebSocketHandler.java | 116 +++++++++++++++++- .../service/netty/TIC2WebSocketServer.java | 57 +++++++-- .../TIC2WebSocketRequestHandler.java | 32 ++++- .../TIC2WebSocketRequestHandlerBase.java | 95 ++++++++++++-- 8 files changed, 461 insertions(+), 72 deletions(-) diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java b/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java index df68bd5..df0dfe8 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java +++ b/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java @@ -27,9 +27,31 @@ import picocli.CommandLine.IVersionProvider; import picocli.CommandLine.ParseResult; -/** Class used to handle the application */ +/** + * Main application class for TIC2WebSocket. + * + *

    This class manages the lifecycle of the TIC2WebSocket application, including initialization, + * configuration loading, command-line parsing, server startup and shutdown, and error handling. + * It integrates with the core TIC logic, client pool, request handler, and Netty server to provide + * a complete WebSocket-based interface for TIC data exchange. + * + *

    Responsibilities include: + *

      + *
    • Parsing command-line arguments and loading configuration + *
    • Starting and stopping the TIC2WebSocket server and core + *
    • Managing application state and error codes + *
    • Handling logging configuration and shutdown hooks + *
    • Providing entry point and main execution loop + *
    + * + * @author Enedis Smarties team + * @see TIC2WebSocketServer + * @see TIC2WebSocketClientPool + * @see TIC2WebSocketRequestHandler + * @see TICCore + */ public class TIC2WebSocketApplication { - /** Project properties (see file project.properties) */ + /** Project properties loaded from TIC2WebSocket.properties. */ public static final Properties PROJECT_PROPERTIES = new Properties(); static { @@ -50,20 +72,22 @@ public class TIC2WebSocketApplication { } } - /** Project name ("project_name" from project.properties) */ + /** Project name ("project_name" from project.properties). */ public static final String NAME = PROJECT_PROPERTIES.getProperty("project_name", ""); - /** Project version ("project_version" from project.properties) */ + /** Project version ("project_version" from project.properties). */ public static final String VERSION = PROJECT_PROPERTIES.getProperty("project_version", ""); - /** Project description ("project_description" from project.properties) */ + /** Project description ("project_description" from project.properties). */ public static final String DESCRIPTION = PROJECT_PROPERTIES.getProperty("project_description", ""); /** - * Program entry point + * Program entry point for TIC2WebSocket. * - * @param args Command line arguments + *

    Initializes and runs the application, handling any startup errors. + * + * @param args command line arguments */ public static void main(String[] args) { try { @@ -89,9 +113,9 @@ public static void main(String[] args) { private TICCore ticCore; /** - * Application constructor + * Constructs a new TIC2WebSocketApplication instance. * - * @param args Command line arguments + * @param args command line arguments */ public TIC2WebSocketApplication(String[] args) { this.commandLineArgs = args; @@ -116,10 +140,12 @@ public String[] getVersion() throws Exception { } /** - * Application execution + * Executes the application main loop. + * + *

    Initializes, starts, and manages the application lifecycle, including shutdown handling. * * @return 0 if success, else an error code - * @throws InterruptedException If the application thread gets interrupted + * @throws InterruptedException if the application thread is interrupted * @see TIC2WebSocketApplicationErrorCode */ public int run() throws InterruptedException { @@ -151,7 +177,9 @@ public void run() { } /** - * Application initialization + * Initializes the application and its components. + * + *

    Parses command-line arguments, loads configuration, and prepares core components. * * @return 0 if success, else an error code * @see TIC2WebSocketApplicationErrorCode @@ -193,7 +221,9 @@ public int init() { } /** - * Application start-up + * Starts the application and its main services. + * + *

    Starts the TIC core and WebSocket server, and logs startup events. * * @return 0 if success, else an error code */ @@ -221,7 +251,7 @@ public int start() { } /** - * Application start-up indicator + * Indicates whether the application is currently started. * * @return true if the application is started, false otherwise */ @@ -230,7 +260,9 @@ public boolean isStarted() { } /** - * Application stop + * Stops the application and releases resources. + * + *

    Stops the WebSocket server and TIC core, and logs shutdown events. * * @return 0 if success, else an error code * @see TIC2WebSocketApplicationErrorCode @@ -255,6 +287,13 @@ public int stop() { return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); } + /** + * Parses command-line arguments and validates them. + * + *

    Logs errors for invalid arguments and returns appropriate error codes. + * + * @return 0 if success, else an error code + */ private int parseCommandLine() { try { ParseResult parseResult = this.commandLineParser.parseArgs(this.commandLineArgs); @@ -274,6 +313,14 @@ private int parseCommandLine() { return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); } + /** + * Updates the logger configuration based on the specified log level. + * + *

    Reconfigures Log4j and sets the application logger. + * + * @param level the log level to use + * @return 0 if success, else an error code + */ private int updateLoggerConfiguration(Level level) { try { System.setProperty( @@ -290,6 +337,13 @@ private int updateLoggerConfiguration(Level level) { return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); } + /** + * Loads the application configuration from file. + * + *

    Reads the configuration file and initializes the configuration object. + * + * @return 0 if success, else an error code + */ private int loadConfiguration() { String configFile = null; diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java b/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java index a93b70d..882f4c1 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java +++ b/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java @@ -7,31 +7,53 @@ package enedis.tic.service; -/** Application error codes list */ +/** + * Enumeration of application error codes for TIC2WebSocket. + * + *

    This enum defines error codes used throughout the TIC2WebSocket application to indicate + * specific error conditions, such as invalid command line arguments, configuration failures, + * and application state errors. Each error code is associated with an integer value for + * consistent error reporting and handling. + * + *

    Common use cases include: + *

      + *
    • Reporting application initialization and runtime errors + *
    • Standardizing error codes for logging and responses + *
    • Facilitating error handling in the main application logic + *
    + * + * @author Enedis Smarties team + */ public enum TIC2WebSocketApplicationErrorCode { - /** Success code */ + /** No error (success). */ NO_ERROR(0), - /** Command line invalid code */ + /** Command line is invalid. */ COMMAND_LINE_INVALID(1), - /** Update logger configuration code */ + /** Logger configuration update failure. */ UPDATE_LOGGER_CONFIGURATION_FAILURE(2), - /** Application already started code */ + /** Application is already started. */ APPLICATION_ALREADY_STARTED(3), - /** Application not started code */ + /** Application is not started. */ APPLICATION_NOT_STARTED(4), - /** Load configuration failure code */ + /** Configuration loading failure. */ LOAD_CONFIGURATION_FAILURE(5); + /** Integer value of the error code. */ private int code; + /** + * Constructs an error code enum value. + * + * @param code the integer value of the error code + */ private TIC2WebSocketApplicationErrorCode(int code) { this.code = code; } /** - * Return error code + * Returns the integer value of the error code. * - * @return code + * @return the error code value */ public int code() { return this.code; diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java b/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java index 66a8cfe..160b4b2 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java +++ b/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java @@ -13,12 +13,42 @@ import picocli.CommandLine.Option; import picocli.CommandLine.TypeConversionException; -/** Class used for command line */ +/** + * Command line options and converters for TIC2WebSocket. + * + *

    This class defines the command line arguments, options, and type converters used to configure + * and launch the TIC2WebSocket application. It leverages Picocli annotations for option parsing + * and provides a custom converter for verbosity levels. + * + *

    Responsibilities include: + *

      + *
    • Defining supported command line options (help, version, verbosity, config file) + *
    • Providing a converter for log verbosity levels + *
    • Storing parsed option values for use in application initialization + *
    + * + * @author Enedis Smarties team + */ @Command() public class TIC2WebSocketCommandLine { + /** + * Picocli type converter for log verbosity levels. + * + *

    Converts integer or string values to Log4j {@link Level} instances for controlling + * application logging verbosity. + */ static class VerboseLevelConverter implements ITypeConverter { - @Override - public Level convert(String value) throws Exception { + /** + * Converts a string value to a Log4j {@link Level}. + * + *

    Accepts both integer and string representations of log levels. + * + * @param value the string value to convert + * @return the corresponding Log4j Level + * @throws Exception if conversion fails + */ + @Override + public Level convert(String value) throws Exception { try { switch (Integer.parseInt(value)) { case 0: @@ -48,40 +78,51 @@ public Level convert(String value) throws Exception { } } + /** + * Option to display help information. + */ @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display help") + names = {"-h", "--help"}, + usageHelp = true, + description = "Display help") boolean helpRequested = false; + /** + * Option to display version information. + */ @Option( - names = {"--version"}, - versionHelp = true, - description = "Display version") + names = {"--version"}, + versionHelp = true, + description = "Display version") boolean versionRequested = false; // @formatter:off + /** + * Option to set the log verbosity level. + */ @Option( - names = {"-v", "--verbose"}, - defaultValue = "2", - paramLabel = "LEVEL", - description = - "Define verbosity level:\n" - + "0 ou OFF = muted\n" - + "1 ou FATAL = critical errors\n" - + "2 ou ERROR = error (default)\n" - + "3 ou WARN = warnings\n" - + "4 ou INFO = informations\n" - + "5 ou DEBUG = debugging\n" - + "6 ou TRACE = traces\n") + names = {"-v", "--verbose"}, + defaultValue = "2", + paramLabel = "LEVEL", + description = + "Define verbosity level:\n" + + "0 ou OFF = muted\n" + + "1 ou FATAL = critical errors\n" + + "2 ou ERROR = error (default)\n" + + "3 ou WARN = warnings\n" + + "4 ou INFO = informations\n" + + "5 ou DEBUG = debugging\n" + + "6 ou TRACE = traces\n") Level verboseLevel; // @formatter:on + /** + * Option to specify the configuration file path. + */ @Option( names = {"--configFile"}, paramLabel = "PATH", description = "Set configuration file") String configFile = null; } -; diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java index 98c28af..150ca83 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java @@ -18,7 +18,29 @@ import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; import io.netty.handler.stream.ChunkedWriteHandler; -/** Channel initializer for TIC2WebSocket server */ +/** + * Netty channel initializer for configuring the WebSocket server pipeline. + * + *

    This class sets up the Netty channel pipeline for incoming socket connections, adding HTTP and WebSocket + * handlers, compression, chunked writing, and custom request handling. It is responsible for preparing each + * channel to handle WebSocket communication and routing requests to the appropriate handler. + * + *

    Main responsibilities: + *

      + *
    • Configure HTTP and WebSocket protocol handlers
    • + *
    • Enable compression and chunked writing for large messages
    • + *
    • Route requests to the custom request handler
    • + *
    • Manage client pool for active connections
    • + *
    + * + *

    Typical usage: + *

      + *
    • Instantiate with a client pool and request handler
    • + *
    • Attach to a Netty server bootstrap for WebSocket support
    • + *
    + * + * @author Enedis Smarties team + */ public class TIC2WebSocketChannelInitializer extends ChannelInitializer { private static final String WEBSOCKET_PATH = "/"; diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java index 7873292..dc622eb 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java @@ -36,20 +36,46 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -/** Netty WebSocket handler for TIC2WebSocket */ +/** + * Netty WebSocket handler for TIC2WebSocket. + * + *

    This handler manages WebSocket connections for TIC2WebSocket, handling incoming WebSocket frames, + * client lifecycle events, and message dispatching. It integrates with the client pool and request handler + * to process requests and send responses or events over the WebSocket channel. + * + *

    Responsibilities include: + *

      + *
    • Managing client connections and their lifecycle + *
    • Parsing and validating incoming WebSocket messages + *
    • Handling requests and generating responses + *
    • Sending events and messages to clients + *
    • Logging and error handling for channel operations + *
    + * + *

    This class is intended to be used as a Netty handler in the server pipeline for real-time TIC data exchange. + * + * @author Enedis Smarties team + * @see TIC2WebSocketClientPool + * @see TIC2WebSocketRequestHandler + * @see EventSender + */ public class TIC2WebSocketHandler extends SimpleChannelInboundHandler implements EventSender { + /** Logger for this handler. */ private static final Logger logger = LogManager.getLogger(TIC2WebSocketHandler.class); + /** Pool managing active WebSocket clients. */ private final TIC2WebSocketClientPool clientPool; + /** Handler for processing incoming requests. */ private final TIC2WebSocketRequestHandler requestHandler; + /** Factory for creating and parsing TIC2WebSocket messages. */ private final TIC2WebSocketMessageFactory factory; /** - * Constructor + * Constructs a new TIC2WebSocketHandler. * - * @param clientPool the client pool - * @param requestHandler the request handler + * @param clientPool the pool managing WebSocket clients + * @param requestHandler the handler for processing requests */ public TIC2WebSocketHandler( TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) { @@ -58,11 +84,25 @@ public TIC2WebSocketHandler( this.factory = new TIC2WebSocketMessageFactory(); } + /** + * Sends an event message to the specified channel. + * + * @param channel the Netty channel to send the event to + * @param event the event message to send + */ @Override public void sendEvent(Channel channel, Event event) { this.sendMessage(channel, event); } + /** + * Invoked when a new channel becomes active. + * + *

    Creates a new client for the channel if it does not already exist. + * + * @param ctx the channel handler context + * @throws Exception if an error occurs during activation + */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); @@ -80,6 +120,14 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); } + /** + * Invoked when a channel becomes inactive. + * + *

    Handles client removal and sends an unsubscribe request if necessary. + * + * @param ctx the channel handler context + * @throws Exception if an error occurs during deactivation + */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); @@ -105,6 +153,15 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); } + /** + * Handles exceptions caught during channel operations. + * + *

    Logs the error and closes the channel. + * + * @param ctx the channel handler context + * @param cause the exception that was caught + * @throws Exception if an error occurs during exception handling + */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { Channel channel = ctx.channel(); @@ -115,6 +172,16 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E ctx.close(); } + /** + * Handles incoming WebSocket frames. + * + *

    Processes text frames as TIC2WebSocket messages, validates and dispatches requests, + * and sends responses. Unsupported frame types are logged as warnings. + * + * @param ctx the channel handler context + * @param frame the received WebSocket frame + * @throws Exception if an error occurs during frame processing + */ @Override protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception { if (frame instanceof TextWebSocketFrame) { @@ -146,6 +213,14 @@ protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) thr } } + /** + * Retrieves the TIC2WebSocketClient associated with the given channel. + * + *

    If no client exists for the channel, a new one is created and registered. + * + * @param channel the Netty channel + * @return the associated TIC2WebSocketClient + */ private TIC2WebSocketClient getClient(Channel channel) { String channelId = channel.id().asLongText(); Optional clientOpt = clientPool.getClient(channelId); @@ -155,6 +230,15 @@ private TIC2WebSocketClient getClient(Channel channel) { return clientOpt.get(); } + /** + * Parses and validates a message from the given text. + * + *

    Returns an empty Optional if the message is invalid or an error occurs. + * + * @param channel the Netty channel + * @param text the raw message text + * @return an Optional containing the parsed Message, or empty if invalid + */ private Optional getMessage(Channel channel, String text) { Message message = null; TIC2WebSocketEndPointErrorCode errorCode = TIC2WebSocketEndPointErrorCode.NO_ERROR; @@ -191,6 +275,15 @@ private Optional getMessage(Channel channel, String text) { return Optional.of(message); } + /** + * Extracts a Request from the given Message if possible. + * + *

    Returns an empty Optional if the message is not a Request. + * + * @param channel the Netty channel + * @param message the parsed Message + * @return an Optional containing the Request, or empty if not applicable + */ private Optional getRequest(Channel channel, Message message) { if (!(message instanceof Request)) { logger.error("Message is not a request"); @@ -201,10 +294,25 @@ private Optional getRequest(Channel channel, Message message) { return Optional.of((Request) message); } + /** + * Handles the given request using the request handler and client. + * + * @param client the TIC2WebSocketClient + * @param request the request to handle + * @return the generated Response + */ private Response handleRequest(TIC2WebSocketClient client, Request request) { return requestHandler.handle(request, client); } + /** + * Sends a message to the specified channel as a JSON WebSocket frame. + * + *

    Logs the sent message and handles any errors during transmission. + * + * @param channel the Netty channel to send the message to + * @param message the message to send + */ private void sendMessage(Channel channel, Message message) { try { String json = message.toJSON().toString(); diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java index 9bb4b0b..c6410c1 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java +++ b/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java @@ -20,26 +20,56 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -/** TIC2WebSocket Netty WebSocket Server */ +/** + * TIC2WebSocket Netty WebSocket Server. + * + *

    This class manages the lifecycle of the WebSocket server, including startup, + * shutdown, and connection handling. It configures the Netty pipeline and integrates with the client pool + * and request handler to support real-time TIC data exchange over WebSocket. + * + *

    Responsibilities include: + *

      + *
    • Starting and stopping the WebSocket server + *
    • Managing server channel and event loop groups + *
    • Binding to the specified host and port + *
    • Integrating with TIC2WebSocket client pool and request handler + *
    • Logging server lifecycle events and errors + *
    + * + *

    This class is intended to be used as the main entry point for launching the TIC2WebSocket server. + * + * @author Enedis Smarties team + * @see TIC2WebSocketClientPool + * @see TIC2WebSocketRequestHandler + * @see TIC2WebSocketChannelInitializer + */ public class TIC2WebSocketServer { + /** Logger for server lifecycle and events. */ private static final Logger logger = LogManager.getLogger(TIC2WebSocketServer.class); + /** Host address to bind the server. */ private final String host; + /** Port to bind the server. */ private final int port; + /** Pool managing active WebSocket clients. */ private final TIC2WebSocketClientPool clientPool; + /** Handler for processing incoming requests. */ private final TIC2WebSocketRequestHandler requestHandler; + /** Netty boss event loop group (accepts connections). */ private EventLoopGroup bossGroup; + /** Netty worker event loop group (handles traffic). */ private EventLoopGroup workerGroup; + /** Main server channel. */ private Channel serverChannel; /** - * Constructor + * Constructs a new TIC2WebSocketServer. * - * @param host the host to bind to - * @param port the port to bind to - * @param clientPool the client pool - * @param requestHandler the request handler + * @param host the host address to bind the server + * @param port the port to bind the server + * @param clientPool the pool managing WebSocket clients + * @param requestHandler the handler for processing requests */ public TIC2WebSocketServer( String host, @@ -53,7 +83,10 @@ public TIC2WebSocketServer( } /** - * Start the WebSocket server + * Starts the WebSocket server and binds to the configured host and port. + * + *

    Initializes Netty event loop groups and configures the server pipeline. Logs startup events and + * handles errors during server initialization. * * @throws Exception if server startup fails */ @@ -82,7 +115,11 @@ public void start() throws Exception { } } - /** Stop the WebSocket server */ + /** + * Stops the WebSocket server and releases resources. + * + *

    Closes the server channel and gracefully shuts down event loop groups. Logs shutdown events. + */ public void stop() { logger.info("Stopping TIC2WebSocket Netty server"); @@ -106,7 +143,9 @@ public void stop() { } /** - * Wait for the server to close + * Waits for the server channel to close. + * + *

    Blocks until the server channel is closed, allowing for graceful shutdown. * * @throws InterruptedException if interrupted while waiting */ diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java index 8a1dbd1..c69bbd8 100644 --- a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java +++ b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java @@ -11,14 +11,36 @@ import enedis.lab.util.message.Response; import enedis.tic.service.client.TIC2WebSocketClient; -/** TIC2WebSocket request handler interface */ +/** + * Interface for handling TIC2WebSocket requests. + * + *

    This interface defines the contract for processing incoming TIC2WebSocket requests and generating + * appropriate responses. Implementations of this interface are responsible for interpreting requests, + * performing necessary operations, and returning a response to the client. + * + *

    Common use cases include: + *

      + *
    • Validating and dispatching TIC2WebSocket requests + *
    • Managing client-specific operations + *
    • Generating responses based on request type and client state + *
    • Error handling and reporting + *
    + * + * @author Enedis Smarties team + * @see Request + * @see Response + * @see TIC2WebSocketClient + */ public interface TIC2WebSocketRequestHandler { /** - * Handle all TIC2WebSocket request + * Handles a TIC2WebSocket request and returns a response. * - * @param request - * @param client - * @return request response + *

    Processes the given request for the specified client, performing any necessary operations + * and returning the corresponding response. + * + * @param request the TIC2WebSocket request to handle + * @param client the client associated with the request + * @return the response generated for the request */ public Response handle(Request request, TIC2WebSocketClient client); } diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java index 761c1cd..461e8f1 100644 --- a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java +++ b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java @@ -36,15 +36,37 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -/** TIC2WebSocket client pool interface */ +/** + * Base implementation of the TIC2WebSocket request handler. + * + *

    This class provides the default logic for handling TIC2WebSocket requests, including subscription, + * unsubscription, reading TIC frames, and retrieving available TICs and modem information. It integrates + * with the TICCore to perform operations and generate appropriate responses for each request type. + * + *

    Responsibilities include: + *

      + *
    • Dispatching and processing supported TIC2WebSocket requests + *
    • Managing client subscriptions and unsubscriptions + *
    • Interfacing with TICCore for data operations + *
    • Generating error responses for unsupported or failed operations + *
    • Logging request handling and errors + *
    + * + * @author Enedis Smarties team + * @see TIC2WebSocketRequestHandler + * @see TIC2WebSocketClient + * @see TICCore + */ public class TIC2WebSocketRequestHandlerBase implements TIC2WebSocketRequestHandler { + /** Logger for request handling and errors. */ private Logger logger; + /** TICCore instance for data operations. */ private TICCore ticCore; /** - * Default constructor + * Constructs a new TIC2WebSocketRequestHandlerBase. * - * @param ticCore + * @param ticCore the TICCore instance for data operations */ public TIC2WebSocketRequestHandlerBase(TICCore ticCore) { super(); @@ -52,6 +74,16 @@ public TIC2WebSocketRequestHandlerBase(TICCore ticCore) { this.ticCore = ticCore; } + /** + * Handles a TIC2WebSocket request and returns a response. + * + *

    Dispatches the request to the appropriate handler method based on its type. Generates error + * responses for unsupported requests. + * + * @param request the TIC2WebSocket request to handle + * @param client the client associated with the request + * @return the response generated for the request + */ @Override public Response handle(Request request, TIC2WebSocketClient client) { Response response = null; @@ -85,6 +117,12 @@ public Response handle(Request request, TIC2WebSocketClient client) { return response; } + /** + * Handles a request to get available TIC identifiers. + * + * @param request the request to process + * @return the response containing available TIC identifiers or an error + */ private Response handleGetAvailableTICsRequest(RequestGetAvailableTICs request) { List ticIdentifiers = this.ticCore.getAvailableTICs(); @@ -106,6 +144,12 @@ private Response handleGetAvailableTICsRequest(RequestGetAvailableTICs request) return response; } + /** + * Handles a request to get modem information. + * + * @param request the request to process + * @return the response containing modem information or an error + */ private Response handleGetModemsInfoRequest(RequestGetModemsInfo request) { List modemsInfo = this.ticCore.getModemsInfo(); @@ -127,6 +171,12 @@ private Response handleGetModemsInfoRequest(RequestGetModemsInfo request) { return response; } + /** + * Handles a request to read a TIC frame. + * + * @param request the request to process + * @return the response containing the TIC frame or an error + */ private Response handleReadTICRequest(RequestReadTIC request) { Response response = null; try { @@ -160,8 +210,15 @@ private Response handleReadTICRequest(RequestReadTIC request) { return response; } + /** + * Handles a request to subscribe to TIC identifiers for a client. + * + * @param request the subscription request + * @param client the client to subscribe + * @return the response indicating subscription success or failure + */ private Response handleSubscribeTICRequest( - RequestSubscribeTIC request, TIC2WebSocketClient client) { + RequestSubscribeTIC request, TIC2WebSocketClient client) { Response response = null; Optional> ticIdentifiers = Optional.ofNullable(request.getData()); @@ -202,8 +259,17 @@ private Response handleSubscribeTICRequest( return response; } + /** + * Determines new TIC subscriptions requested by the client. + * + *

    Compares current subscriptions with requested identifiers and returns only new subscriptions. + * + * @param currentSubscriptions the client's current subscriptions + * @param askedTicIdentifiers the requested TIC identifiers + * @return a list of new TIC identifiers to subscribe + */ private List getNewSubcriptions( - List currentSubscriptions, List askedTicIdentifiers) { + List currentSubscriptions, List askedTicIdentifiers) { List newSubscriptions = new ArrayList(); for (TICIdentifier askedTicIdentifier : askedTicIdentifiers) { @@ -221,8 +287,15 @@ private List getNewSubcriptions( return newSubscriptions; } + /** + * Handles a request to unsubscribe from TIC identifiers for a client. + * + * @param request the unsubscription request + * @param client the client to unsubscribe + * @return the response indicating unsubscription success or failure + */ private Response handleUnsubscribeTICRequest( - RequestUnsubscribeTIC request, TIC2WebSocketClient client) { + RequestUnsubscribeTIC request, TIC2WebSocketClient client) { Response response = null; Optional> ticIdentifiers = Optional.ofNullable(request.getData()); @@ -259,8 +332,16 @@ private Response handleUnsubscribeTICRequest( return response; } + /** + * Creates a generic error response for a given request name and error code. + * + * @param name the name of the request + * @param code the error code + * @param message the error message + * @return the error response + */ private Response createErrorResponse( - String name, TIC2WebSocketEndPointErrorCode code, String message) { + String name, TIC2WebSocketEndPointErrorCode code, String message) { try { return new ResponseBase(name, LocalDateTime.now(), code.value(), message, null); } catch (DataDictionaryException e) { From 70e35ac2d4c13efcaa98e5fb4057f26a598f1d81 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:13:27 +0200 Subject: [PATCH 35/59] fix: type declaration for kTicMode --- .../enedis/tic/service/config/TIC2WebSocketConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java index 66a37ca..05ca25d 100644 --- a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java +++ b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java @@ -270,7 +270,7 @@ private void loadKeyDescriptors() { new KeyDescriptorNumberMinMax(KEY_SERVER_PORT, true, SERVER_PORT_MIN, SERVER_PORT_MAX); this.keys.add(this.kServerPort); - this.kTicMode = new KeyDescriptorEnum<>(KEY_TIC_MODE, false, TICMode.class); + this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, false, TICMode.class); this.keys.add(this.kTicMode); this.kTicPortNames = From ffbc4c949925bf6d99f232dd09862781521e9d9c Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:10:31 +0100 Subject: [PATCH 36/59] =?UTF-8?q?refactor(architecture):=20v2.0.0=20?= =?UTF-8?q?=E2=80=93=20nouvelle=20architecture=20+=20outils=20de=20diagnos?= =?UTF-8?q?tic=20+=20ws=20tester?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refonte globale de l’architecture pour simplifier la base de code. * Migration des anciens packages enedis.* vers tic.* (core / frame / stream / io / service / util) avec nettoyage des anciennes implémentations. * Ajout/standardisation des codecs (JSON + encodage/décodage de trames) * Ajout d’outils de diagnostic en ligne de commande (core, modem, serial, USB) + enrichissement des tests unitaires. * Ajout d’une page HTML de test WebSocket et amélioration de la conformité REUSE / doc. * Support MacOS pour les ports série --- .github/workflows/reuse.yml | 5 +- .gitignore | 4 +- CHANGELOG.fr.md | 10 + CHANGELOG.md | 10 + LICENSES/CC0-1.0.txt | 121 -- README.fr.md | 15 +- README.md | 15 +- REUSE.toml | 20 + docs/assets/enedis-logo.svg | 38 + docs/assets/fonts/Enedis-Black.woff | Bin 0 -> 20012 bytes docs/assets/fonts/Enedis-Black.woff2 | Bin 0 -> 15584 bytes docs/assets/fonts/Enedis-Bold.woff | Bin 0 -> 20200 bytes docs/assets/fonts/Enedis-Bold.woff2 | Bin 0 -> 15596 bytes docs/assets/fonts/Enedis-Light.woff | Bin 0 -> 19904 bytes docs/assets/fonts/Enedis-Light.woff2 | Bin 0 -> 15332 bytes docs/assets/fonts/Enedis-Medium.woff | Bin 0 -> 20188 bytes docs/assets/fonts/Enedis-Medium.woff2 | Bin 0 -> 15736 bytes docs/assets/fonts/Enedis-Regular.woff | Bin 0 -> 20392 bytes docs/assets/fonts/Enedis-Regular.woff2 | Bin 0 -> 15828 bytes docs/assets/fonts/Enedis-Thin.woff | Bin 0 -> 19856 bytes docs/assets/fonts/Enedis-Thin.woff2 | Bin 0 -> 15264 bytes docs/ws-tester.html | 1179 ++++++++++++ pom.xml | 22 +- .../config/TIC2WebSocketConfiguration.json | 29 - .../TIC2WebSocketConfiguration.json.license | 6 - .../message/EventOnError.json | 26 - .../message/EventOnError.json.license | 6 - .../message/EventOnTICData.json | 26 - .../message/EventOnTICData.json.license | 6 - .../message/RequestGetAvailableTICs.json | 14 - .../RequestGetAvailableTICs.json.license | 6 - .../message/RequestGetModemsInfo.json | 14 - .../message/RequestGetModemsInfo.json.license | 6 - .../message/RequestReadTIC.json | 22 - .../message/RequestReadTIC.json.license | 6 - .../message/RequestSubscribeTIC.json | 22 - .../message/RequestSubscribeTIC.json.license | 6 - .../message/RequestUnsubscribeTIC.json | 22 - .../RequestUnsubscribeTIC.json.license | 6 - .../message/ResponseGetAvailableTICs.json | 34 - .../ResponseGetAvailableTICs.json.license | 6 - .../message/ResponseGetModemsInfo.json | 34 - .../ResponseGetModemsInfo.json.license | 6 - .../message/ResponseReadTIC.json | 34 - .../message/ResponseReadTIC.json.license | 6 - .../message/ResponseSubscribeTIC.json | 26 - .../message/ResponseSubscribeTIC.json.license | 6 - .../message/ResponseUnsubscribeTIC.json | 26 - .../ResponseUnsubscribeTIC.json.license | 6 - .../tic/core/TICCoreError.json | 30 - .../tic/core/TICCoreError.json.license | 6 - .../tic/core/TICCoreFrame.json | 30 - .../tic/core/TICCoreFrame.json.license | 6 - .../tic/core/TICIdentifier.json | 25 - .../tic/core/TICIdentifier.json.license | 6 - src/main/java/enedis/lab/codec/Codec.java | 36 - .../java/enedis/lab/codec/CodecException.java | 110 -- .../java/enedis/lab/io/channels/Channel.java | 132 -- .../enedis/lab/io/channels/ChannelBase.java | 124 -- .../lab/io/channels/ChannelConfiguration.java | 333 ---- .../lab/io/channels/ChannelDirection.java | 49 - .../lab/io/channels/ChannelException.java | 227 --- .../lab/io/channels/ChannelListener.java | 101 -- .../lab/io/channels/ChannelPhysical.java | 213 --- .../lab/io/channels/ChannelProtocol.java | 59 - .../enedis/lab/io/channels/ChannelStatus.java | 37 - .../ChannelSerialPortConfiguration.java | 532 ------ .../lab/io/channels/serialport/Parity.java | 64 - .../lab/io/datastreams/DataInputStream.java | 75 - .../enedis/lab/io/datastreams/DataStream.java | 64 - .../lab/io/datastreams/DataStreamBase.java | 386 ---- .../datastreams/DataStreamConfiguration.java | 303 ---- .../io/datastreams/DataStreamDirection.java | 39 - .../io/datastreams/DataStreamException.java | 183 -- .../io/datastreams/DataStreamListener.java | 87 - .../lab/io/datastreams/DataStreamStatus.java | 49 - .../lab/io/datastreams/DataStreamType.java | 60 - .../lab/io/datastreams/DataStreamUser.java | 113 -- .../io/serialport/SerialPortDescriptor.java | 441 ----- .../enedis/lab/io/tic/TICPortDescriptor.java | 219 --- .../java/enedis/lab/io/tic/TICPortFinder.java | 65 - .../enedis/lab/io/tic/TICPortFinderBase.java | 122 -- .../lab/io/tic/TICPortPlugNotifier.java | 67 - .../enedis/lab/io/usb/USBPortDescriptor.java | 609 ------- .../java/enedis/lab/io/usb/USBPortFinder.java | 71 - .../java/enedis/lab/protocol/tic/TICMode.java | 113 -- .../tic/channels/ChannelTICSerialPort.java | 254 --- .../ChannelTICSerialPortConfiguration.java | 237 --- .../serialport/ChannelSerialPort.java | 1201 ------------ .../ChannelSerialPortErrorCode.java | 94 - .../tic/codec/CodecTICFrameHistoric.java | 140 -- .../codec/CodecTICFrameHistoricDataSet.java | 205 --- .../tic/codec/CodecTICFrameStandard.java | 139 -- .../codec/CodecTICFrameStandardDataSet.java | 209 --- .../lab/protocol/tic/codec/TICCodec.java | 259 --- .../tic/datastreams/TICInputStream.java | 236 --- .../datastreams/TICStreamConfiguration.java | 185 -- .../lab/protocol/tic/frame/TICError.java | 108 -- .../lab/protocol/tic/frame/TICFrame.java | 427 ----- .../protocol/tic/frame/TICFrameDataSet.java | 250 --- .../tic/frame/historic/TICFrameHistoric.java | 104 -- .../historic/TICFrameHistoricDataSet.java | 151 -- .../tic/frame/standard/TICException.java | 77 - .../tic/frame/standard/TICFrameStandard.java | 202 --- .../standard/TICFrameStandardDataSet.java | 291 --- .../java/enedis/lab/types/BytesArray.java | 996 ---------- .../java/enedis/lab/types/DataArrayList.java | 304 ---- .../java/enedis/lab/types/DataDictionary.java | 145 -- .../lab/types/DataDictionaryException.java | 72 - src/main/java/enedis/lab/types/DataList.java | 57 - .../java/enedis/lab/types/ExceptionBase.java | 87 - .../types/configuration/Configuration.java | 69 - .../configuration/ConfigurationBase.java | 185 -- .../configuration/ConfigurationException.java | 123 -- .../datadictionary/DataDictionaryBase.java | 691 ------- .../types/datadictionary/KeyDescriptor.java | 71 - .../datadictionary/KeyDescriptorBase.java | 180 -- .../KeyDescriptorDataDictionary.java | 130 -- .../datadictionary/KeyDescriptorEnum.java | 109 -- .../datadictionary/KeyDescriptorList.java | 193 -- .../KeyDescriptorListMinMaxSize.java | 120 -- .../KeyDescriptorLocalDateTime.java | 115 -- .../datadictionary/KeyDescriptorNumber.java | 107 -- .../KeyDescriptorNumberMinMax.java | 114 -- .../datadictionary/KeyDescriptorString.java | 98 - .../java/enedis/lab/util/MinMaxChecker.java | 118 -- .../java/enedis/lab/util/SystemError.java | 67 - src/main/java/enedis/lab/util/SystemLibC.java | 41 - .../java/enedis/lab/util/message/Event.java | 132 -- .../java/enedis/lab/util/message/Message.java | 156 -- .../java/enedis/lab/util/message/Request.java | 100 - .../enedis/lab/util/message/Response.java | 198 -- .../enedis/lab/util/message/ResponseBase.java | 140 -- .../factory/AbstractMessageFactory.java | 119 -- .../message/factory/BasicMessageFactory.java | 123 -- .../util/message/factory/EventFactory.java | 41 - .../util/message/factory/MessageFactory.java | 152 -- .../util/message/factory/RequestFactory.java | 40 - .../util/message/factory/ResponseFactory.java | 41 - .../java/enedis/tic/core/TICCoreError.java | 227 --- .../java/enedis/tic/core/TICCoreFrame.java | 224 --- .../enedis/tic/core/TICCoreStreamBase.java | 279 --- .../java/enedis/tic/core/TICIdentifier.java | 213 --- .../config/TIC2WebSocketConfiguration.java | 286 --- .../tic/service/message/EventOnError.java | 153 -- .../tic/service/message/EventOnTICData.java | 153 -- .../message/RequestGetAvailableTICs.java | 81 - .../service/message/RequestGetModemsInfo.java | 82 - .../tic/service/message/RequestReadTIC.java | 150 -- .../service/message/RequestSubscribeTIC.java | 150 -- .../message/RequestUnsubscribeTIC.java | 150 -- .../message/ResponseGetAvailableTICs.java | 159 -- .../message/ResponseGetModemsInfo.java | 160 -- .../tic/service/message/ResponseReadTIC.java | 158 -- .../service/message/ResponseSubscribeTIC.java | 113 -- .../message/ResponseUnsubscribeTIC.java | 113 -- .../factory/TIC2WebSocketEventFactory.java | 39 - .../factory/TIC2WebSocketMessageFactory.java | 43 - .../factory/TIC2WebSocketRequestFactory.java | 49 - .../factory/TIC2WebSocketResponseFactory.java | 49 - .../tic/core/ReadNextFrameSubscriber.java | 2 +- .../java/{enedis => }/tic/core/TICCore.java | 8 +- .../{enedis => }/tic/core/TICCoreBase.java | 226 ++- src/main/java/tic/core/TICCoreError.java | 112 ++ .../tic/core/TICCoreErrorCode.java | 2 +- .../tic/core/TICCoreException.java | 27 +- src/main/java/tic/core/TICCoreFrame.java | 96 + .../{enedis => }/tic/core/TICCoreStream.java | 6 +- src/main/java/tic/core/TICCoreStreamBase.java | 277 +++ .../tic/core/TICCoreSubscriber.java | 4 +- src/main/java/tic/core/TICIdentifier.java | 167 ++ .../tic/core/codec/TICCoreErrorCodec.java | 71 + .../tic/core/codec/TICCoreFrameCodec.java | 66 + .../tic/core/codec/TICIdentifierCodec.java | 119 ++ .../java/tic/diagnostic/core/TICCoreApp.java | 16 + .../java/tic/diagnostic/core/TICCoreCli.java | 188 ++ .../core/TICCoreIdentifierOptions.java | 43 + .../core/TICCorePrintingSubscriber.java | 64 + .../commands/TICCoreAvailableCommand.java | 48 + .../core/commands/TICCoreModemsCommand.java | 49 + .../core/commands/TICCoreReadCommand.java | 70 + .../commands/TICCoreSubscribeCommand.java | 115 ++ .../tic/diagnostic/modem/ModemFinderApp.java | 39 + .../modem/ModemPlugNotifierApp.java | 69 + .../serialport/SerialPortFinderApp.java | 33 + .../tic/diagnostic/usb/UsbPortFinderApp.java | 30 + src/main/java/tic/frame/TICFrame.java | 111 ++ src/main/java/tic/frame/TICMode.java | 22 + src/main/java/tic/frame/TICModeDetector.java | 86 + .../java/tic/frame/checksum/TICChecksum.java | 106 ++ .../tic/frame/checksum/TICChecksumOffset.java | 113 ++ .../java/tic/frame/codec/TICFrameCodec.java | 136 ++ .../frame/codec/TICFrameDetailledCodec.java | 67 + .../frame/codec/TICFrameSummarizedCodec.java | 61 + .../frame/delimiter/TICFrameDelimiter.java | 30 + .../frame/delimiter/TICGroupDelimiter.java | 30 + .../tic/frame/delimiter/TICSeparator.java | 56 + .../tic/frame/delimiter/TICStartPattern.java | 67 + src/main/java/tic/frame/group/TICGroup.java | 90 + .../lab => tic}/io/PlugSubscriber.java | 4 +- .../{enedis/lab => tic}/io/PortFinder.java | 8 +- .../lab => tic}/io/PortPlugNotifier.java | 20 +- .../java/tic/io/modem/ModemDescriptor.java | 126 ++ src/main/java/tic/io/modem/ModemFinder.java | 69 + .../java/tic/io/modem/ModemFinderBase.java | 114 ++ .../java/tic/io/modem/ModemJsonCodec.java | 94 + .../java/tic/io/modem/ModemPlugNotifier.java | 24 + .../io/modem/ModemType.java} | 20 +- .../io/serialport/SerialPortDescriptor.java | 269 +++ .../io/serialport/SerialPortFinder.java | 28 +- .../io/serialport/SerialPortFinderBase.java | 50 +- .../serialport/SerialPortFinderForLinux.java | 133 +- .../serialport/SerialPortFinderForMacOsX.java | 370 ++++ .../SerialPortFinderForWindows.java | 68 +- .../io/serialport/SerialPortJsonEncoder.java | 61 + .../java/tic/io/usb/UsbPortDescriptor.java | 541 ++++++ src/main/java/tic/io/usb/UsbPortFinder.java | 55 + .../io/usb/UsbPortFinderBase.java} | 74 +- .../java/tic/io/usb/UsbPortJsonEncoder.java | 70 + .../tic/service/TIC2WebSocketApplication.java | 28 +- .../TIC2WebSocketApplicationErrorCode.java | 2 +- .../tic/service/TIC2WebSocketCommandLine.java | 2 +- .../service/client/TIC2WebSocketClient.java | 38 +- .../client/TIC2WebSocketClientPool.java | 4 +- .../client/TIC2WebSocketClientPoolBase.java | 4 +- .../config/TIC2WebSocketConfiguration.java | 141 ++ .../TIC2WebSocketConfigurationCodec.java | 123 ++ .../TIC2WebSocketConfigurationLoader.java | 39 + .../tic/service/endpoint/EventSender.java | 4 +- .../TIC2WebSocketEndPointErrorCode.java | 2 +- .../TIC2WebSocketEndPointException.java | 2 +- .../tic/service/message/EventOnError.java | 68 + .../tic/service/message/EventOnTICData.java | 68 + .../message/RequestGetAvailableTICs.java | 37 + .../service/message/RequestGetModemsInfo.java | 37 + .../tic/service/message/RequestReadTIC.java | 65 + .../service/message/RequestSubscribeTIC.java | 66 + .../message/RequestUnsubscribeTIC.java | 66 + .../tic/service/message/ResponseError.java | 26 + .../message/ResponseGetAvailableTICs.java | 74 + .../message/ResponseGetModemsInfo.java | 73 + .../tic/service/message/ResponseReadTIC.java | 61 + .../service/message/ResponseSubscribeTIC.java | 44 + .../message/ResponseUnsubscribeTIC.java | 44 + .../TIC2WebSocketChannelInitializer.java | 6 +- .../service/netty/TIC2WebSocketHandler.java | 135 +- .../service/netty/TIC2WebSocketServer.java | 6 +- .../TIC2WebSocketRequestHandler.java | 8 +- .../TIC2WebSocketRequestHandlerBase.java | 145 +- src/main/java/tic/stream/TICStream.java | 259 +++ .../java/tic/stream/TICStreamListener.java | 28 + .../tic/stream/TICStreamModeDetector.java | 105 ++ src/main/java/tic/stream/TICStreamReader.java | 243 +++ .../configuration/TICStreamConfiguration.java | 97 + .../TICStreamConfigurationLoader.java | 82 + .../tic/stream/identifier/SerialPortId.java | 30 + .../tic/stream/identifier/SerialPortName.java | 30 + .../identifier/TICStreamIdentifier.java | 57 + .../identifier/TICStreamIdentifierType.java | 22 + src/main/java/tic/util/ValueChecker.java | 69 + src/main/java/tic/util/codec/Codec.java | 14 + .../java/tic/util/codec/JsonArrayCodec.java | 14 + .../java/tic/util/codec/JsonObjectCodec.java | 14 + .../java/tic/util/codec/JsonStringCodec.java | 24 + src/main/java/tic/util/message/Event.java | 61 + src/main/java/tic/util/message/Message.java | 76 + .../lab => tic}/util/message/MessageType.java | 2 +- src/main/java/tic/util/message/Request.java | 37 + src/main/java/tic/util/message/Response.java | 105 ++ .../tic/util/message/ResponseWithData.java | 42 + .../util/message/codec/EventJsonEncoder.java | 63 + .../util/message/codec/MessageJsonCodec.java | 100 + .../message/codec/RequestJsonDecoder.java | 127 ++ .../message/codec/RequestJsonEncoder.java | 60 + .../message/codec/ResponseJsonEncoder.java | 82 + .../message/exception/MessageException.java | 2 +- .../MessageInvalidContentException.java | 2 +- .../MessageInvalidFormatException.java | 2 +- .../MessageInvalidTypeException.java | 2 +- .../MessageKeyNameDoesntExistException.java | 2 +- .../MessageKeyTypeDoesntExistException.java | 2 +- .../UnsupportedMessageException.java | 2 +- .../util/task/FilteredNotifier.java | 3 +- .../util/task/FilteredNotifierBase.java | 3 +- .../lab => tic}/util/task/Notifier.java | 3 +- .../lab => tic}/util/task/NotifierBase.java | 3 +- .../lab => tic}/util/task/Subscriber.java | 12 +- .../{enedis/lab => tic}/util/task/Task.java | 12 +- .../lab => tic}/util/task/TaskBase.java | 12 +- .../lab => tic}/util/task/TaskPeriodic.java | 14 +- .../task/TaskPeriodicWithSubscribers.java | 11 +- .../{enedis/lab => tic}/util/time/Time.java | 2 +- .../TIC2WebSocketConfiguration.json.license | 6 - .../config/TICStreamConfiguration.json | 8 + src/main/resources/log4j2-debug.xml | 32 +- src/main/resources/log4j2-error.xml | 32 +- src/main/resources/log4j2-fatal.xml | 32 +- src/main/resources/log4j2-info.xml | 32 +- src/main/resources/log4j2-off.xml | 32 +- src/main/resources/log4j2-trace.xml | 32 +- src/main/resources/log4j2-warn.xml | 32 +- .../enedis/lab/io/tic/TICPortFinderMock.java | 39 - .../enedis/tic/core/TICIdentifierTest.java | 167 -- .../TIC2WebSocketEventFactoryTest.java | 89 - .../TIC2WebSocketMessageFactoryTest.java | 36 - .../TIC2WebSocketRequestFactoryTest.java | 316 ---- .../TIC2WebSocketResponseFactoryTest.java | 201 -- .../netty/TIC2WebSocketServerTest.java | 37 - src/test/java/tic/ResourceLoader.java | 50 + .../tic/core/TICCoreBaseTest.java | 644 +++++-- .../tic/core/TICCoreReadNextFrameTask.java | 4 +- .../tic/core/TICCoreStreamMock.java | 11 +- .../tic/core/TICCoreSubscriberMock.java | 15 +- .../tic/core/TICCoreSubscriberOnDataCall.java | 4 +- .../core/TICCoreSubscriberOnErrorCall.java | 4 +- src/test/java/tic/core/TICIdentifierTest.java | 114 ++ src/test/java/tic/frame/TICFrameTest.java | 326 ++++ .../java/tic/frame/TICModeDetectorTest.java | 134 ++ .../frame/checksum/TICChecksumOffsetTest.java | 200 ++ .../tic/frame/checksum/TICChecksumTest.java | 140 ++ .../tic/frame/codec/TICFrameCodecTest.java | 295 +++ .../frame/codec/TICFrameJsonEncoderTest.java | 177 ++ .../delimiter/TICFrameDelimiterTest.java | 36 + .../delimiter/TICGroupDelimiterTest.java | 35 + .../tic/frame/delimiter/TICSeparatorTest.java | 86 + .../frame/delimiter/TICStartPatternTest.java | 112 ++ .../java/tic/frame/group/TICGroupTest.java | 222 +++ .../lab => tic}/io/PortFinderMock.java | 22 +- .../tic/io/modem/ModemDescriptorTest.java | 515 ++++++ .../java/tic/io/modem/ModemFinderMock.java | 52 + .../tic/io/modem/ModemJsonEncoderTest.java | 96 + .../serialport/SerialPortDescriptorTest.java | 879 +++++++++ .../serialport/SerialPortFinderBaseTest.java | 101 ++ .../serialport/SerialPortJsonEncoderTest.java | 82 + .../tic/io/usb/UsbPortDescriptorTest.java | 1616 +++++++++++++++++ .../tic/io/usb/UsbPortFinderBaseTest.java | 79 + .../tic/io/usb/UsbPortJsonEncoderTest.java | 99 + .../lab => tic}/mock/FunctionCall.java | 2 +- .../TIC2WebSocketConfigurationLoaderTest.java | 193 ++ .../TICStreamConfigurationLoaderTest.java | 187 ++ .../TICStreamConfigurationTest.java | 128 ++ .../message/codec/MessageJsonCodecTest.java | 467 +++++ .../tic/frame/codec/ticFrameHistoric.txt | 10 + .../codec/ticFrameHistoric_full_details.json | 50 + .../tic/frame/codec/ticFrameStandard.txt | 39 + .../codec/ticFrameStandard_PCOUP_Invalid.txt | 39 + .../codec/ticFrameStandard_summarized.json | 40 + .../resources/tic/io/modem/AllFields.json | 11 + .../resources/tic/io/modem/Descriptor.json | 11 + src/test/resources/tic/io/modem/NullList.json | 1 + .../resources/tic/io/modem/NullModemType.json | 6 + .../tic/io/serialport/AllFields.json | 12 + .../resources/tic/io/serialport/NullList.json | 1 + .../tic/io/serialport/NullifiedStrings.json | 7 + .../tic/io/usb/AllDescriptorFields.json | 21 + src/test/resources/tic/io/usb/NullList.json | 1 + .../tic/io/usb/WithNullStringFields.json | 18 + .../tic/service/config_empty_portnames.json | 4 + .../resources/tic/service/config_full.json | 5 + .../service/config_invalid_missing_port.json | 3 + .../tic/service/config_invalid_mode.json | 4 + .../service/config_invalid_port_range.json | 3 + ...fig_invalid_portnames_duplicates_trim.json | 4 + .../config_invalid_portnames_empty.json | 4 + ...config_invalid_portnames_null_element.json | 4 + .../resources/tic/service/config_minimal.json | 3 + .../tic/service/config_mode_lowercase.json | 4 + .../stream/configuration/loader-valid.json | 8 + .../withInvalidIdentifierType.json | 6 + .../configuration/withInvalidTicMode.json | 8 + .../configuration/withInvalidTimeout.json | 7 + .../configuration/withMissingIdentifier.json | 4 + .../configuration/withPortIdIdentifier.json | 6 + .../withValidPortNameConfig.json | 8 + .../message/codec/Invalid_MissingName.json | 3 + .../message/codec/Invalid_MissingType.json | 3 + .../codec/Invalid_ReadTIC_MissingData.json | 4 + .../util/message/codec/Invalid_TypeEvent.json | 4 + .../codec/Invalid_UnsupportedName.json | 4 + .../codec/RequestGetAvailableTICs.json | 4 + .../message/codec/RequestGetModemsInfo.json | 4 + .../codec/RequestReadTIC_WithAllFields.json | 9 + .../codec/RequestReadTIC_WithPortId.json | 7 + .../codec/RequestReadTIC_WithPortName.json | 7 + .../RequestReadTIC_WithSerialNumber.json | 7 + .../RequestSubscribeTIC_WithEmptyArray.json | 5 + .../codec/RequestSubscribeTIC_WithNoData.json | 4 + ...RequestSubscribeTIC_WithOneIdentifier.json | 7 + ...estSubscribeTIC_WithOneIdentifierList.json | 9 + .../RequestUnsubscribeTIC_WithEmptyArray.json | 5 + .../RequestUnsubscribeTIC_WithNoData.json | 4 + ...questUnsubscribeTIC_WithOneIdentifier.json | 7 + ...tUnsubscribeTIC_WithOneIdentifierList.json | 9 + 393 files changed, 16691 insertions(+), 20753 deletions(-) delete mode 100644 LICENSES/CC0-1.0.txt create mode 100644 REUSE.toml create mode 100644 docs/assets/enedis-logo.svg create mode 100644 docs/assets/fonts/Enedis-Black.woff create mode 100644 docs/assets/fonts/Enedis-Black.woff2 create mode 100644 docs/assets/fonts/Enedis-Bold.woff create mode 100644 docs/assets/fonts/Enedis-Bold.woff2 create mode 100644 docs/assets/fonts/Enedis-Light.woff create mode 100644 docs/assets/fonts/Enedis-Light.woff2 create mode 100644 docs/assets/fonts/Enedis-Medium.woff create mode 100644 docs/assets/fonts/Enedis-Medium.woff2 create mode 100644 docs/assets/fonts/Enedis-Regular.woff create mode 100644 docs/assets/fonts/Enedis-Regular.woff2 create mode 100644 docs/assets/fonts/Enedis-Thin.woff create mode 100644 docs/assets/fonts/Enedis-Thin.woff2 create mode 100644 docs/ws-tester.html delete mode 100644 src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json delete mode 100644 src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license delete mode 100644 src/dataDictionaryDescriptors/message/EventOnError.json delete mode 100644 src/dataDictionaryDescriptors/message/EventOnError.json.license delete mode 100644 src/dataDictionaryDescriptors/message/EventOnTICData.json delete mode 100644 src/dataDictionaryDescriptors/message/EventOnTICData.json.license delete mode 100644 src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json delete mode 100644 src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license delete mode 100644 src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json delete mode 100644 src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license delete mode 100644 src/dataDictionaryDescriptors/message/RequestReadTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/RequestReadTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json delete mode 100644 src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license delete mode 100644 src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json delete mode 100644 src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license delete mode 100644 src/dataDictionaryDescriptors/message/ResponseReadTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreError.json delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICIdentifier.json delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license delete mode 100644 src/main/java/enedis/lab/codec/Codec.java delete mode 100644 src/main/java/enedis/lab/codec/CodecException.java delete mode 100644 src/main/java/enedis/lab/io/channels/Channel.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelBase.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelConfiguration.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelDirection.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelException.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelListener.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelPhysical.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelProtocol.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelStatus.java delete mode 100644 src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java delete mode 100644 src/main/java/enedis/lab/io/channels/serialport/Parity.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataInputStream.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStream.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamBase.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamException.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamListener.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamType.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamUser.java delete mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java delete mode 100644 src/main/java/enedis/lab/io/tic/TICPortDescriptor.java delete mode 100644 src/main/java/enedis/lab/io/tic/TICPortFinder.java delete mode 100644 src/main/java/enedis/lab/io/tic/TICPortFinderBase.java delete mode 100644 src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java delete mode 100644 src/main/java/enedis/lab/io/usb/USBPortDescriptor.java delete mode 100644 src/main/java/enedis/lab/io/usb/USBPortFinder.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/TICMode.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/TICError.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java delete mode 100644 src/main/java/enedis/lab/types/BytesArray.java delete mode 100644 src/main/java/enedis/lab/types/DataArrayList.java delete mode 100644 src/main/java/enedis/lab/types/DataDictionary.java delete mode 100644 src/main/java/enedis/lab/types/DataDictionaryException.java delete mode 100644 src/main/java/enedis/lab/types/DataList.java delete mode 100644 src/main/java/enedis/lab/types/ExceptionBase.java delete mode 100644 src/main/java/enedis/lab/types/configuration/Configuration.java delete mode 100644 src/main/java/enedis/lab/types/configuration/ConfigurationBase.java delete mode 100644 src/main/java/enedis/lab/types/configuration/ConfigurationException.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java delete mode 100644 src/main/java/enedis/lab/util/MinMaxChecker.java delete mode 100644 src/main/java/enedis/lab/util/SystemError.java delete mode 100644 src/main/java/enedis/lab/util/SystemLibC.java delete mode 100644 src/main/java/enedis/lab/util/message/Event.java delete mode 100644 src/main/java/enedis/lab/util/message/Message.java delete mode 100644 src/main/java/enedis/lab/util/message/Request.java delete mode 100644 src/main/java/enedis/lab/util/message/Response.java delete mode 100644 src/main/java/enedis/lab/util/message/ResponseBase.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/EventFactory.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/MessageFactory.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/RequestFactory.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/ResponseFactory.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreError.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreFrame.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreStreamBase.java delete mode 100644 src/main/java/enedis/tic/core/TICIdentifier.java delete mode 100644 src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java delete mode 100644 src/main/java/enedis/tic/service/message/EventOnError.java delete mode 100644 src/main/java/enedis/tic/service/message/EventOnTICData.java delete mode 100644 src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java delete mode 100644 src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java delete mode 100644 src/main/java/enedis/tic/service/message/RequestReadTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java delete mode 100644 src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java delete mode 100644 src/main/java/enedis/tic/service/message/ResponseReadTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java delete mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java delete mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java delete mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java rename src/main/java/{enedis => }/tic/core/ReadNextFrameSubscriber.java (98%) rename src/main/java/{enedis => }/tic/core/TICCore.java (94%) rename src/main/java/{enedis => }/tic/core/TICCoreBase.java (66%) create mode 100644 src/main/java/tic/core/TICCoreError.java rename src/main/java/{enedis => }/tic/core/TICCoreErrorCode.java (98%) rename src/main/java/{enedis => }/tic/core/TICCoreException.java (52%) create mode 100644 src/main/java/tic/core/TICCoreFrame.java rename src/main/java/{enedis => }/tic/core/TICCoreStream.java (90%) create mode 100644 src/main/java/tic/core/TICCoreStreamBase.java rename src/main/java/{enedis => }/tic/core/TICCoreSubscriber.java (93%) create mode 100644 src/main/java/tic/core/TICIdentifier.java create mode 100644 src/main/java/tic/core/codec/TICCoreErrorCodec.java create mode 100644 src/main/java/tic/core/codec/TICCoreFrameCodec.java create mode 100644 src/main/java/tic/core/codec/TICIdentifierCodec.java create mode 100644 src/main/java/tic/diagnostic/core/TICCoreApp.java create mode 100644 src/main/java/tic/diagnostic/core/TICCoreCli.java create mode 100644 src/main/java/tic/diagnostic/core/TICCoreIdentifierOptions.java create mode 100644 src/main/java/tic/diagnostic/core/TICCorePrintingSubscriber.java create mode 100644 src/main/java/tic/diagnostic/core/commands/TICCoreAvailableCommand.java create mode 100644 src/main/java/tic/diagnostic/core/commands/TICCoreModemsCommand.java create mode 100644 src/main/java/tic/diagnostic/core/commands/TICCoreReadCommand.java create mode 100644 src/main/java/tic/diagnostic/core/commands/TICCoreSubscribeCommand.java create mode 100644 src/main/java/tic/diagnostic/modem/ModemFinderApp.java create mode 100644 src/main/java/tic/diagnostic/modem/ModemPlugNotifierApp.java create mode 100644 src/main/java/tic/diagnostic/serialport/SerialPortFinderApp.java create mode 100644 src/main/java/tic/diagnostic/usb/UsbPortFinderApp.java create mode 100644 src/main/java/tic/frame/TICFrame.java create mode 100644 src/main/java/tic/frame/TICMode.java create mode 100644 src/main/java/tic/frame/TICModeDetector.java create mode 100644 src/main/java/tic/frame/checksum/TICChecksum.java create mode 100644 src/main/java/tic/frame/checksum/TICChecksumOffset.java create mode 100644 src/main/java/tic/frame/codec/TICFrameCodec.java create mode 100644 src/main/java/tic/frame/codec/TICFrameDetailledCodec.java create mode 100644 src/main/java/tic/frame/codec/TICFrameSummarizedCodec.java create mode 100644 src/main/java/tic/frame/delimiter/TICFrameDelimiter.java create mode 100644 src/main/java/tic/frame/delimiter/TICGroupDelimiter.java create mode 100644 src/main/java/tic/frame/delimiter/TICSeparator.java create mode 100644 src/main/java/tic/frame/delimiter/TICStartPattern.java create mode 100644 src/main/java/tic/frame/group/TICGroup.java rename src/main/java/{enedis/lab => tic}/io/PlugSubscriber.java (96%) rename src/main/java/{enedis/lab => tic}/io/PortFinder.java (92%) rename src/main/java/{enedis/lab => tic}/io/PortPlugNotifier.java (93%) create mode 100644 src/main/java/tic/io/modem/ModemDescriptor.java create mode 100644 src/main/java/tic/io/modem/ModemFinder.java create mode 100644 src/main/java/tic/io/modem/ModemFinderBase.java create mode 100644 src/main/java/tic/io/modem/ModemJsonCodec.java create mode 100644 src/main/java/tic/io/modem/ModemPlugNotifier.java rename src/main/java/{enedis/lab/io/tic/TICModemType.java => tic/io/modem/ModemType.java} (60%) create mode 100644 src/main/java/tic/io/serialport/SerialPortDescriptor.java rename src/main/java/{enedis/lab => tic}/io/serialport/SerialPortFinder.java (83%) rename src/main/java/{enedis/lab => tic}/io/serialport/SerialPortFinderBase.java (66%) rename src/main/java/{enedis/lab => tic}/io/serialport/SerialPortFinderForLinux.java (84%) create mode 100644 src/main/java/tic/io/serialport/SerialPortFinderForMacOsX.java rename src/main/java/{enedis/lab => tic}/io/serialport/SerialPortFinderForWindows.java (88%) create mode 100644 src/main/java/tic/io/serialport/SerialPortJsonEncoder.java create mode 100644 src/main/java/tic/io/usb/UsbPortDescriptor.java create mode 100644 src/main/java/tic/io/usb/UsbPortFinder.java rename src/main/java/{enedis/lab/io/usb/USBPortFinderBase.java => tic/io/usb/UsbPortFinderBase.java} (54%) create mode 100644 src/main/java/tic/io/usb/UsbPortJsonEncoder.java rename src/main/java/{enedis => }/tic/service/TIC2WebSocketApplication.java (94%) rename src/main/java/{enedis => }/tic/service/TIC2WebSocketApplicationErrorCode.java (98%) rename src/main/java/{enedis => }/tic/service/TIC2WebSocketCommandLine.java (99%) rename src/main/java/{enedis => }/tic/service/client/TIC2WebSocketClient.java (69%) rename src/main/java/{enedis => }/tic/service/client/TIC2WebSocketClientPool.java (96%) rename src/main/java/{enedis => }/tic/service/client/TIC2WebSocketClientPoolBase.java (98%) create mode 100644 src/main/java/tic/service/config/TIC2WebSocketConfiguration.java create mode 100644 src/main/java/tic/service/config/TIC2WebSocketConfigurationCodec.java create mode 100644 src/main/java/tic/service/config/TIC2WebSocketConfigurationLoader.java rename src/main/java/{enedis => }/tic/service/endpoint/EventSender.java (94%) rename src/main/java/{enedis => }/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java (98%) rename src/main/java/{enedis => }/tic/service/endpoint/TIC2WebSocketEndPointException.java (98%) create mode 100644 src/main/java/tic/service/message/EventOnError.java create mode 100644 src/main/java/tic/service/message/EventOnTICData.java create mode 100644 src/main/java/tic/service/message/RequestGetAvailableTICs.java create mode 100644 src/main/java/tic/service/message/RequestGetModemsInfo.java create mode 100644 src/main/java/tic/service/message/RequestReadTIC.java create mode 100644 src/main/java/tic/service/message/RequestSubscribeTIC.java create mode 100644 src/main/java/tic/service/message/RequestUnsubscribeTIC.java create mode 100644 src/main/java/tic/service/message/ResponseError.java create mode 100644 src/main/java/tic/service/message/ResponseGetAvailableTICs.java create mode 100644 src/main/java/tic/service/message/ResponseGetModemsInfo.java create mode 100644 src/main/java/tic/service/message/ResponseReadTIC.java create mode 100644 src/main/java/tic/service/message/ResponseSubscribeTIC.java create mode 100644 src/main/java/tic/service/message/ResponseUnsubscribeTIC.java rename src/main/java/{enedis => }/tic/service/netty/TIC2WebSocketChannelInitializer.java (94%) rename src/main/java/{enedis => }/tic/service/netty/TIC2WebSocketHandler.java (69%) rename src/main/java/{enedis => }/tic/service/netty/TIC2WebSocketServer.java (96%) rename src/main/java/{enedis => }/tic/service/requesthandler/TIC2WebSocketRequestHandler.java (88%) rename src/main/java/{enedis => }/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java (71%) create mode 100644 src/main/java/tic/stream/TICStream.java create mode 100644 src/main/java/tic/stream/TICStreamListener.java create mode 100644 src/main/java/tic/stream/TICStreamModeDetector.java create mode 100644 src/main/java/tic/stream/TICStreamReader.java create mode 100644 src/main/java/tic/stream/configuration/TICStreamConfiguration.java create mode 100644 src/main/java/tic/stream/configuration/TICStreamConfigurationLoader.java create mode 100644 src/main/java/tic/stream/identifier/SerialPortId.java create mode 100644 src/main/java/tic/stream/identifier/SerialPortName.java create mode 100644 src/main/java/tic/stream/identifier/TICStreamIdentifier.java create mode 100644 src/main/java/tic/stream/identifier/TICStreamIdentifierType.java create mode 100644 src/main/java/tic/util/ValueChecker.java create mode 100644 src/main/java/tic/util/codec/Codec.java create mode 100644 src/main/java/tic/util/codec/JsonArrayCodec.java create mode 100644 src/main/java/tic/util/codec/JsonObjectCodec.java create mode 100644 src/main/java/tic/util/codec/JsonStringCodec.java create mode 100644 src/main/java/tic/util/message/Event.java create mode 100644 src/main/java/tic/util/message/Message.java rename src/main/java/{enedis/lab => tic}/util/message/MessageType.java (95%) create mode 100644 src/main/java/tic/util/message/Request.java create mode 100644 src/main/java/tic/util/message/Response.java create mode 100644 src/main/java/tic/util/message/ResponseWithData.java create mode 100644 src/main/java/tic/util/message/codec/EventJsonEncoder.java create mode 100644 src/main/java/tic/util/message/codec/MessageJsonCodec.java create mode 100644 src/main/java/tic/util/message/codec/RequestJsonDecoder.java create mode 100644 src/main/java/tic/util/message/codec/RequestJsonEncoder.java create mode 100644 src/main/java/tic/util/message/codec/ResponseJsonEncoder.java rename src/main/java/{enedis/lab => tic}/util/message/exception/MessageException.java (98%) rename src/main/java/{enedis/lab => tic}/util/message/exception/MessageInvalidContentException.java (98%) rename src/main/java/{enedis/lab => tic}/util/message/exception/MessageInvalidFormatException.java (98%) rename src/main/java/{enedis/lab => tic}/util/message/exception/MessageInvalidTypeException.java (98%) rename src/main/java/{enedis/lab => tic}/util/message/exception/MessageKeyNameDoesntExistException.java (98%) rename src/main/java/{enedis/lab => tic}/util/message/exception/MessageKeyTypeDoesntExistException.java (98%) rename src/main/java/{enedis/lab => tic}/util/message/exception/UnsupportedMessageException.java (98%) rename src/main/java/{enedis/lab => tic}/util/task/FilteredNotifier.java (98%) rename src/main/java/{enedis/lab => tic}/util/task/FilteredNotifierBase.java (99%) rename src/main/java/{enedis/lab => tic}/util/task/Notifier.java (95%) rename src/main/java/{enedis/lab => tic}/util/task/NotifierBase.java (96%) rename src/main/java/{enedis/lab => tic}/util/task/Subscriber.java (64%) rename src/main/java/{enedis/lab => tic}/util/task/Task.java (74%) rename src/main/java/{enedis/lab => tic}/util/task/TaskBase.java (91%) rename src/main/java/{enedis/lab => tic}/util/task/TaskPeriodic.java (87%) rename src/main/java/{enedis/lab => tic}/util/task/TaskPeriodicWithSubscribers.java (82%) rename src/main/java/{enedis/lab => tic}/util/time/Time.java (99%) delete mode 100644 src/main/resources/config/TIC2WebSocketConfiguration.json.license create mode 100644 src/main/resources/config/TICStreamConfiguration.json delete mode 100644 src/test/java/enedis/lab/io/tic/TICPortFinderMock.java delete mode 100644 src/test/java/enedis/tic/core/TICIdentifierTest.java delete mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java delete mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java delete mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java delete mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java delete mode 100644 src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java create mode 100644 src/test/java/tic/ResourceLoader.java rename src/test/java/{enedis => }/tic/core/TICCoreBaseTest.java (50%) rename src/test/java/{enedis => }/tic/core/TICCoreReadNextFrameTask.java (94%) rename src/test/java/{enedis => }/tic/core/TICCoreStreamMock.java (88%) rename src/test/java/{enedis => }/tic/core/TICCoreSubscriberMock.java (62%) rename src/test/java/{enedis => }/tic/core/TICCoreSubscriberOnDataCall.java (87%) rename src/test/java/{enedis => }/tic/core/TICCoreSubscriberOnErrorCall.java (87%) create mode 100644 src/test/java/tic/core/TICIdentifierTest.java create mode 100644 src/test/java/tic/frame/TICFrameTest.java create mode 100644 src/test/java/tic/frame/TICModeDetectorTest.java create mode 100644 src/test/java/tic/frame/checksum/TICChecksumOffsetTest.java create mode 100644 src/test/java/tic/frame/checksum/TICChecksumTest.java create mode 100644 src/test/java/tic/frame/codec/TICFrameCodecTest.java create mode 100644 src/test/java/tic/frame/codec/TICFrameJsonEncoderTest.java create mode 100644 src/test/java/tic/frame/delimiter/TICFrameDelimiterTest.java create mode 100644 src/test/java/tic/frame/delimiter/TICGroupDelimiterTest.java create mode 100644 src/test/java/tic/frame/delimiter/TICSeparatorTest.java create mode 100644 src/test/java/tic/frame/delimiter/TICStartPatternTest.java create mode 100644 src/test/java/tic/frame/group/TICGroupTest.java rename src/test/java/{enedis/lab => tic}/io/PortFinderMock.java (65%) create mode 100644 src/test/java/tic/io/modem/ModemDescriptorTest.java create mode 100644 src/test/java/tic/io/modem/ModemFinderMock.java create mode 100644 src/test/java/tic/io/modem/ModemJsonEncoderTest.java create mode 100644 src/test/java/tic/io/serialport/SerialPortDescriptorTest.java create mode 100644 src/test/java/tic/io/serialport/SerialPortFinderBaseTest.java create mode 100644 src/test/java/tic/io/serialport/SerialPortJsonEncoderTest.java create mode 100644 src/test/java/tic/io/usb/UsbPortDescriptorTest.java create mode 100644 src/test/java/tic/io/usb/UsbPortFinderBaseTest.java create mode 100644 src/test/java/tic/io/usb/UsbPortJsonEncoderTest.java rename src/test/java/{enedis/lab => tic}/mock/FunctionCall.java (95%) create mode 100644 src/test/java/tic/service/config/TIC2WebSocketConfigurationLoaderTest.java create mode 100644 src/test/java/tic/stream/configuration/TICStreamConfigurationLoaderTest.java create mode 100644 src/test/java/tic/stream/configuration/TICStreamConfigurationTest.java create mode 100644 src/test/java/tic/util/message/codec/MessageJsonCodecTest.java create mode 100644 src/test/resources/tic/frame/codec/ticFrameHistoric.txt create mode 100644 src/test/resources/tic/frame/codec/ticFrameHistoric_full_details.json create mode 100644 src/test/resources/tic/frame/codec/ticFrameStandard.txt create mode 100644 src/test/resources/tic/frame/codec/ticFrameStandard_PCOUP_Invalid.txt create mode 100644 src/test/resources/tic/frame/codec/ticFrameStandard_summarized.json create mode 100644 src/test/resources/tic/io/modem/AllFields.json create mode 100644 src/test/resources/tic/io/modem/Descriptor.json create mode 100644 src/test/resources/tic/io/modem/NullList.json create mode 100644 src/test/resources/tic/io/modem/NullModemType.json create mode 100644 src/test/resources/tic/io/serialport/AllFields.json create mode 100644 src/test/resources/tic/io/serialport/NullList.json create mode 100644 src/test/resources/tic/io/serialport/NullifiedStrings.json create mode 100644 src/test/resources/tic/io/usb/AllDescriptorFields.json create mode 100644 src/test/resources/tic/io/usb/NullList.json create mode 100644 src/test/resources/tic/io/usb/WithNullStringFields.json create mode 100644 src/test/resources/tic/service/config_empty_portnames.json create mode 100644 src/test/resources/tic/service/config_full.json create mode 100644 src/test/resources/tic/service/config_invalid_missing_port.json create mode 100644 src/test/resources/tic/service/config_invalid_mode.json create mode 100644 src/test/resources/tic/service/config_invalid_port_range.json create mode 100644 src/test/resources/tic/service/config_invalid_portnames_duplicates_trim.json create mode 100644 src/test/resources/tic/service/config_invalid_portnames_empty.json create mode 100644 src/test/resources/tic/service/config_invalid_portnames_null_element.json create mode 100644 src/test/resources/tic/service/config_minimal.json create mode 100644 src/test/resources/tic/service/config_mode_lowercase.json create mode 100644 src/test/resources/tic/stream/configuration/loader-valid.json create mode 100644 src/test/resources/tic/stream/configuration/withInvalidIdentifierType.json create mode 100644 src/test/resources/tic/stream/configuration/withInvalidTicMode.json create mode 100644 src/test/resources/tic/stream/configuration/withInvalidTimeout.json create mode 100644 src/test/resources/tic/stream/configuration/withMissingIdentifier.json create mode 100644 src/test/resources/tic/stream/configuration/withPortIdIdentifier.json create mode 100644 src/test/resources/tic/stream/configuration/withValidPortNameConfig.json create mode 100644 src/test/resources/tic/util/message/codec/Invalid_MissingName.json create mode 100644 src/test/resources/tic/util/message/codec/Invalid_MissingType.json create mode 100644 src/test/resources/tic/util/message/codec/Invalid_ReadTIC_MissingData.json create mode 100644 src/test/resources/tic/util/message/codec/Invalid_TypeEvent.json create mode 100644 src/test/resources/tic/util/message/codec/Invalid_UnsupportedName.json create mode 100644 src/test/resources/tic/util/message/codec/RequestGetAvailableTICs.json create mode 100644 src/test/resources/tic/util/message/codec/RequestGetModemsInfo.json create mode 100644 src/test/resources/tic/util/message/codec/RequestReadTIC_WithAllFields.json create mode 100644 src/test/resources/tic/util/message/codec/RequestReadTIC_WithPortId.json create mode 100644 src/test/resources/tic/util/message/codec/RequestReadTIC_WithPortName.json create mode 100644 src/test/resources/tic/util/message/codec/RequestReadTIC_WithSerialNumber.json create mode 100644 src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithEmptyArray.json create mode 100644 src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithNoData.json create mode 100644 src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithOneIdentifier.json create mode 100644 src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithOneIdentifierList.json create mode 100644 src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithEmptyArray.json create mode 100644 src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithNoData.json create mode 100644 src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithOneIdentifier.json create mode 100644 src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithOneIdentifierList.json diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml index 897b6d0..02d113f 100644 --- a/.github/workflows/reuse.yml +++ b/.github/workflows/reuse.yml @@ -1,6 +1,5 @@ -# SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. -# -# SPDX-License-Identifier: CC0-1.0 +#SPDX-FileCopyrightText: 2025 Enedis Smarties team +#SPDX-License-Identifier: Apache-2.0 --- name: REUSE Compliance Check diff --git a/.gitignore b/.gitignore index 8758e00..8787fe0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,7 @@ # SPDX-License-Identifier: Apache-2.0 .vscode/ +.DS_Store src/main/resources/TIC2WebSocket.properties -target/ \ No newline at end of file +target/ +var/ diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 90c0bfe..8f52ad7 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -9,6 +9,16 @@ [🇫🇷 Français](CHANGELOG.fr.md) | [🇺🇸 English](CHANGELOG.md) +## [v2.0.0](https://github.com/Enedis-OSS/TIC2WebSocket/tree/v2.0.0) +### ✨ Nouvelles fonctionnalités: +- Ajout de la découverte des ports série sous macOS (`SerialPortFinderForMacOsX`) +- Nouveaux utilitaires CLI de diagnostic (core, modem, port série, USB) +- Ajout d'une interface de test via une page HTML + +### 🔧 Améliorations & corrections: +- Refactorisation de l'architecture du projet et simplification de la structure de code +- Correction des avertissements du logger + ## [v1.0.0](https://github.com/Enedis-OSS/TIC2WebSocket/tree/v1.0.0) ### ✨ Nouvelles fonctionnalités: - Connexion et lecture des trames TIC (Télé-Information Client) via port série diff --git a/CHANGELOG.md b/CHANGELOG.md index ca39601..617bed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ [🇫🇷 Français](CHANGELOG.fr.md) | [🇺🇸 English](CHANGELOG.md) +## [v2.0.0](https://github.com/Enedis-OSS/TIC2WebSocket/tree/v2.0.0) +### ✨ New features: +- Added macOS serial port discovery (`SerialPortFinderForMacOsX`) +- New diagnostic CLI utilities (core, modem, serial port, USB) +- Added a test interface via an HTML page + +### 🔧 Improvements & fixes: +- Refactored project architecture and simplified code structure +- Fixed logger warnings + ## [v1.0.0](https://github.com/Enedis-OSS/TIC2WebSocket/tree/v1.0.0) ### ✨ New features: - Serial port connection and reading of TIC (Télé-Information Client) frames diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt deleted file mode 100644 index 0e259d4..0000000 --- a/LICENSES/CC0-1.0.txt +++ /dev/null @@ -1,121 +0,0 @@ -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. diff --git a/README.fr.md b/README.fr.md index d261e98..3de3070 100644 --- a/README.fr.md +++ b/README.fr.md @@ -91,7 +91,20 @@ Ajoutez l'option `--help` pour obtenir des informations de base sur l'utilisatio ## Documentation -[TODO] +### Page HTML de test WebSocket + +Une page HTML autonome est disponible pour tester rapidement l’API WebSocket : + +- Fichier : [docs/ws-tester.html](docs/ws-tester.html) +- URL par défaut : `ws://localhost:19584/` + +Fonctionnement : + +- Ouvrez la page dans un navigateur (optionnel : servez le dossier `docs/` via un serveur HTTP statique). +- Renseignez l’URL WebSocket (ou host/port), puis cliquez sur `Connect`. +- Choisissez une requête prédéfinie (`GetModemsInfo`, `GetAvailableTICs`, `ReadTIC`, `SubscribeTIC`, `UnsubscribeTIC`) puis cliquez sur `Send`. +- Les filtres d’identifiant (SerialNumber / PortId / PortName) permettent de cibler un TIC. +- Les messages entrants s’affichent dans Logs ; les messages de type `EVENT` sont regroupés par `(nom d’événement + identifiant)`. ## Contribuer ? diff --git a/README.md b/README.md index 297cad2..5a75266 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,20 @@ Add `--help` option to get basic information on how to use the launcher. ## Documentation -[TODO] +### WebSocket tester (HTML) + +A small, self-contained HTML page is available to quickly test the WebSocket API: + +- File: [docs/ws-tester.html](docs/ws-tester.html) +- Default URL: `ws://localhost:19584/` + +How it works: + +- Open the page in a browser (optionally serve the `docs/` folder with any static server). +- Set the WebSocket URL (or host/port), click `Connect`. +- Pick a request preset (`GetModemsInfo`, `GetAvailableTICs`, `ReadTIC`, `SubscribeTIC`, `UnsubscribeTIC`) and click `Send`. +- Optional identifier filters (SerialNumber / PortId / PortName) help target a specific TIC. +- Incoming messages are displayed in the Logs panel; `EVENT` messages are grouped by `(event name + identifier)` for readability. ## Contributing ? diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..b5c7366 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 Enedis Smarties team +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +version = 1 + +# A simple glob of all excluded file types in the project +[[annotations]] +path = [ + "**/*.txt", + "**/*.json", + "**/*.woff", + "**/*.woff2", + "**/*.html", + "**/*.svg" +] +SPDX-FileCopyrightText = "2025 Enedis Smarties team " +SPDX-License-Identifier = "Apache-2.0" \ No newline at end of file diff --git a/docs/assets/enedis-logo.svg b/docs/assets/enedis-logo.svg new file mode 100644 index 0000000..53c68c7 --- /dev/null +++ b/docs/assets/enedis-logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/fonts/Enedis-Black.woff b/docs/assets/fonts/Enedis-Black.woff new file mode 100644 index 0000000000000000000000000000000000000000..8e6d337e802100fb93c34c37ee2ab31d613a8d20 GIT binary patch literal 20012 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 PdQD~k0000100000d9GOz literal 0 HcmV?d00001 diff --git a/docs/assets/fonts/Enedis-Black.woff2 b/docs/assets/fonts/Enedis-Black.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b7fe54854e58dfdfd58f477efbe132710a58a9dd GIT binary patch literal 15584 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 literal 0 HcmV?d00001 diff --git a/docs/assets/fonts/Enedis-Bold.woff b/docs/assets/fonts/Enedis-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..863448698bfcaf2080b3340b4dc8ff20c127385a GIT binary patch literal 20200 zcmZ6y19T@(@Gts}ZQHh!jkCeV+SnVLo8%kYwr$(CZQJI?FTel2@11vV_o?Zr>QD95 znLekxYii0xUR)di0{AX+Cjiv{+G6c*UgW>K|GS7POG

    cE07%|C>N0M8(7b0D<>! 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

    Q68~$&CaU78Lr5bSB2S0lt&aM z_D7d*CRkj?vm#6GfVH1U-X;|mDnZ9a5XVb7!y74;h)E-T~xL zCO$kq{3rw-&PoQSBr~g-FLrt>4>k`my&@-b_@+4)8yFZF7*)*VZbNu4NS}#`-D*kS z3JP{@Z!Xy%GRLrSaSmi#EQxPW_)iIkU*Nwa$&sy&4)qUa*LweT{{@r27N^V+SVG4F zxM+}%|F83G4`5R$WjbcMA>7y39BUs6GUzLBLgFi&mEh$IVdU%a3oif(fCfY^{Ox?CjYZ8sEk>dDH$+cIFGqjEAi=Q4 zNW`een8l>V^u)}@9Kc+|e8fV;qW-QTSlUO>=Nu*95ft0950+q zoN=5ZTs7QQJXgFiygPgy{B?qULIJ`u!W$wzqCH|NVhiFn5^54%5;u~0QYKPu(t6S_ zG7>T`vN5tx@<{Sx@>%i=3PK7E3I~c{iVTWcidBj;N+?PJN<&IN%6`ff$}1`&DnY70 zR5etSROi&+-j{1er3akH!eZi0=kr&ZS{%@bkYsvPhV_7eH8r(^4J1woweRPMKe!Tf zKyV030C7MVG6iXk0x9iMn?RX*c3`6Os#Lp0GhH@mQ%o4Q zpvy8GB~r4_7&*J=zy%lFP(-o1!43OZg|IE zb<_x`!o;2)q@?2&^0^TdY$^1-Y=&8Miw6F&lBK%-7kwDNTQW-P_WDe-^I5l#9U^2y{Jbt#8nqWIo@!ZcXRxzRml`>VzAL|+*};m6Jp<+np0Fe1B!ic)CozoW zQ6OMlV~Js_wjdqrOqn*yKck8^+;5qNCnc*HzDLksJ|CuHp0>x0O(TeQvyszg~8w0uuKlwRIh@v8E#|@{Q z2X{-ac>LtDCxe-{zfSV^AGFT6PW6*tp?rdB z#tZ}*d%TQd%FIHbNv$AoC`EJYM2*15UIBzwA>{UZEio*)E0E3@@AR~gQ-MQDb6D~vNbSu@%^U)QN=J4t~=&}!~J-Zb4}q&b4&_T7zx3ifmpN23u|C+hL*3CJ#{g0X1O9_QtMUitZ=m} z7cg{iKm0%+VFZLKpEsw!0x9{Q?i*{`n$2t7gfnwg&dzIb;tU`|tC@zhEe$7(F@${V zreM3|!xsvNV^bgOMScuduxlD|LiarTqytQU>h~w?Q-;bUBZs6D#KiA%6aPUhXx%}x z+N9d{dOH|ZbJT?xK%65ZKB4zni;VY{S&=8ZmFR>7k>j?W2lS^#gK(mf^&~2ct@@!q z$5C1x9(Xgu^~hws^X~+{yFvV0gui3ho%peDx+M1F-OKxZVyFc*+B$+}H4*MvPP>EK z{yK2}+}&+y-@nTJ3|1E@*CMvZ0t0!E^s+YQDQ%Wd_0ER9aob<{rCCaRv ziB?;SH)ffu2&EUT24TUc*Ql8Z63{KfsL3q`EcGfK2dwDj<9{cxD*HhqI_LrRcw}n1$(A%BCry4_t zkXxVfwA~rQmiZzn{4#KJ$KHjA1;v^4BIInE#|NFW+s+q0ig|f8^CBZ-^WP|=wj8%h zpPr76#29w_+w}-KZbwYd@nEHol1CgIY^RITqr5nA85pzl%6>@_&-rq}XiYx4yDgCU zl#q0$jYWo<(A^dHk5kl7Po8b_d@@2GjB;UD*}_y@(?>Mzn#qdvg#FoDKEAn~MIS_9 zggivlLu4ZmblE!q8(vqfvhvS&z3J)!5XB-~h8Zbp%Jht+=s^P$U^iKq6l^k*Bv zAWXKnp+{RaLGul!c~s=yyMQ`dUtegZ^7`j#)1$3Q@URS^fjbcAO2!LkIt_07fH)4z zDj^6+hn!wxG%F_KKNH1(5TC7iD%{$7!${A}er?F(gOWl+rnnLOmFQkR^&}Tz+jVns zbgR6br}$|6%8PvXwqTzU>#%+*%$U0Sj%Tb;Y#*EVvuXX3Q;vk>BnliNm?`I9$?iw+ zwV?a;o441$)O*uY=e9jlL1n$)^767EqG&=M@#`R|vp~q0L7_Cx%&QFlOJ_#mUQmAo zu)3NVjmKlE1-%Smv3Wj-@VBy8RHYUGaiW7c)Y^E+oL^00%_+-dM$YXeE>g~KL`9~O z8Htn7(XRr-Go1$)Lqo%dz7^~&4myQb8lM!@#w$W34v9l(KPq-I!;@HF}wsf~t+A}?~aLv1U8(g^%9{{Wgklqr=xuuXZHa#GFX7e65DGHSt@tu$|fF#>^ig^@Lr(VgBG?mxiSU zCY!JK(J9Q=XQ*LB{$luw&4x$2UO)ZOvSO^J^72?pFYe;ihiajCnYybu4HM$v9sGh` zo*QD0Uqp36g4&xL~58}!foYCV+=F<RJroFGqUqQJv}RwTHGk5rD$stf9*o)chC7J4Fns_#BpF(i`ZdlV-~ zlas1Tk1sPM-}I|>{sa%pO1|>B6j9CF$jY(e zVWhT{jAL(`51ywr9Km? z5ArM7=mh~q<=H*-zin#r-Fm9s$WXJ*Z;t$@>U=phC5Hz>Yb1wIFSjIrCkcVmo?v1M z*%eGv6D%gGR6=SE(XP%8y$6Y=0-%6^7yeU_!wePD+tW!N4bs%0evSY+T70sF_jeiz zSxFPWBt;Lz@fc$T|5K_OM>LW5U@=g6qc#j2Lp_AD1ATJf9;TKIoE|wZQlNU-WnZ?L zXl=JL`?$r*T@xk1?(h2ELD$yQT76;o+L9g;pxJ)?arCYH&vJYA{L{D4Z+3Z$z9+X6=F9NebwnfY?} z>(=)zZ1DKANr{RDz_gB2`;VVMLu7(ZsJUER*<<1mWWIBDmTAJIzb?}hBeFp7)0tz6 z9_)CjhZE>@8HKNMLrq|NqCB-Ns@$`&{eyq5-56cwUB(}qF<0rSWWJF*!0|fdSXrMb zKR|)BTledc)W>+q=z6_vYbWw}2pzsEgV>kayd8QwBl^7D4uMSfh zJ4e>(e!aE7qQCDFp|F!;H0w12%O|J&(5~|ZiSpc7tZ$-=J3=myIJRH-*9_{)+EQN2 zwEIgTE;BLf$YHJW_48;>+pzDm?d7u#&3U!h6Kp+GMTyT#y`iE+XTg}+WvTfw3JW7K zAp&cbZMWu?vD59(=-t%OQq*Jlf;2la;=QfmUML6 z-@U1$v8|HJ*ND@s$rPVcgva>|$?7Z{EgVys9hn6O9v?KfcC){O_<~3-{D!n#qavaG zCiYaV!rN)38gdKG#Ju=0tbT4TncTIO&!K4T)=;Yg1|QADF^${ZWA`G{Xm0#2wpFQ--|jnBHE8xAc<2huy8 z#QZIot#Yi+ONaBGgM(-84`*0OF8vu@rGj(Wpo5?XOleC$hjG(%t;C|Sqwy_ZT9_LtNRC@u&E37V_3;}i87!Dga~$3^3; ztgqwA=95onr^gG(H$y6BL7DY{n^ImI{sf(JJ@KKXI2X(nnXM%`zWo7@RW#tWMpg{npf9ST3Ff56+={S0=eR~nTC>5BfS?C zQF2*9AMZ7pcAJZKI3jF06z&*2lLM1S;(vir04D7YwYD~7+s?4Awb^(VOg0=9>A^q+ zX96wz5&D^^yJ(E_e-c{EP8<47GJ!(8clmgbFbo0N3x zLgo1h9+QTeimamw9hPx}#s^IZD=Z<4fl1G=p_CvQLqg|>*hg%NL1Lj9;pnK&&ZkQ> zD24RC{ouA`L6Cih6OD6{;DH(uz71p~T2WGA7rv64Zq6fXU613cM)#+RO^(kS`K{tj zfQD+UIQkTh0i&xaCC{u(z3`uuDD(L#oJG(s=l0C@bKM9EQBZ6(g_QIA<_o@;=VZdQ z+RKA&{(n2!U#u**yb+W>7qtNPzytAm;|RGE157WOOTLLbph*v2NTubsr(ILx77Dd6<#AlfgL1ES!g;g&KUU65bGC~dK^n)r%TU)qBk{$kV!|`Qfv&V z)s`j)=duDabJ|C+p-D?jQYWyM42CE|i-BVq{-z6g(+Th7KiY>5p{>DIY!DZl`$eI6 z<%5Co>X|@KO$Uz=2>Q|m>Mg7qG>qXg)MF&o6~%@bgBHiJ+hUlcPhR8r)5T0qv!JHW zuGrXH&&g5sNn!uy-#@#31?Q)sF^I*Jno=-(GCy~lNQ9GCm84{~Jk24stKQP>3FDrx>1%g=s~v=z(9;ieRkCJL+(a@ zy5)~T+<)l)JDzCNknJHuM9#CnYN13%Z{`s)e`3<1jksK^>-JpS3F5V}EY(og!DEoE ztc3AHCWLynH#-hm4A(-Cx-RQw;9?3{2q>eaEwi*ZSJB(>3yQ}Z=h3K+cPt}1mg`?g zNW#FhF?~%a?oYONY6T2nMza=DFKDB4wGSsVpN?$c}OX_4ZxWWJ4 zd(GfR)!*QBS90%KLFtE1MHz;mwOw@&1!i=e`F1GkgL}MacHX8ql+Jc6cm$CgXG-UV z(?m4Q+H_-v_#Ka(3O%i#p2M4)i`xB-d6(9#!)Cv@j!ljHuDiOoE?wL{KE&f^)3m9& z9tM=vd_?#pCTR*z-t6Ftkt3B;O`U;A`CIwGssGcw_APq%YG05b_RMK_a&uQ1e$M*g z1>2lX|NY!Y?9@##BZS;Te5A_ZE_Ed5PK_sBF#lsUG=2P(awvE06&U`Dj$vvZ)^e!n zi(3u+AF@bOd)Af1W*x)OFBcj7h(qod;l29H>BiR_^*0xA*Re(WwWgRwn$fea(Xr(>(jN$ zd?iEP?9bDLtR+gNOA>f%T8ol0!gz@Y}?Ib7Oa3ym`d9rt>=4H zn`B4mMqF?_hdVjU9GBYT*@_lw1x;PiLLtIV>5myHgbM{*jq420gbwJ=ao9|CH4R&w zY57{vSM+v(opL^>T{3uV!=IH1rxj}P#~yMOP^tBog!`*EB7H7+c=W0xo&plgT&l;F ze8IEtps!&d-`lThEJsl)J2Tf&W^r%{340-C?@8npCN|0vf%E_g#*1xhxgOoP#6Zh2%t7^c zgfpLk(_K(VPlN0Ch==`PM{X#pt0QVKO~~OAKTTIeT57POcjJ)T8!Ixh45R%ov{pa; z@G$quN#k))MgbKLNL6>WJA*wL@beQm;_-* zxediWd!>hg^SQ?s=0!h=MZ!(-$##zk+h7wIfyX}X)Bf>6(GK4>8P4J~g9*wl(O+AK zn01QbPBA$#JvMe{0=yi=;Ze^#n5lHf`QptKKz{Q<{S(M3kf(Xmo3yEFspMtw_sT7N z_2O~bg^Bv>a}w?1Zb;YGxNOnT5zDC9)qxiO30}*hiH^$U4neNaB;{2{*W6gN3@#wJ zUGk5I-U$W=>u8--LZowLT`1Z}agXD9X-&6^AJ zf|Hmr)OEVdP=(`uDCyV)o*#y2B?WUm9a`ZMZY9h&$6^w^*t;9NWy@!OvUR*;4{7St zr?Ox=xrs^#LGCPEF=_<^Wf#3-bG2SAXC zb~y9?^#s&XGZc4_W7e~gl6jp;jPCBBqSC|gm!3Gc&jf?l4-Mkok;U45_SMzhfX=z%S-&NPAV;BSGjGf`L&H#xoc-PWrgxfw^meD zP6WeEw78%!L5nIiyE(rhYwvuSVQL^+d)pqvY;j&rbSx}< zT3{Y$>;VGs^!pQ8+ouP#gaRJIyfqOQXt}qU`bS9E!cxDfZ>k=o?1!YGu4?cp%~|=v zFm$p2Fv9LMU(p~U+MqzTn9m}n&kSJnV^JzJmAHn(Eg=?0$ej=%-uLSlw$@A$;ALH| zhlpQBVzKskfh7`ef3v_3 zClCfeU>!1cjc`HyhsY)F-ueC_4S0UPe~5T;c&sdV!1s!TulwUDQAZAJ=6fLp zm!fXM8Xfh~a&u_o2=q^`2Jy}uy~?&+O2t;VIa`WHS}C8W(?r!nS@yZb;9*}xbfzkM zsg_6QN&-sL)}LN;guvB#1cG!?`uzF|{3)Y8A2+}Ovct0^`t9na--ZehrC)coQf2vq zQ=~d^0E5*ghn~!nNbvO*;pZ5Ze4rfFy)xv)*2(~+o|!*oHcB-Z2k^@sJSOA@i`#9l zfTnvwlKdBog11u35!BerplGHmKO6dUvEaEea&&XN@>;4=bacI4ogSD?QVsOFCM1e; zp5x=JI7#4TG#br~>tAW{5ra+iPwYrTZ?>cXZMt9xDID?HGo-I$RMPp-$T=_m*&a6_ zbL`RE!7Va%y`T7DniIcK!CquyfX?7-1RAemxL&dO^5u_X=kw5C9S9zra!jv(Zb{2& zXN8|szd5vH@tQ8^HEwpoIJh1ew=K%y7fco#Ba91HJIuv&eh&6dG@vO#+l^^e_}`)hIK1Lt^c5|t&tsaSkTC{ z&vgzTJak%N;0$NLuvCa}C4+!Q^n=I5ftJk8Sq}}|*&+Tqaa#Gx4+>&COGhxBdLJPQ z{nu}WF0ejC_pWnOlJf48#p?)JMB~p9fVvbT=~1KS9c4mQHE^ge-kY2r%Sbs2@iOrz z=7IE30Tx0g9vujV4@@2Nx?D9b zPi;Ov%)4y9Ot76LpX5)5W3p>@**(!W8kt5K`2gs%U&EMTgWPCGNjQvDoOpgQ`qSeC zg$l=j1bPf{F%#WbwHx1crVwoBNtGKi^YI7?7pF&{RT|iV6fMXe{k0gnA?5EVr$_d0s;pI zQq{*Uz}GT3GI3S z`pt_mJZGNc>lY`VCJqrz9vbfr4FKC!L&k#pw;GxSEY}NYplltH zRY0x^cDf4DEG^G^L))Ht-fXQ=n`inmFzbFLxVhEU(pqZ#anyS_zT}kEcK+a5e50$_ z-_`U>rJ#yIn}Cot6>Aav=fZ4li8q@r#ob#cFl=-H?{Ylt4f3{4(I#uTTQauu<&$re z`0(iE&T0D;EXMIvzxHBz#n!71CIXQyX=nxkHnE_<*)mMA$1GLYB+tycdB7^s!)iy5 zRdL1jlDpH;mYt4_jEt9zmyXR-H*FZ9+-2oai-NNp0h{6^+$U`}I;W3Fj>uc69SnB} zFYO86dR<^`VdgDSAz}%%k{@;@>{g!rc^ajd2PS`t{NCd&U49Di@8#-LVNU zjvkUSX*wo_-OD(Ja>L2dr=D)BGr!svO5pR}{nc6|x3;c;8=Ia6#<}F!9Er`@dTRqB zdQ<}S8i&=;UQCRIZ=qBQ**8$x+emGd&Im+NlBn;N945c&?2=B<8qOPWP04eLV$#mnfo7EC|CoxGk>u(L5P!-A(R7DtC z0iMOww6h?tPz?OwHdUpzy5YwIukRp>j`3U8`%kIum~N9!gIupm-~(S`Ss{7&WZ7>5 zz95Zjv02(di+Xa&#mP!nq*vP+IYhb0TCG508WM@TB6ix49ssOnyGhnEny+n;5QBoo zUh;)ehY=KNfE5Tx@$zo|@6+(bqr^UB`{hQmkt-gk2l2_%)>2Ma_1g>I#yIcG4?gx; z9mkBHZY8$UpURozY59=|-!W6ftJC(s55Kh}9Pj&*Ms_!wuW>f5TlbS*-8xFBcEvhB z34(Om8Cc z)&F|QaS$lz<%KUulzDcY4ackl;-z}X{S(B(F=%nAN0^waj8ns33Y;gc7t6bDy8OWq zb5Y8(YaZbIJ(=A_LQr)#7=cut)_3x`HK7O$)%;1R#sMnO44eJ zC$qemP03EFX-WICoI9{s>mW*nESjdA%)Tc6byymBBPx>mbIc`J!l;bk4PXy6IUNAA1ykD2MnCg0zGO@>@n+TEuEmo8a6vl~j=3R?0%Ev3Sw z#_4{xBU)+Kmk3MZUTnDD`)=+kXq@^f4el>=Q6f(LbSJ52OzhJ_V_AJ+ z3i7#}G(6Zs{`Bi1%Xvq9|E%5q+LQdde9HKmq?xAzQ?K$acUWhp1-X@Ha4u{?9St1_ownKCEx9fk2R9M zC6q`1Yz~YOtF^LG1fo*t*|GcUACEGyl=b})sY$6OWZuT{+wt2JbEQ8RJ|Y|H5L%75 zQE%OEQiHeis>e@-*{hCM3d-kkMMV_SGQ&vtU(LquWD;+@Oz%T=zHa7K&Ji7QXZc$|83pf0=Et()KU`f9x{tF69j8Zk6?~d| z?0&*SY}R??fR|!1AFo*zG?2i+(B?HXS=Ff!UvS?YIdsHL)8V13^~S&VrdK>QUs=Ag zTERrNC1q8%C1LDEP4qTL-rMTkhQqIi{+Nd6d5d6R^wdlYl_jp#Ed}78 z$vsv70QVdoYITy-nk=1kmG2GnARQ%n)>5e9)>6(EPY*>W6lK14+;Mhl%)S04?fakC zoqU~i&HYjA3|2aQjMHmLWG!SI2AV3-tM+S>-KFC*5oWl_??%LO5>KI^30`a_d^Vw@ zH!Y1T81aj!p)wzm+#|=5AercZ>;3vPBp#x1I)s!`<&fSIY{>m*GXu0p@mxJlbHP_% zvC8`PyPRCF;-yuaJ||YhxVKPMD1t|IdW^%tz?aXA3Sidnc8bBcy&io(_abu{fA-sB zA^~9K*}c_u*{I?3aB>DgF{oFDFoX?Tv&{OKt&8p2LS!`dU;XJ_E00GsFBfAF;?W>g z*{uEMQM==e2OQ61c%~vsyqm4vzbxJ+W{+vPxg3_9wsi2|uy9AyOgY_zYmJs>CrLW# z<>zN*<>%)h(fDVSU#uT*)NEi%^O}eUhc{u;KPyc0+YrT7=A7ID&beRmBK3z}44qJ2 zc@zIuQfOpVOk8ACZqg-Hms&E)jmLNi&#(V{oH8;@8{Z$Du+pn_Qk0Bj)3E3LXUn={ zaeb&M*~QJ2I(wV#b3d2t zv;_IVlUK{4-fZ_0_uy1KAGj)v^mm@5O8Foh5`-?Thxu@qQDO|c^m8Bb%PZUuAtrwaNedykwPqz)V@53#bRd^sPRd%UlFL8T{K{%&&}PH z0bdKj)7^{pg;p%nNn=sxinrYZLWlCSJUmBj}b1%q)mX1m2`#-#+(yF>cQ!W}z`8ScZ3fFT50c zk{QPd5SNS6ZTHayO;6%t!IbJGnTk?QwM=l4R)Th9z9#PaezgIg4<&{Y)Q}GQRr*kR3AZ7oT3=^vDH!otGq(!M_xhNn>a7vR5>LS8Hs~qymFW z8W~68$q+~r@nvHsmylC!f-Vyw=u!bDfq&wTKAdPNgfJ7!ds)Kf%;va#TA4aT*5$K znn!AQT%E398@^}smeh6|_)X~Ro=rZW;mPS(wMtaP4w{~+ppazH2b|x+WVY$`7WhU!{X18sXCdNvqR=4?q$Pc!_I%oprx;8#rcaR$;`UB5H?0zR3{M1 z=M@%#`P4_&@G3}$SXcxWogaIH>$ct8^kG1-+!^uikkvJRea=5p|0;~o@8QP7Xv>IJ z8{cLnnm4HWk6!{oddcN|j34!|Ef;mae?I>&$8Az>T?jpJIv|EfJ|*uh3i1Plai(z+^$ltK3S_$wB5fBuHUOJb8#Yu{iElmqwk8WR?LgU-a z4yw!xWX@~{G#tZlC3+eA=5il@=9byoehlv2KH4h+`JGGLJ^XOho~rgsaA&#Zpa@+fuDBVGx zhwK_CK`6X7@2l+8JBbn%CyNNDF##OGF{p#V-aI`I^uX&EhE`?N5Tq*0-b(8YT_H zI#;2MCP68Twh%RFAS7jovk#RZq@yI3S&T9i3?~kI#;QW<^v`c|byqfx2-={*=9jh< z2>;hdm+8)X;5#fb@WM-NczeP#!|{seIO}cmS}-XjgndlbK`0Li-}PTk1K;z4^t5#P zKuIz&^1Q$s59zF__x;}e+@CKcg20l*04Hhxh5J%PPP51nId^$J{_ev4>8GtX#OL5d z@8}LjJt>+wd>}t7?hB6zHy-1|t=|QHUPEj&^yaIXMz;#Qz@^}y_kxvoHu^jmPe)j}Qpq@a22@grJQD;UH zQ-KQSSdsp(H9rt@q`EnrhtW%6MQ14URWx)XTV@IgzX*7Uk2^1B+Wi@*hRmGl(w=0t z-U1m2iWj~XV!fVXLOB8LJBNfdF26c_|Db;YQy=L0dj`>GYm*KbahwqI4Mj6X8H8YS zV9}MnRLgUgP|rFumoqkINKRCj?ycJp-wQyznRA$Z@$Mr_hrq5^(5rlIeM9EY_4@@` z2(~*MT)fIi0SNM>*c7omq+py@SWk~G43*Cct8S?#tT8VLemQWok>^D?SOVcmb+iY-G_S#?q#@NfO`q<({L}rea3Mw z!#xLgGn>*J&fYwm&FAbn&Qp%_)FPZOz&WFw+oc8n8tz@eAtcNop@?UUlp<1!PD&9e zMWhswQbbBo`iQAl@b#srO=hRA{F!l^Q(!`04qzuxGN2iIT3|=)B%8(OG%xq$G8*$do=uYk@ka|+1%FMV7 zH(zx=NBw!eo7u+DcO0EYu+<3sOIkBS%@kUesm*FlhME-LCbUm7jNA^Dm8W%v))KNj z^1&uPwc){bBz92K3AZlQ#Ax87yHE5VW!xKh=@>LfuOZ0P7OHB?TAPG-0Z$h2WP#d@ zyoZFp)8j)ZNuHQy+yvvYj59f7g7uvxFZg7nqi}eG{>Pwt>4)c0+TK9pW6&Tp1PwFp z6m4;OcznykzYPB}TuQ1}xhkua=uss1nc88bH=#p%UgpWrQa}&Gx5T{j%-e%o8Jz}o zTt+o)bo!^O^iON+xJH>7m#1gmWLms52o0+Ze4YO!yiOzOEIrO6p-%M}auST~W~eE` z-GjTw_(@uQ)q4%TO=)~~Y1S(fmnP$tiOVuEXd=L5HBVRMIliA~WOU<2vwyViZ2efa z%Rct0vaIqI5L3ZSfGxO}R$CkN84pJA)5^*$>fb5%Z56B#4B&%Ya47JCY1W{v8((1^ za`vyagN;mL!j%=&-~#JxRs&B z%3M*qxaYNk*{`c=YyP~xGG~(SzKHy6wJv6jTAS$ksbsTsjby9MgMRmI-cD(ro`O{P ztw?N4QfE$@CT7c=HOGmfN%WgR)9K67zg1Q%=Yk*&=6UN_NWU@`cfb-LkZ|=iNT>q3?9{^J-Vb@sh6^qvW;>LMHCq`8n(+7=usEyyawrQl9VRdEE}|D zovf8jauuJ}GoJ|3&ZC3H&Lq5FTQ%!S-6r^64Yzjm+5v~C+ye(eYoqxZ*@Nu4SBtyf zDK{f^o7^JXk$*3vOn*VHEzwZ=+$-uzU07m_wWX!1%#~@&+OVu!z_$TE?2^6Qw_s0u z84Gtno!EAR^FTuX8jq}3YNRC9noWv7irlL0H{gq#@ak=j*Y$EcUcDP{UMKhAFLUjd zJLE3))sOH+gcvbM<4=>*k!$OlliA3D$V-v;BA-XTN@k3^ET|z&CKtm-#$5jH6E3N#KM#E4EF!6JdY1vrrgJ>I!uo@ zDfjCt+RrLFE)OcAVsOB+dsFYrF=$AUhzy#wu#USRgW6e4(js?#v<{MGEFNI-iY9n@ z(m}T9gjW}2F^^mo=21`#)Qn1Kj~+%C{H7nCgRZ4fQKhZa?of={e+3mJ04<(6kwN8kEIo;whRNb-0YDSg^5XX;Cz(V$l)BJPZzZEj#88_K02V zFb3}!WG2H7W%`g28(r^yhs&lJF~x{9BQngj%sk7A+*#DIUtVDbuc|Hk;gp6`0c#cD zmQl>50JmI-!&)rUc(lamX-4Io{GvmlywDqdl{d1^8@V~&cv#}>ph;Sqpcdw{k^2>j z(jAg+?mfugf~|X*^H%QLxNlcXbrkL?xmj&7rnvAO_}n7T6>QZYxH-9-e)rJtUh4KC z?|x`M^Z@iAG7dnGLXSZ|gAPJJXUs34UqVknPcr^j&{NRU(66Cqpl6}ypx;5yL%&Cd z7oa~tFG7EWUS=-U`0io84?@P;Z!+dBXass25BwFKQ{3O-{w{rvL)Nx=Y&%Jo8dj8X zx6aWb`$sgEGDN9C0E^_xO+-o?+%2kUeo2tRq9uoK8~+#7I!737F-KqVwjo9jYs~wK z{>4K7_bJXdM!q+QwnekcDrt0CB$+7B!@*$KW_4xV>MDh+%i@439ns(aJuRU2>znDlI2e%Vw7m3XCW)qM&PS@eyT}cLS>^ub%3FcPBiI##^}ex^=kC zt;L(6yPbOz;Yis+%(S7S=54 zds97M8DwpAnWa_UNKIM{w>B4NuOD<8U1sSuiHem)IfEf0Z$RGz*tZTf`>g>o(&UG< zc_*T4dBVox@}oND+;7&beV0^0OP5=UljWPYX6|#2cr+`wMsHnf|28*BnHenFVlkIx z&AuVBpRRt_RGZ6U5;d0LT}r}oMxcHB#NXm`l^t(X_431IOU200(hA3IQ*spCP+BS|AMid?To@-kXi0_O5moFAXsXZ|mT)|;J z-{Xny@-q1k$yI0)A{|Tr(uzLi*{oGnpK&}C1q&{y#g|<7v{iP(AN&hT_Z!Vn1AX?n z1P`Jj6^v321&36_e#LcsU2Ahj(Uz(pG>W6771=)&wjWZIslbzCk$jzp=a^b8rx|vs zY*Y&QrOc@@M5fANrt%FEr_aH@B3ny%%^_k-=W>6Jq|y?Se3xG?CCQE(S!#!!wHtCK z=Si|!n&0O5;je`*@g$;SMv0e2qs8f6bWhVuS^{5*RMQW;dZbY?fvXi0Xje>Nvtj~WiV1WpCeW*xfcb^| zL)1PhkINp#1okT?@Dq87@`z$nFGwfcH!JQHaVU{Nf#Uk9TM|gEmj(QQ5VtvY6-elpluE1#|~|DPk`aINjqq1c+ie>z>+OzCo^`s#M2CoEgS zwrkV|w#;4@uQO%#+U^iIgV;5?I(xr9?9=FI1G+b$^%Zc9sFX&zPEn);`X`v}P0V5L zm2mf)nZ+8_@mB0}8>38r330W?Q2N}f(UpdOyJqN>c5Y0H|hv@GD8lpPumNi4oI zw1xGNMU%SK$8qQBCOl}!poIqgTSu{|;SJh;CEn@8=Ql1oi-lL){{WK!3X}iVspR&a81Dh|@0;i}cK`2Dl$ z$?07(gcsg(&%?PlZw>}1Gch5~N$(;iOa_yb9X#PIm$*^_D%5e0E>9zNkNa^%Gwz>9 zbnS#;olwsn4r}t>mhD`8HmKUpqr@iL)UHN@A!(5(H^YQG^?UV)^!9qhw8+QfW{amH zm;A2ClU$JfZRU=;QTIhgl4Wf`P1T^oO!RDL0-fvXo&6kZD|^&^t=O5oUKaVXle%oO z!2yT-NQMP-ezm^wr}f?PPs_h7|JIDJeB{1W5oUOiTjaP=?fnBIdQD~k0000100000 DK0KN$ literal 0 HcmV?d00001 diff --git a/docs/assets/fonts/Enedis-Bold.woff2 b/docs/assets/fonts/Enedis-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f7efc5c152687505a629f46fbc47f6757936be33 GIT binary patch literal 15596 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 GWt|Ou8S#Dq literal 0 HcmV?d00001 diff --git a/docs/assets/fonts/Enedis-Light.woff b/docs/assets/fonts/Enedis-Light.woff new file mode 100644 index 0000000000000000000000000000000000000000..168b1f20de48c4b51b8c59c06ea7bbd02a77cd61 GIT binary patch literal 19904 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 literal 0 HcmV?d00001 diff --git a/docs/assets/fonts/Enedis-Light.woff2 b/docs/assets/fonts/Enedis-Light.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..aea44b7042c2ed9b12dfe3868092a09218f17b6f GIT binary patch literal 15332 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 CrkXDR literal 0 HcmV?d00001 diff --git a/docs/assets/fonts/Enedis-Medium.woff b/docs/assets/fonts/Enedis-Medium.woff new file mode 100644 index 0000000000000000000000000000000000000000..22b5e6dec8ac729fe2f1be1eab1d5ac4e4cb57d5 GIT binary patch literal 20188 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 DQrmZa literal 0 HcmV?d00001 diff --git a/docs/assets/fonts/Enedis-Medium.woff2 b/docs/assets/fonts/Enedis-Medium.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..21e5790f503fbe0a86095a73bf1bf02c6624f2e1 GIT binary patch literal 15736 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
    I`uiBT>)K=_H|RoFTP}1;2LT3F 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 UQSJQ$BYI6{000000RR91003w-#Q*>R literal 0 HcmV?d00001 diff --git a/docs/assets/fonts/Enedis-Regular.woff2 b/docs/assets/fonts/Enedis-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..27c7cbb9eb3d8d7267bbf0354208682d096a913b GIT binary patch literal 15828 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 literal 0 HcmV?d00001 diff --git a/docs/assets/fonts/Enedis-Thin.woff b/docs/assets/fonts/Enedis-Thin.woff new file mode 100644 index 0000000000000000000000000000000000000000..153266465064722f2b038bc2bc159e65fd18cacf GIT binary patch literal 19856 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 literal 0 HcmV?d00001 diff --git a/docs/assets/fonts/Enedis-Thin.woff2 b/docs/assets/fonts/Enedis-Thin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ab094640777fce4b8dd96f2e2d0a983a0dd1749f GIT binary patch literal 15264 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 + + + + + + TIC2WebSocket Tester + + + + +

    +
    +
    +
    + +
    +

    TIC2WebSocket Tester

    +

    Connect to ws://localhost:19584/ then pick a request (e.g. + GetModemsInfo, + GetAvailableTICs, ReadTIC, SubscribeTIC...). +

    +
    +
    +
    +
    + +
    +
    +

    Connection

    +
    + + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + +

    + Select 0 to 3 fields. If none is selected, the request is sent as the all + variant (except ReadTIC which requires one identifier). + Priority: SerialNumber > PortId > + PortName. + Lower-priority fields are disabled when a higher-priority one is active. +

    + +
    +
    + 1 + + +
    +
    + 2 + + +
    +
    + 3 + + +
    +
    + +

    +
    +
    + + + + +
    + + + + +
    + +
    + + Disconnected +
    + +

    + Tip: open your browser console to see JS errors. +

    +
    +
    + +
    +

    Logs

    +
    +
    +
    + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index bd8b907..30ca7b2 100644 --- a/pom.xml +++ b/pom.xml @@ -7,13 +7,13 @@ SPDX-License-Identifier: Apache-2.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 fr.enedis TIC2WebSocket - 1.0.0 + 2.0.0 jar ${project.artifactId} @@ -100,6 +100,18 @@ SPDX-License-Identifier: Apache-2.0 4.13.2 test + + org.skyscreamer + jsonassert + 1.5.1 + test + + + com.vaadin.external.google + android-json + + + org.json json @@ -110,6 +122,12 @@ SPDX-License-Identifier: Apache-2.0 log4j-core 2.17.2 + + org.apache.logging.log4j + log4j-slf4j-impl + 2.17.2 + runtime + io.github.java-native jssc diff --git a/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json b/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json deleted file mode 100644 index f15c099..0000000 --- a/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "package" : "enedis.tic.service.config", - "name": "TIC2WebSocketConfiguration", - "type": "ConfigurationBase", - "attributes": [ - { - "name": "serverPort", - "mandatory": true, - "type": "Number", - "constraint": "MinMax", - "min": 1, - "max": 65535 - }, - { - "name": "ticMode", - "mandatory": false, - "type": "Enum", - "enumType": "TICMode" - }, - { - "name": "ticPortNames", - "mandatory": false, - "type": "List", - "listType": "String", - "constraint": "MinMaxSize", - "min": 1 - } - ] -} diff --git a/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license b/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/EventOnError.json b/src/dataDictionaryDescriptors/message/EventOnError.json deleted file mode 100644 index a00674f..0000000 --- a/src/dataDictionaryDescriptors/message/EventOnError.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "EventOnError", - "extends" : "Event", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"OnError\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "DataDictionary", - "dataDictionaryType": "TICCoreError" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/EventOnError.json.license b/src/dataDictionaryDescriptors/message/EventOnError.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/EventOnError.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/EventOnTICData.json b/src/dataDictionaryDescriptors/message/EventOnTICData.json deleted file mode 100644 index 180d5fd..0000000 --- a/src/dataDictionaryDescriptors/message/EventOnTICData.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "EventOnTICData", - "extends" : "Event", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"OnTICData\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : true, - "type": "DataDictionary", - "dataDictionaryType": "TICCoreFrame" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/EventOnTICData.json.license b/src/dataDictionaryDescriptors/message/EventOnTICData.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/EventOnTICData.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json b/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json deleted file mode 100644 index afa0795..0000000 --- a/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "RequestGetAvailableTICs", - "extends" : "Request", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"GetAvailableTICs\"", - "removeFromConstructor" : true - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license b/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json b/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json deleted file mode 100644 index 2846109..0000000 --- a/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "RequestGetModemsInfo", - "extends" : "Request", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"GetModemsInfo\"", - "removeFromConstructor" : true - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license b/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestReadTIC.json b/src/dataDictionaryDescriptors/message/RequestReadTIC.json deleted file mode 100644 index 30ca65a..0000000 --- a/src/dataDictionaryDescriptors/message/RequestReadTIC.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "RequestReadTIC", - "extends" : "Request", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"ReadTIC\"", - "removeFromConstructor" : true - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : true, - "type": "DataDictionary", - "dataDictionaryType": "TICIdentifier" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestReadTIC.json.license b/src/dataDictionaryDescriptors/message/RequestReadTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/RequestReadTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json b/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json deleted file mode 100644 index f97b2fb..0000000 --- a/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "RequestSubscribeTIC", - "extends" : "Request", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"SubscribeTIC\"", - "removeFromConstructor" : true - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "List", - "listType": "TICIdentifier" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json b/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json deleted file mode 100644 index fd33cd9..0000000 --- a/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "RequestUnsubscribeTIC", - "extends" : "Request", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"UnsubscribeTIC\"", - "removeFromConstructor" : true - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "List", - "listType": "TICIdentifier" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json b/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json deleted file mode 100644 index cb53f58..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "ResponseGetAvailableTICs", - "extends" : "Response", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"GetAvailableTICs\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - }, - { - "name": "errorCode", - "type": "Number" - }, - { - "name": "errorMessage", - "type": "String" - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "List", - "listType": "TICIdentifier" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license b/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json b/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json deleted file mode 100644 index dbc7c86..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "ResponseGetModemsInfo", - "extends" : "Response", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"GetModemsInfo\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - }, - { - "name": "errorCode", - "type": "Number" - }, - { - "name": "errorMessage", - "type": "String" - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "List", - "listType": "TICPortDescriptor" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license b/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseReadTIC.json b/src/dataDictionaryDescriptors/message/ResponseReadTIC.json deleted file mode 100644 index 9498969..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseReadTIC.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "ResponseReadTIC", - "extends" : "Response", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"ReadTIC\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - }, - { - "name": "errorCode", - "type": "Number" - }, - { - "name": "errorMessage", - "type": "String" - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "DataDictionary", - "dataDictionaryType": "TICCoreFrame" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license b/src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json b/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json deleted file mode 100644 index 7de0ca3..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "ResponseSubscribeTIC", - "extends" : "Response", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"SubscribeTIC\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - }, - { - "name": "errorCode", - "type": "Number" - }, - { - "name": "errorMessage", - "type": "String" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json b/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json deleted file mode 100644 index 834ed75..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "ResponseUnsubscribeTIC", - "extends" : "Response", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"UnsubscribeTIC\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - }, - { - "name": "errorCode", - "type": "Number" - }, - { - "name": "errorMessage", - "type": "String" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreError.json b/src/dataDictionaryDescriptors/tic/core/TICCoreError.json deleted file mode 100644 index 04569e7..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICCoreError.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "package" : "enedis.tic.core", - "name": "TICCoreError", - "type": "DataDictionaryBase", - "attributes": [ - { - "name": "identifier", - "mandatory": true, - "type": "DataDictionary", - "dataDictionaryType": "TICIdentifier" - }, - { - "name": "errorCode", - "mandatory": true, - "type": "Number" - }, - { - "name": "errorMessage", - "mandatory": true, - "type": "String", - "emptyAllow": false - }, - { - "name": "data", - "mandatory": false, - "type": "DataDictionary", - "dataDictionaryType": "DataDictionaryBase" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license b/src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json b/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json deleted file mode 100644 index 73b606a..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "package" : "enedis.tic.core", - "name": "TICCoreFrame", - "type": "DataDictionaryBase", - "attributes": [ - { - "name": "identifier", - "mandatory": true, - "type": "DataDictionary", - "dataDictionaryType": "TICIdentifier" - }, - { - "name": "mode", - "mandatory": true, - "type": "Enum", - "enumType": "TICMode" - }, - { - "name": "captureDateTime", - "mandatory": true, - "type": "LocalDateTime" - }, - { - "name": "content", - "mandatory": true, - "type": "DataDictionary", - "dataDictionaryType": "DataDictionaryBase" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license b/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json b/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json deleted file mode 100644 index fbd5bda..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "package" : "enedis.tic.core", - "name": "TICIdentifier", - "type": "DataDictionaryBase", - "attributes": [ - { - "name": "portId", - "mandatory": false, - "type": "String", - "emptyAllow": false - }, - { - "name": "portName", - "mandatory": false, - "type": "String", - "emptyAllow": false - }, - { - "name": "serialNumber", - "mandatory": false, - "type": "String", - "emptyAllow": false - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license b/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/main/java/enedis/lab/codec/Codec.java b/src/main/java/enedis/lab/codec/Codec.java deleted file mode 100644 index 6beb320..0000000 --- a/src/main/java/enedis/lab/codec/Codec.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.codec; - -/** - * Codec interface - * - * @param - * @param - * @author Enedis Smarties team - */ -public interface Codec { - - /** - * Decode type K to type T - * - * @param object - * @return instance of T - * @throws CodecException - */ - public T decode(K object) throws CodecException; - - /** - * Encode type T to type K - * - * @param object - * @return instance of K - * @throws CodecException - */ - public K encode(T object) throws CodecException; -} diff --git a/src/main/java/enedis/lab/codec/CodecException.java b/src/main/java/enedis/lab/codec/CodecException.java deleted file mode 100644 index 5e31d75..0000000 --- a/src/main/java/enedis/lab/codec/CodecException.java +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.codec; - -/** - * Exception thrown during codec operations. - * - *

    This exception is used to signal errors occurring during data encoding and decoding processes. - * - * @author Enedis Smarties team - */ -public class CodecException extends Exception { - - private static final long serialVersionUID = 6029680699870915485L; - - private Object data; - - /** Creates a new CodecException with no detail message. */ - public CodecException() { - super(); - } - - /** - * Creates a new CodecException with the specified detail message and cause. - * - * @param message the detail message (which is saved for later retrieval by the {@link - * Throwable#getMessage()} method) - * @param cause the cause (which is saved for later retrieval by the {@link Throwable#getCause()} - * method) - */ - public CodecException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Creates a new CodecException with the specified detail message. - * - * @param message the detail message (which is saved for later retrieval by the {@link - * Throwable#getMessage()} method) - */ - public CodecException(String message) { - super(message); - } - - /** - * Creates a new CodecException with the specified detail message and data. - * - * @param message the detail message (which is saved for later retrieval by the {@link - * Throwable#getMessage()} method) - * @param data additional data object associated with this exception - */ - public CodecException(String message, Object data) { - super(message); - this.data = data; - } - - /** - * Creates a new CodecException with the specified cause. - * - * @param cause the cause (which is saved for later retrieval by the {@link Throwable#getCause()} - * method) - */ - public CodecException(Throwable cause) { - super(cause); - } - - /** - * Creates and throws a CodecException for invalid value scenarios. - * - * @param info additional information about the invalid value - * @throws CodecException always thrown with message "Invalid value : " + info - */ - public static void raiseInvalidValue(String info) throws CodecException { - throw new CodecException("Invalid value : " + info); - } - - /** - * Creates and throws a CodecException for missing value scenarios. - * - * @param value the name or identifier of the missing value - * @throws CodecException always thrown with message "Missing value : " + value - */ - public static void raiseMissingValue(String value) throws CodecException { - throw new CodecException("Missing value : " + value); - } - - /** - * Creates and throws a CodecException for data inconsistency scenarios. - * - * @param info additional information about the inconsistency - * @throws CodecException always thrown with message "Inconsistency : " + info - */ - public static void raiseInconsistency(String info) throws CodecException { - throw new CodecException("Inconsistency : " + info); - } - - /** - * Returns the additional data object associated with this exception. - * - * @return the data object, or null if no data was provided - */ - public Object getData() { - return this.data; - } -} diff --git a/src/main/java/enedis/lab/io/channels/Channel.java b/src/main/java/enedis/lab/io/channels/Channel.java deleted file mode 100644 index 24d52ad..0000000 --- a/src/main/java/enedis/lab/io/channels/Channel.java +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.util.task.Notifier; -import enedis.lab.util.task.Task; - -/** - * Interface defining the contract for communication channels. - * - *

    A channel represents a communication endpoint that can be used to send and receive data. This - * interface extends both {@link Task} and {@link Notifier} to provide task execution capabilities - * and event notification to registered {@link ChannelListener} instances. - * - *

    Channels support different protocols (e.g., serial port, TCP, UDP) and directions - * (receive-only, transmit-only, or bidirectional). Each channel must be configured before use and - * can be monitored for status changes. - * - *

    Implementations of this interface should handle the specific communication protocol details - * while providing a unified interface for data exchange. - * - * @author Enedis Smarties team - * @see Task - * @see Notifier - * @see ChannelListener - * @see ChannelConfiguration - * @see ChannelProtocol - * @see ChannelDirection - * @see ChannelStatus - */ -public interface Channel extends Task, Notifier { - - /** - * Initializes and configures the channel with the provided configuration. - * - *

    This method must be called before any read or write operations can be performed. The - * configuration contains all necessary parameters for the specific channel type, such as port - * settings for serial channels or network settings for TCP channels. - * - *

    After successful setup, the channel should be ready for communication operations. - * - * @param configuration the channel configuration containing all necessary parameters - * @throws ChannelException if the configuration is invalid, the channel cannot be initialized, or - * there are resource allocation issues - */ - public void setup(ChannelConfiguration configuration) throws ChannelException; - - /** - * Reads data from the channel. - * - *

    This method performs a synchronous read operation, blocking until data is available or a - * timeout occurs (depending on the channel implementation). The returned byte array contains the - * raw data received from the channel. - * - *

    The channel must be properly configured and opened before calling this method. - * - * @return a byte array containing the data read from the channel, or an empty array if no data is - * available - * @throws ChannelException if the read operation fails, the channel is not properly configured, - * or a communication error occurs - */ - public byte[] read() throws ChannelException; - - /** - * Writes data to the channel. - * - *

    This method performs a synchronous write operation, sending the provided data through the - * channel. The operation blocks until the data is successfully transmitted or an error occurs. - * - *

    The channel must be properly configured and opened before calling this method. - * - * @param data the byte array containing the data to be written to the channel - * @throws ChannelException if the write operation fails, the channel is not properly configured, - * or a communication error occurs - */ - public void write(byte[] data) throws ChannelException; - - /** - * Retrieves the name of this channel. - * - *

    The channel name is typically used for identification and logging purposes. It is usually - * set during channel creation or configuration. - * - * @return the channel name, or null if not set - */ - public String getName(); - - /** - * Retrieves the communication protocol used by this channel. - * - *

    The protocol determines the underlying communication mechanism, such as serial port, TCP, - * UDP, or other supported protocols. - * - * @return the channel protocol, or null if not set - */ - public ChannelProtocol getProtocol(); - - /** - * Retrieves the communication direction of this channel. - * - *

    The direction indicates whether the channel can receive data (RX), transmit data (TX), or - * both (RXTX). This affects which operations are available. - * - * @return the channel direction, or null if not set - */ - public ChannelDirection getDirection(); - - /** - * Retrieves the current status of this channel. - * - *

    The status indicates the operational state of the channel, such as whether it is open, - * closed, error state, or in the process of opening/closing. - * - * @return the current channel status - */ - public ChannelStatus getStatus(); - - /** - * Retrieves the configuration used by this channel. - * - *

    The configuration contains all the parameters that were used to set up the channel, - * including protocol-specific settings and operational parameters. - * - * @return the channel configuration, or null if the channel has not been configured - */ - public ChannelConfiguration getConfiguration(); -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelBase.java b/src/main/java/enedis/lab/io/channels/ChannelBase.java deleted file mode 100644 index 6244631..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelBase.java +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.task.TaskPeriodic; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Abstract base class providing common functionality for channel implementations. - * - *

    This class serves as a foundation for all channel implementations, providing common setup - * logic, configuration management, and logging capabilities. It extends {@link TaskPeriodic} to - * provide periodic task execution and implements the {@link Channel} interface. - * - *

    The class handles configuration validation, backup and restoration in case of setup failures, - * and provides a template method pattern for channel-specific setup operations through the abstract - * {@link #setup()} method. - * - *

    Subclasses should implement the abstract {@link #setup()} method to provide channel-specific - * initialization logic. - * - * @author Enedis Smarties team - * @see Channel - * @see TaskPeriodic - * @see ChannelConfiguration - */ -public abstract class ChannelBase extends TaskPeriodic implements Channel { - - /** Logger instance for this channel implementation. */ - protected Logger logger; - - /** Configuration used by this channel. */ - protected ChannelConfiguration configuration; - - /** - * Constructs a new channel with the specified configuration. - * - *

    This constructor initializes the logger for the specific channel implementation and - * immediately sets up the channel using the provided configuration. If the setup fails, a {@link - * ChannelException} is thrown. - * - * @param configuration the configuration to use for setting up the channel - * @throws ChannelException if the configuration is invalid or the channel cannot be properly - * initialized - */ - protected ChannelBase(ChannelConfiguration configuration) throws ChannelException { - this.logger = LogManager.getLogger(this.getClass()); - this.setup(configuration); - } - - /** - * Sets up the channel with the provided configuration. - * - *

    This method implements a robust setup process that includes: - * - *

      - *
    • Configuration validation - *
    • Configuration backup (if reconfiguring) - *
    • Channel-specific setup via {@link #setup()} - *
    • Automatic rollback on setup failure - *
    - * - *

    If the setup fails, the previous configuration is automatically restored to maintain the - * channel in a consistent state. - * - * @param configuration the configuration to apply to the channel - * @throws ChannelException if the configuration is null, invalid, or if the channel-specific - * setup fails - */ - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException { - /* Check configuration */ - if (configuration == null) { - ChannelException.raiseInvalidConfiguration("null"); - } - /* Copy configuration */ - ChannelConfiguration configurationBackUp = null; - try { - if (this.configuration == null) { - this.configuration = (ChannelConfiguration) configuration.clone(); - } else { - configurationBackUp = (ChannelConfiguration) this.configuration.clone(); - this.configuration.copy(configuration); - } - } catch (DataDictionaryException exception) { - ChannelException.raiseInvalidConfiguration(exception.getMessage()); - } - /* Set up from internal configuration */ - try { - this.setup(); - } catch (Exception exception) { - /* Restore internal configuration */ - if (configurationBackUp != null) { - try { - this.configuration.copy(configurationBackUp); - } catch (DataDictionaryException dataDictionaryException) { - this.logger.error("", dataDictionaryException); - } - } - } - } - - /** - * Performs channel-specific setup operations. - * - *

    This method is called by the public {@link #setup(ChannelConfiguration)} method after the - * configuration has been validated and applied. Subclasses should override this method to - * implement channel-specific initialization logic, such as opening communication ports, - * establishing network connections, or initializing hardware. - * - *

    If this method throws an exception, the configuration will be automatically rolled back to - * its previous state. - * - * @throws ChannelException if the channel-specific setup fails - */ - protected void setup() throws ChannelException {} -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java b/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java deleted file mode 100644 index 8543fe1..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.configuration.ConfigurationBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorString; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Configuration class for communication channels. - * - *

    This class extends {@link ConfigurationBase} and provides the base configuration parameters - * required for all types of communication channels. It defines the essential properties that every - * channel must have: name, protocol, direction, and alias. - * - *

    The configuration supports validation through key descriptors and provides methods for setting - * and retrieving channel parameters. It can be instantiated from various sources including maps, - * data dictionaries, and direct parameter values. - * - *

    This class serves as the foundation for more specific channel configurations. - * - * @author Enedis Smarties team - * @see ConfigurationBase - * @see ChannelProtocol - * @see ChannelDirection - */ -public class ChannelConfiguration extends ConfigurationBase { - - /** Key for the channel name configuration parameter. */ - protected static final String KEY_NAME = "name"; - - /** Key for the channel protocol configuration parameter. */ - protected static final String KEY_PROTOCOL = "protocol"; - - /** Key for the channel direction configuration parameter. */ - protected static final String KEY_DIRECTION = "direction"; - - /** Key for the channel alias configuration parameter. */ - protected static final String KEY_ALIAS = "alias"; - - /** List of key descriptors for configuration validation. */ - private List> keys = new ArrayList>(); - - /** Key descriptor for channel name validation. */ - protected KeyDescriptorString kName; - - /** Key descriptor for channel protocol validation. */ - protected KeyDescriptorEnum kProtocol; - - /** Key descriptor for channel direction validation. */ - protected KeyDescriptorEnum kDirection; - - /** Key descriptor for channel alias validation. */ - protected KeyDescriptorString kAlias; - - /** - * Default constructor for channel configuration. - * - *

    Initializes the configuration with default values and sets up the key descriptors for - * parameter validation. - */ - protected ChannelConfiguration() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructs a new channel configuration from a map of key-value pairs. - * - *

    This constructor initializes the configuration with values from the provided map, performing - * validation and setting default values for missing parameters. - * - * @param map the map containing configuration parameters - * @throws DataDictionaryException if the map contains invalid values or required parameters are - * missing - */ - public ChannelConfiguration(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a new channel configuration by copying from another data dictionary. - * - *

    This constructor creates a new configuration instance by copying all parameters from the - * provided data dictionary, ensuring consistency between configurations. - * - * @param other the data dictionary to copy configuration from - * @throws DataDictionaryException if the source data dictionary contains invalid values or is - * incompatible with channel configuration - */ - public ChannelConfiguration(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a new channel configuration with a name and file reference, initializing all - * parameters to their default values. - * - *

    This constructor is typically used when creating a new configuration that will be populated - * later with specific channel parameters. - * - * @param name the configuration name - * @param file the configuration file associated with this configuration - */ - public ChannelConfiguration(String name, File file) { - this(); - this.init(name, file); - } - - /** - * Constructs a new channel configuration with all parameters set to specific values. - * - *

    This constructor provides a convenient way to create a fully configured channel with all - * necessary parameters specified at construction time. - * - * @param name the channel name - * @param protocol the communication protocol to use - * @param direction the communication direction (RX, TX, or RXTX) - * @param alias the channel alias (optional) - * @throws DataDictionaryException if any of the provided values are invalid or incompatible with - * channel configuration requirements - */ - public ChannelConfiguration( - String name, ChannelProtocol protocol, ChannelDirection direction, String alias) - throws DataDictionaryException { - this(); - - this.setName(name); - this.setProtocol(protocol); - this.setDirection(direction); - this.setAlias(alias); - - this.checkAndUpdate(); - } - - /** - * Updates optional parameters with default values if not already set. - * - *

    This method is called during configuration validation to ensure that all required parameters - * are properly set. For the base channel configuration, this method delegates to the parent class - * implementation. - * - * @throws DataDictionaryException if there are validation errors with the configuration - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - super.updateOptionalParameters(); - } - - /** - * Retrieves the channel name. - * - *

    The channel name is used for identification and logging purposes. It is typically set during - * channel creation or configuration. - * - * @return the channel name, or null if not set - */ - public String getName() { - return (String) this.data.get(KEY_NAME); - } - - /** - * Retrieves the communication protocol used by this channel. - * - *

    The protocol determines the underlying communication mechanism, such as serial port, TCP, - * UDP, or other supported protocols. - * - * @return the channel protocol, or null if not set - */ - public ChannelProtocol getProtocol() { - return (ChannelProtocol) this.data.get(KEY_PROTOCOL); - } - - /** - * Retrieves the communication direction of this channel. - * - *

    The direction indicates whether the channel can receive data (RX), transmit data (TX), or - * both (RXTX). This affects which operations are available. - * - * @return the channel direction, or null if not set - */ - public ChannelDirection getDirection() { - return (ChannelDirection) this.data.get(KEY_DIRECTION); - } - - /** - * Retrieves the channel alias. - * - *

    The alias provides an alternative identifier for the channel, typically used for - * user-friendly display or alternative referencing. - * - * @return the channel alias, or null if not set - */ - public String getAlias() { - return (String) this.data.get(KEY_ALIAS); - } - - /** - * Sets the channel name. - * - *

    The channel name is used for identification and logging purposes. It must be a non-empty - * string and is required for a valid configuration. - * - * @param name the channel name to set - * @throws DataDictionaryException if the name is invalid, empty, or null - */ - public void setName(String name) throws DataDictionaryException { - this.setName((Object) name); - } - - /** - * Sets the communication protocol for this channel. - * - *

    The protocol determines the underlying communication mechanism. Must be one of the supported - * {@link ChannelProtocol} values. - * - * @param protocol the communication protocol to use - * @throws DataDictionaryException if the protocol is invalid or not supported - */ - public void setProtocol(ChannelProtocol protocol) throws DataDictionaryException { - this.setProtocol((Object) protocol); - } - - /** - * Sets the communication direction for this channel. - * - *

    The direction determines whether the channel can receive data (RX), transmit data (TX), or - * both (RXTX). Must be one of the supported {@link ChannelDirection} values. - * - * @param direction the communication direction to use - * @throws DataDictionaryException if the direction is invalid or not supported - */ - public void setDirection(ChannelDirection direction) throws DataDictionaryException { - this.setDirection((Object) direction); - } - - /** - * Sets the channel alias. - * - *

    The alias provides an alternative identifier for the channel, typically used for - * user-friendly display or alternative referencing. This parameter is optional and can be null. - * - * @param alias the channel alias to set (can be null) - * @throws DataDictionaryException if the alias format is invalid - */ - public void setAlias(String alias) throws DataDictionaryException { - this.setAlias((Object) alias); - } - - /** - * Internal method to set the channel name with object conversion. - * - * @param name the name object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setName(Object name) throws DataDictionaryException { - this.data.put(KEY_NAME, this.kName.convert(name)); - } - - /** - * Internal method to set the channel protocol with object conversion. - * - * @param protocol the protocol object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setProtocol(Object protocol) throws DataDictionaryException { - this.data.put(KEY_PROTOCOL, this.kProtocol.convert(protocol)); - } - - /** - * Internal method to set the channel direction with object conversion. - * - * @param direction the direction object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setDirection(Object direction) throws DataDictionaryException { - this.data.put(KEY_DIRECTION, this.kDirection.convert(direction)); - } - - /** - * Internal method to set the channel alias with object conversion. - * - * @param alias the alias object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setAlias(Object alias) throws DataDictionaryException { - this.data.put(KEY_ALIAS, this.kAlias.convert(alias)); - } - - /** - * Initializes and configures all key descriptors for parameter validation. - * - *

    This method sets up the validation rules for all channel configuration parameters, including - * required/optional status and type validation. - * - * @throws RuntimeException if there is an error during key descriptor initialization - */ - private void loadKeyDescriptors() { - try { - this.kName = new KeyDescriptorString(KEY_NAME, true, false); - this.keys.add(this.kName); - - this.kProtocol = - new KeyDescriptorEnum(KEY_PROTOCOL, true, ChannelProtocol.class); - this.keys.add(this.kProtocol); - - this.kDirection = - new KeyDescriptorEnum(KEY_DIRECTION, true, ChannelDirection.class); - this.keys.add(this.kDirection); - - this.kAlias = new KeyDescriptorString(KEY_ALIAS, false, false); - this.keys.add(this.kAlias); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelDirection.java b/src/main/java/enedis/lab/io/channels/ChannelDirection.java deleted file mode 100644 index ed4c3f1..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelDirection.java +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -/** - * Enumeration representing the communication direction for channels. - * - *

    This enum defines the possible communication directions that a channel can support. The - * direction determines which operations are available on the channel (read, write, or both). - * - *

    This enum is used in {@link ChannelConfiguration} to specify the communication direction for - * channel instances. - * - * @author Enedis Smarties team - * @see ChannelConfiguration - * @see Channel - */ -public enum ChannelDirection { - - /** - * Receive-only direction. - * - *

    Channels with this direction can only receive data from the communication endpoint. Read - * operations are supported, but write operations will fail. - */ - RX, - - /** - * Transmit-only direction. - * - *

    Channels with this direction can only send data to the communication endpoint. Write - * operations are supported, but read operations will fail. - */ - TX, - - /** - * Bidirectional communication. - * - *

    Channels with this direction can both receive and send data. Both read and write operations - * are supported, enabling full bidirectional communication. This is the most common direction for - * interactive communication channels. - */ - RXTX -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelException.java b/src/main/java/enedis/lab/io/channels/ChannelException.java deleted file mode 100644 index bb2d100..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelException.java +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.types.ExceptionBase; - -/** - * Exception class for channel-related errors. - * - *

    This exception is thrown when errors occur during channel operations such as setup, - * configuration, read/write operations, or channel management. It extends {@link ExceptionBase} to - * provide structured error handling with error codes and detailed error messages. - * - *

    The class provides static factory methods for creating specific types of channel exceptions, - * each with appropriate error codes and descriptive messages. - * - *

    Common scenarios that trigger this exception include: - * - *

      - *
    • Invalid channel configuration - *
    • Channel not ready for operations - *
    • Conflicting channel types - *
    • Operation denied due to channel state - *
    • Internal channel errors - *
    - * - * @author Enedis Smarties team - * @see ExceptionBase - * @see Channel - * @see ChannelConfiguration - */ -public class ChannelException extends ExceptionBase { - - private static final long serialVersionUID = -3507876436535833098L; - - /** Error code for invalid or unsupported channel types. */ - public static final int ERRCODE_INVALID_CHANNEL = -5; - - /** Error code for attempting to create a channel that already exists. */ - public static final int ERRCODE_ALREADY_EXISTS = -6; - - /** Error code for internal channel processing errors. */ - public static final int ERRCODE_INTERNAL_ERROR = -7; - - /** Error code for operations attempted on channels that are not ready. */ - public static final int ERRCODE_CHANNEL_NOT_READY = -8; - - /** Error code for invalid or incompatible configuration types. */ - public static final int ERRCODE_INVALID_CONFIGURATION_TYPE = -9; - - /** Error code for invalid configuration parameters or values. */ - public static final int ERRCODE_INVALID_CONFIGURATION = -10; - - /** Error code for operations that are not allowed in the current channel state. */ - public static final int ERRCODE_OPERATION_DENIED = -11; - - /** Error code for operations attempted on non-existent channels. */ - public static final int ERRCODE_CHANNEL_DOESNT_EXIST = -12; - - /** Error code for unexpected or unhandled errors. */ - public static final int ERRCODE_UNEXPECTED = -99; - - /** - * Creates and throws a ChannelException for unhandled channel types. - * - *

    This method is used when an attempt is made to create or use a channel type that is not - * supported by the system. - * - * @param channelType the unsupported channel type that was requested - * @throws ChannelException always thrown with error code {@link #ERRCODE_INVALID_CHANNEL} - */ - public static void raiseUnhandledChannelType(String channelType) throws ChannelException { - throw new ChannelException(ERRCODE_INVALID_CHANNEL, channelType + " channel is not handled"); - } - - /** - * Creates and throws a ChannelException for conflicting channel types. - * - *

    This method is used when a channel type conflicts with an expected type, typically during - * channel configuration or type validation. - * - * @param channelType the actual channel type that was provided - * @param expectedChannelType the expected channel type - * @throws ChannelException always thrown with error code {@link #ERRCODE_INVALID_CHANNEL} - */ - public static void raiseConflictingChannelType(String channelType, String expectedChannelType) - throws ChannelException { - throw new ChannelException( - ERRCODE_INVALID_CHANNEL, - channelType + " channel is conflicting with " + expectedChannelType); - } - - /** - * Creates and throws a ChannelException for internal errors. - * - *

    This method is used when an internal error occurs during channel processing that cannot be - * attributed to user input or configuration. - * - * @param info additional information about the internal error - * @throws ChannelException always thrown with error code {@link #ERRCODE_INTERNAL_ERROR} - */ - public static void raiseInternalError(String info) throws ChannelException { - throw new ChannelException(ERRCODE_INTERNAL_ERROR, info); - } - - /** - * Creates and throws a ChannelException for channels that are not ready. - * - *

    This method is used when an operation is attempted on a channel that is not in a ready state - * (e.g., not properly configured or initialized). - * - * @param info additional information about why the channel is not ready - * @throws ChannelException always thrown with error code {@link #ERRCODE_CHANNEL_NOT_READY} - */ - public static void raiseChannelNotReady(String info) throws ChannelException { - throw new ChannelException(ERRCODE_CHANNEL_NOT_READY, info); - } - - /** - * Creates and throws a ChannelException for invalid configuration types. - * - *

    This method is used when a channel configuration is of the wrong type for the expected - * channel implementation. - * - * @param configuration the invalid configuration that was provided - * @param expected_configuration_name the name of the expected configuration type - * @throws ChannelException always thrown with error code {@link - * #ERRCODE_INVALID_CONFIGURATION_TYPE} - */ - public static void raiseInvalidConfigurationType( - ChannelConfiguration configuration, String expected_configuration_name) - throws ChannelException { - throw new ChannelException( - ERRCODE_INVALID_CONFIGURATION_TYPE, - "Configuration " - + configuration.getClass().getSimpleName() - + " is not a valid type (" - + expected_configuration_name - + " expected)"); - } - - /** - * Creates and throws a ChannelException for invalid configuration parameters. - * - *

    This method is used when channel configuration parameters are invalid, missing, or - * incompatible. - * - * @param info additional information about the configuration error - * @throws ChannelException always thrown with error code {@link #ERRCODE_INVALID_CONFIGURATION} - */ - public static void raiseInvalidConfiguration(String info) throws ChannelException { - throw new ChannelException( - ERRCODE_INVALID_CONFIGURATION, "Configuration is invalid (" + info + ")"); - } - - /** - * Creates and throws a ChannelException for denied operations. - * - *

    This method is used when an operation is attempted that is not allowed in the current - * channel state or configuration. - * - * @param operation the operation that was denied - * @throws ChannelException always thrown with error code {@link #ERRCODE_OPERATION_DENIED} - */ - public static void raiseOperationDenied(String operation) throws ChannelException { - throw new ChannelException( - ERRCODE_OPERATION_DENIED, "Operation \'" + operation + "\' is not allowed"); - } - - /** - * Creates and throws a ChannelException for unexpected errors. - * - *

    This method is used when an unexpected or unhandled error occurs during channel operations. - * - * @param info additional information about the unexpected error - * @throws ChannelException always thrown with error code {@link #ERRCODE_UNEXPECTED} - */ - public static void raiseUnexpectedError(String info) throws ChannelException { - throw new ChannelException(ERRCODE_UNEXPECTED, "Unexpected error occurs : " + info); - } - - /** - * Creates and throws a ChannelException for duplicate channel names. - * - *

    This method is used when attempting to create a channel with a name that already exists in - * the system. - * - * @param channelName the name of the channel that already exists - * @throws ChannelException always thrown with error code {@link #ERRCODE_ALREADY_EXISTS} - */ - public static void raiseAlreadyExists(String channelName) throws ChannelException { - throw new ChannelException( - ERRCODE_ALREADY_EXISTS, "Channel " + channelName + " already exists"); - } - - /** - * Creates and throws a ChannelException for non-existent channels. - * - *

    This method is used when attempting to perform operations on a channel that does not exist - * in the system. - * - * @param channelName the name of the channel that does not exist - * @throws ChannelException always thrown with error code {@link #ERRCODE_CHANNEL_DOESNT_EXIST} - */ - public static void raiseDoesntExist(String channelName) throws ChannelException { - throw new ChannelException( - ERRCODE_CHANNEL_DOESNT_EXIST, "Channel " + channelName + " doesn't exist"); - } - - /** - * Constructs a new ChannelException with the specified error code and message. - * - *

    This constructor creates a channel exception with a specific error code and descriptive - * message. The error code should be one of the predefined constants in this class. - * - * @param code the error code for this exception - * @param info the detailed error message - */ - public ChannelException(int code, String info) { - super(code, info); - } -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelListener.java b/src/main/java/enedis/lab/io/channels/ChannelListener.java deleted file mode 100644 index d4c45b1..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelListener.java +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.Subscriber; - -/** - * Interface for receiving channel events and notifications. - * - *

    This interface extends {@link Subscriber} and provides methods for handling various channel - * events such as data read/write operations, status changes, and error conditions. Implementations - * of this interface can be registered with channels to receive real-time notifications about - * channel activities. - * - *

    The listener pattern allows for decoupled event handling, enabling multiple listeners to be - * registered with a single channel and receive notifications about all channel activities. - * - *

    Common use cases include: - * - *

      - *
    • Logging channel activities - *
    • Monitoring channel status - *
    • Processing received data - *
    • Error handling and reporting - *
    - * - * @author Enedis Smarties team - * @see Subscriber - * @see Channel - * @see ChannelStatus - */ -public interface ChannelListener extends Subscriber { - - /** - * Called when data is successfully read from a channel. - * - *

    This method is invoked whenever a channel performs a successful read operation. The received - * data is provided as a byte array containing the raw data that was read from the channel. - * - * @param channelName the name of the channel that read the data - * @param data the raw bytes that were read from the channel - */ - public void onDataRead(String channelName, byte[] data); - - /** - * Called when data is successfully written to a channel. - * - *

    This method is invoked whenever a channel performs a successful write operation. The - * transmitted data is provided as a byte array containing the raw data that was written to the - * channel. - * - * @param channelName the name of the channel that wrote the data - * @param data the raw bytes that were written to the channel - */ - public void onDataWritten(String channelName, byte[] data); - - /** - * Called when the status of a channel changes. - * - *

    This method is invoked whenever a channel's operational status changes, such as when it goes - * from closed to open, or from ready to error state. This allows listeners to monitor channel - * state transitions. - * - * @param channelName the name of the channel whose status changed - * @param status the new status of the channel - */ - public void onStatusChanged(String channelName, ChannelStatus status); - - /** - * Called when an error is detected on a channel. - * - *

    This method is invoked whenever an error occurs during channel operations. It provides basic - * error information including the error code and message. - * - * @param channelName the name of the channel where the error occurred - * @param errorCode the numeric error code identifying the type of error - * @param errorMessage a descriptive message explaining the error - */ - public void onErrorDetected(String channelName, int errorCode, String errorMessage); - - /** - * Called when an error is detected on a channel with additional context data. - * - *

    This method is invoked whenever an error occurs during channel operations and additional - * context data is available. It provides comprehensive error information including the error - * code, message, and associated data dictionary. - * - * @param channelName the name of the channel where the error occurred - * @param errorCode the numeric error code identifying the type of error - * @param errorMessage a descriptive message explaining the error - * @param data additional context data associated with the error - */ - public void onErrorDetected( - String channelName, int errorCode, String errorMessage, DataDictionary data); -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelPhysical.java b/src/main/java/enedis/lab/io/channels/ChannelPhysical.java deleted file mode 100644 index 136a01e..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelPhysical.java +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.util.task.NotifierBase; -import java.util.Collection; - -/** - * Abstract base class for physical communication channels. - * - *

    This class extends {@link ChannelBase} and provides the foundation for all physical - * communication channel implementations. It handles the common functionality for physical channels - * including status management, listener notification, and event propagation. - * - *

    Physical channels represent actual communication endpoints such as serial ports, network - * sockets, or other hardware interfaces. This class provides the infrastructure for managing - * channel state and notifying registered listeners about channel events. - * - *

    Key features provided by this class: - * - *

      - *
    • Status management with automatic listener notification - *
    • Event notification system for data read/write operations - *
    • Error detection and reporting - *
    • Listener subscription management - *
    - * - * @author Enedis Smarties team - * @see ChannelBase - * @see ChannelListener - * @see ChannelStatus - * @see NotifierBase - */ -public abstract class ChannelPhysical extends ChannelBase { - - /** Notifier for managing channel event listeners. */ - private NotifierBase notifier; - - /** Current status of the physical channel. */ - protected ChannelStatus status; - - /** - * Constructs a new physical channel with the specified configuration. - * - *

    This constructor initializes the physical channel with the provided configuration and sets - * up the internal notification system. The channel starts in a STOPPED status and is ready for - * setup operations. - * - * @param configuration the configuration to use for setting up the channel - * @throws ChannelException if the configuration is invalid or the channel cannot be properly - * initialized - */ - protected ChannelPhysical(ChannelConfiguration configuration) throws ChannelException { - super(configuration); - this.init(); - } - - @Override - public void subscribe(ChannelListener listener) { - this.notifier.subscribe(listener); - } - - @Override - public void unsubscribe(ChannelListener listener) { - this.notifier.unsubscribe(listener); - } - - @Override - public boolean hasSubscriber(ChannelListener subscriber) { - return this.notifier.hasSubscriber(subscriber); - } - - @Override - public Collection getSubscribers() { - return this.notifier.getSubscribers(); - } - - @Override - public String getName() { - return this.configuration.getName(); - } - - @Override - public ChannelProtocol getProtocol() { - return this.configuration.getProtocol(); - } - - @Override - public ChannelDirection getDirection() { - return this.configuration.getDirection(); - } - - @Override - public ChannelStatus getStatus() { - return this.status; - } - - @Override - public ChannelConfiguration getConfiguration() { - return this.configuration; - } - - /** - * Sets the channel status and notifies all registered listeners. - * - *

    This method updates the channel status and automatically notifies all registered listeners - * about the status change. - * - * @param status the new status for the channel - */ - protected void setStatus(ChannelStatus status) { - this.setStatus(status, true); - } - - /** - * Sets the channel status with optional listener notification. - * - *

    This method updates the channel status and optionally notifies registered listeners about - * the status change. This allows for controlled status updates without triggering notifications. - * - * @param status the new status for the channel - * @param notify if true, listeners will be notified of the status change; if false, no - * notifications will be sent - */ - protected void setStatus(ChannelStatus status, boolean notify) { - if (status != this.status) { - this.status = status; - if (status == ChannelStatus.ERROR) { - this.logger.error("Channel " + this.getName() + " new status : " + status.name()); - } else { - this.logger.info("Channel " + this.getName() + " new status : " + status.name()); - } - if (true == notify) { - this.notifyOnStatusChanged(); - } - } - } - - /** - * Notifies all registered listeners about data that was read from the channel. - * - *

    This method is called internally when data is successfully read from the physical channel. - * It notifies all registered listeners with the channel name and the raw data that was read. - * - * @param data the raw bytes that were read from the channel - */ - protected void notifyOnDataRead(byte[] data) { - if (data != null) { - for (ChannelListener subscriber : this.notifier.getSubscribers()) { - subscriber.onDataRead(this.getName(), data); - } - } - } - - /** - * Notifies all registered listeners about data that was written to the channel. - * - *

    This method is called internally when data is successfully written to the physical channel. - * It notifies all registered listeners with the channel name and the raw data that was written. - * - * @param data the raw bytes that were written to the channel - */ - protected void notifyOnDataWritten(byte[] data) { - if (data != null) { - for (ChannelListener subscriber : this.notifier.getSubscribers()) { - subscriber.onDataWritten(this.getName(), data); - } - } - } - - /** - * Notifies all registered listeners about a status change. - * - *

    This method is called internally when the channel status changes. It notifies all registered - * listeners with the channel name and the new status. - */ - protected void notifyOnStatusChanged() { - for (ChannelListener subscriber : this.notifier.getSubscribers()) { - subscriber.onStatusChanged(this.getName(), this.status); - } - } - - /** - * Notifies all registered listeners about an error that was detected. - * - *

    This method is called internally when an error occurs during channel operations. It notifies - * all registered listeners with the channel name, error code, and error message. - * - * @param errorCode the numeric error code identifying the type of error - * @param errorMessage a descriptive message explaining the error - */ - protected void notifyOnErrorDetected(int errorCode, String errorMessage) { - for (ChannelListener subscriber : this.notifier.getSubscribers()) { - subscriber.onErrorDetected(this.getName(), errorCode, errorMessage); - } - } - - /** - * Initializes the physical channel with default values. - * - *

    This method sets up the initial state of the physical channel, including setting the status - * to STOPPED and initializing the notification system for managing listeners. - */ - private void init() { - this.status = ChannelStatus.STOPPED; - this.notifier = new NotifierBase(); - } -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java b/src/main/java/enedis/lab/io/channels/ChannelProtocol.java deleted file mode 100644 index b1f2c4e..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -/** - * Enumeration representing different types of communication channel protocols that can be used for - * data transmission. - * - *

    This enum defines the various communication protocols supported by the system, ranging from - * file-based operations to network protocols and industrial communication standards like Modbus. - * - * @author Enedis Smarties team - */ -public enum ChannelProtocol { - - /** - * File-based communication protocol. Used for reading from or writing to local files on the - * filesystem. - */ - FILE, - - /** - * Serial port communication protocol. Used for direct communication through serial interfaces - * (RS-232, RS-485, etc.). - */ - SERIAL_PORT, - - /** - * Modbus RTU (Remote Terminal Unit) protocol. A serial communication protocol commonly used in - * industrial automation systems. - */ - MODBUS_RTU, - - /** Modbus TCP protocol. A network-based implementation of the Modbus protocol over TCP/IP. */ - MODBUS_TCP, - - /** - * Modbus stub protocol. A testing/mock implementation of Modbus for development and testing - * purposes. - */ - MODBUS_STUB, - - /** - * Transmission Control Protocol (TCP). A reliable, connection-oriented network protocol for data - * transmission. - */ - TCP, - - /** - * User Datagram Protocol (UDP). A connectionless network protocol for fast, lightweight data - * transmission. - */ - UDP, -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelStatus.java b/src/main/java/enedis/lab/io/channels/ChannelStatus.java deleted file mode 100644 index 7204170..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelStatus.java +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -/** - * Enumeration representing the operational status of a communication channel. - * - *

    This enum defines the possible states that a channel can be in during its lifecycle, from - * initialization through operation to error conditions. The status is used to monitor and manage - * the health and availability of communication channels. - * - * @author Enedis Smarties team - */ -public enum ChannelStatus { - /** - * Channel is stopped and not actively processing data. This is the initial state when a channel - * is created but not yet started, or when it has been explicitly stopped. - */ - STOPPED, - - /** - * Channel is started and actively processing data. The channel is operational and ready to handle - * communication requests. - */ - STARTED, - - /** - * Channel has encountered an error and is not functioning properly. This state indicates that the - * channel needs attention or recovery actions. - */ - ERROR -} diff --git a/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java b/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java deleted file mode 100644 index 451f869..0000000 --- a/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java +++ /dev/null @@ -1,532 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -import enedis.lab.io.channels.ChannelConfiguration; -import enedis.lab.io.channels.ChannelDirection; -import enedis.lab.io.channels.ChannelProtocol; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Configuration class for serial port communication channels. - * - *

    This class extends {@link ChannelConfiguration} and provides specific configuration parameters - * for serial port communication, including port identification, baud rate, parity settings, data - * bits, stop bits, and timeout configurations. - * - *

    The configuration supports both port ID and port name identification methods, with validation - * for standard serial communication parameters. - * - *

    Supported baud rates: 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, - * 460800, 921600, 1843200, 3686400 - * - *

    Supported data bits: 5, 6, 7, 8 - * - *

    Supported stop bits: 1.0, 1.5, 2.0 - * - *

    Default sync read timeout: 10000 milliseconds - * - * @author Enedis Smarties team - * @see ChannelConfiguration - * @see ChannelProtocol#SERIAL_PORT - * @see ChannelDirection#RXTX - */ -public class ChannelSerialPortConfiguration extends ChannelConfiguration { - - /** Key for the serial port identifier configuration parameter. */ - protected static final String KEY_PORT_ID = "portId"; - - /** Key for the serial port name configuration parameter. */ - protected static final String KEY_PORT_NAME = "portName"; - - /** Key for the baud rate configuration parameter. */ - protected static final String KEY_BAUDRATE = "baudrate"; - - /** Key for the parity configuration parameter. */ - protected static final String KEY_PARITY = "parity"; - - /** Key for the data bits configuration parameter. */ - protected static final String KEY_DATA_BITS = "dataBits"; - - /** Key for the stop bits configuration parameter. */ - protected static final String KEY_STOP_BITS = "stopBits"; - - /** Key for the synchronous read timeout configuration parameter. */ - protected static final String KEY_SYNC_READ_TIMEOUT = "syncReadTimeout"; - - /** Accepted protocol value for serial port channels. */ - private static final ChannelProtocol PROTOCOL_ACCEPTED_VALUE = ChannelProtocol.SERIAL_PORT; - - /** Accepted direction value for serial port channels (bidirectional). */ - private static final ChannelDirection DIRECTION_ACCEPTED_VALUE = ChannelDirection.RXTX; - - /** Array of supported baud rate values for serial communication. */ - private static final Number[] BAUDRATE_ACCEPTED_VALUES = { - 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1843200, - 3686400 - }; - - /** Array of supported data bits values (5, 6, 7, or 8 bits). */ - private static final Number[] DATA_BITS_ACCEPTED_VALUES = {5, 6, 7, 8}; - - /** Array of supported stop bits values (1.0, 1.5, or 2.0). */ - private static final Number[] STOP_BITS_ACCEPTED_VALUES = {1.0d, 1.5d, 2.0d}; - - /** Default timeout value for synchronous read operations (10000 milliseconds). */ - private static final Number SYNC_READ_TIMEOUT_DEFAULT_VALUE = 10000; - - /** List of key descriptors for configuration validation. */ - private List> keys = new ArrayList>(); - - /** Key descriptor for port ID validation. */ - protected KeyDescriptorString kPortId; - - /** Key descriptor for port name validation. */ - protected KeyDescriptorString kPortName; - - /** Key descriptor for baud rate validation. */ - protected KeyDescriptorNumber kBaudrate; - - /** Key descriptor for parity validation. */ - protected KeyDescriptorEnum kParity; - - /** Key descriptor for data bits validation. */ - protected KeyDescriptorNumber kDataBits; - - /** Key descriptor for stop bits validation. */ - protected KeyDescriptorNumber kStopBits; - - /** Key descriptor for sync read timeout validation. */ - protected KeyDescriptorNumber kSyncReadTimeout; - - /** - * Default constructor for serial port configuration. - * - *

    Initializes the configuration with default values and sets up the key descriptors for - * parameter validation. - */ - protected ChannelSerialPortConfiguration() { - super(); - this.loadKeyDescriptors(); - - this.kProtocol.setAcceptedValues(PROTOCOL_ACCEPTED_VALUE); - this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUE); - } - - /** - * Constructs a new serial port configuration from a map of key-value pairs. - * - *

    This constructor initializes the configuration with values from the provided map, performing - * validation and setting default values for missing parameters. - * - * @param map the map containing configuration parameters - * @throws DataDictionaryException if the map contains invalid values or required parameters are - * missing - */ - public ChannelSerialPortConfiguration(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a new serial port configuration by copying from another data dictionary. - * - *

    This constructor creates a new configuration instance by copying all parameters from the - * provided data dictionary, ensuring consistency between configurations. - * - * @param other the data dictionary to copy configuration from - * @throws DataDictionaryException if the source data dictionary contains invalid values or is - * incompatible with serial port configuration - */ - public ChannelSerialPortConfiguration(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a new serial port configuration with a name and file reference, initializing all - * parameters to their default values. - * - *

    - * - * @param name the configuration name - * @param file the configuration file associated with this configuration - */ - public ChannelSerialPortConfiguration(String name, File file) { - this(); - this.init(name, file); - } - - /** - * Constructs a new serial port configuration with all parameters set to specific values. - * - *

    This constructor provides a convenient way to create a fully configured serial port channel - * with all necessary parameters specified at construction time. - * - * @param name the configuration name - * @param alias the configuration alias - * @param portId the serial port identifier (alternative to portName) - * @param portName the serial port name (alternative to portId) - * @param baudrate the communication baud rate - * @param parity the parity setting for data validation - * @param dataBits the number of data bits per frame - * @param stopBits the number of stop bits - * @param syncReadTimeout the timeout for synchronous read operations in milliseconds - * @throws DataDictionaryException if any of the provided values are invalid or incompatible with - * serial port communication requirements - */ - public ChannelSerialPortConfiguration( - String name, - String alias, - String portId, - String portName, - Number baudrate, - Parity parity, - Number dataBits, - Number stopBits, - Number syncReadTimeout) - throws DataDictionaryException { - this(); - - this.setName(name); - this.setAlias(alias); - this.setPortId(portId); - this.setPortName(portName); - this.setBaudrate(baudrate); - this.setParity(parity); - this.setDataBits(dataBits); - this.setStopBits(stopBits); - this.setSyncReadTimeout(syncReadTimeout); - - this.checkAndUpdate(); - } - - /** - * Updates optional parameters with default values if not already set. - * - *

    This method ensures that either port ID or port name is specified, and sets default values - * for protocol, direction, and sync read timeout if they are not already configured. - * - * @throws DataDictionaryException if neither port ID nor port name is defined, or if there are - * validation errors with the configuration - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_PORT_ID) && !this.exists(KEY_PORT_NAME)) { - throw new DataDictionaryException( - "Neither " + KEY_PORT_ID + " nor " + KEY_PORT_NAME + " is defined"); - } - if (!this.exists(KEY_PROTOCOL)) { - this.setProtocol(PROTOCOL_ACCEPTED_VALUE); - } - if (!this.exists(KEY_DIRECTION)) { - this.setDirection(DIRECTION_ACCEPTED_VALUE); - } - if (!this.exists(KEY_SYNC_READ_TIMEOUT)) { - this.setSyncReadTimeout(SYNC_READ_TIMEOUT_DEFAULT_VALUE); - } - super.updateOptionalParameters(); - } - - /** - * Retrieves the serial port identifier. - * - *

    The port ID is an alternative way to identify the serial port, typically used when the - * system provides a unique identifier for the port. - * - * @return the port identifier, or null if not set - */ - public String getPortId() { - return (String) this.data.get(KEY_PORT_ID); - } - - /** - * Retrieves the serial port name. - * - *

    The port name is typically a system-specific identifier such as "/dev/ttyUSB0" on Unix - * systems or "COM1" on Windows systems. - * - * @return the port name, or null if not set - */ - public String getPortName() { - return (String) this.data.get(KEY_PORT_NAME); - } - - /** - * Retrieves the communication baud rate. - * - *

    The baud rate determines the speed of data transmission over the serial port. - * - * @return the baud rate in bits per second, or null if not set - */ - public Number getBaudrate() { - return (Number) this.data.get(KEY_BAUDRATE); - } - - /** - * Retrieves the parity setting for data validation. - * - *

    Parity is used for error detection in serial communication. It can be NONE, ODD, or EVEN - * depending on the communication protocol. - * - * @return the parity setting, or null if not set - */ - public Parity getParity() { - return (Parity) this.data.get(KEY_PARITY); - } - - /** - * Retrieves the number of data bits per frame. - * - *

    Data bits determine how many bits are used to represent each character in the serial - * communication. Common values are 7 or 8 bits. - * - * @return the number of data bits, or null if not set - */ - public Number getDataBits() { - return (Number) this.data.get(KEY_DATA_BITS); - } - - /** - * Retrieves the number of stop bits. - * - *

    Stop bits are used to signal the end of a data frame in serial communication. - * - * @return the number of stop bits, or null if not set - */ - public Number getStopBits() { - return (Number) this.data.get(KEY_STOP_BITS); - } - - /** - * Retrieves the timeout for synchronous read operations. - * - *

    This timeout determines how long the system will wait for data to be available on the serial - * port before timing out the read operation. - * - * @return the timeout value in milliseconds, or null if not set - */ - public Number getSyncReadTimeout() { - return (Number) this.data.get(KEY_SYNC_READ_TIMEOUT); - } - - /** - * Sets the serial port identifier. - * - *

    The port ID provides an alternative way to identify the serial port, typically used when the - * system provides a unique identifier for the port. Either port ID or port name must be specified - * for a valid configuration. - * - * @param portId the port identifier to set - * @throws DataDictionaryException if the port ID is invalid or conflicts with other configuration - * parameters - */ - public void setPortId(String portId) throws DataDictionaryException { - this.setPortId((Object) portId); - } - - /** - * Sets the serial port name. - * - *

    The port name is typically a system-specific identifier such as "/dev/ttyUSB0" on Unix - * systems or "COM1" on Windows systems. Either port ID or port name must be specified for a valid - * configuration. - * - * @param portName the port name to set - * @throws DataDictionaryException if the port name is invalid or conflicts with other - * configuration parameters - */ - public void setPortName(String portName) throws DataDictionaryException { - this.setPortName((Object) portName); - } - - /** - * Sets the communication baud rate. - * - *

    The baud rate determines the speed of data transmission over the serial port. Must be one of - * the supported values: {@link #BAUDRATE_ACCEPTED_VALUES}. - * - * @param baudrate the baud rate in bits per second - * @throws DataDictionaryException if the baud rate is not supported or invalid - */ - public void setBaudrate(Number baudrate) throws DataDictionaryException { - this.setBaudrate((Object) baudrate); - } - - /** - * Sets the parity setting for data validation. - * - *

    Parity is used for error detection in serial communication. The parity setting must be one - * of the supported {@link Parity} enum values. - * - * @param parity the parity setting to use - * @throws DataDictionaryException if the parity value is invalid or not supported - */ - public void setParity(Parity parity) throws DataDictionaryException { - this.setParity((Object) parity); - } - - /** - * Sets the number of data bits per frame. - * - *

    Data bits determine how many bits are used to represent each character in the serial - * communication. Must be one of: {@link #DATA_BITS_ACCEPTED_VALUES}. - * - * @param dataBits the number of data bits to use - * @throws DataDictionaryException if the data bits value is not supported or invalid - */ - public void setDataBits(Number dataBits) throws DataDictionaryException { - this.setDataBits((Object) dataBits); - } - - /** - * Sets the number of stop bits. - * - *

    Stop bits are used to signal the end of a data frame in serial communication. Must be one - * of: {@link #STOP_BITS_ACCEPTED_VALUES}. - * - * @param stopBits the number of stop bits to use - * @throws DataDictionaryException if the stop bits value is not supported or invalid - */ - public void setStopBits(Number stopBits) throws DataDictionaryException { - this.setStopBits((Object) stopBits); - } - - /** - * Sets the timeout for synchronous read operations. - * - *

    This timeout determines how long the system will wait for data to be available on the serial - * port before timing out the read operation. If not set, defaults to {@link - * #SYNC_READ_TIMEOUT_DEFAULT_VALUE}. - * - * @param syncReadTimeout the timeout value in milliseconds - * @throws DataDictionaryException if the timeout value is invalid or negative - */ - public void setSyncReadTimeout(Number syncReadTimeout) throws DataDictionaryException { - this.setSyncReadTimeout((Object) syncReadTimeout); - } - - /** - * Internal method to set the port ID with object conversion. - * - * @param portId the port ID object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setPortId(Object portId) throws DataDictionaryException { - this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); - } - - /** - * Internal method to set the port name with object conversion. - * - * @param portName the port name object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setPortName(Object portName) throws DataDictionaryException { - this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); - } - - /** - * Internal method to set the baud rate with object conversion. - * - * @param baudrate the baud rate object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setBaudrate(Object baudrate) throws DataDictionaryException { - this.data.put(KEY_BAUDRATE, this.kBaudrate.convert(baudrate)); - } - - /** - * Internal method to set the parity with object conversion. - * - * @param parity the parity object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setParity(Object parity) throws DataDictionaryException { - this.data.put(KEY_PARITY, this.kParity.convert(parity)); - } - - /** - * Internal method to set the data bits with object conversion. - * - * @param dataBits the data bits object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setDataBits(Object dataBits) throws DataDictionaryException { - this.data.put(KEY_DATA_BITS, this.kDataBits.convert(dataBits)); - } - - /** - * Internal method to set the stop bits with object conversion. - * - * @param stopBits the stop bits object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setStopBits(Object stopBits) throws DataDictionaryException { - this.data.put(KEY_STOP_BITS, this.kStopBits.convert(stopBits)); - } - - /** - * Internal method to set the sync read timeout with object conversion. - * - * @param syncReadTimeout the sync read timeout object to convert and set - * @throws DataDictionaryException if the conversion fails - */ - protected void setSyncReadTimeout(Object syncReadTimeout) throws DataDictionaryException { - this.data.put(KEY_SYNC_READ_TIMEOUT, this.kSyncReadTimeout.convert(syncReadTimeout)); - } - - /** - * Initializes and configures all key descriptors for parameter validation. - * - *

    This method sets up the validation rules for all serial port configuration parameters, - * including accepted values and required/optional status. - * - * @throws RuntimeException if there is an error during key descriptor initialization - */ - private void loadKeyDescriptors() { - try { - this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); - this.keys.add(this.kPortId); - - this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); - this.keys.add(this.kPortName); - - this.kBaudrate = new KeyDescriptorNumber(KEY_BAUDRATE, true); - this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); - this.keys.add(this.kBaudrate); - - this.kParity = new KeyDescriptorEnum(KEY_PARITY, true, Parity.class); - this.keys.add(this.kParity); - - this.kDataBits = new KeyDescriptorNumber(KEY_DATA_BITS, true); - this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUES); - this.keys.add(this.kDataBits); - - this.kStopBits = new KeyDescriptorNumber(KEY_STOP_BITS, true); - this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUES); - this.keys.add(this.kStopBits); - - this.kSyncReadTimeout = new KeyDescriptorNumber(KEY_SYNC_READ_TIMEOUT, false); - this.keys.add(this.kSyncReadTimeout); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/channels/serialport/Parity.java b/src/main/java/enedis/lab/io/channels/serialport/Parity.java deleted file mode 100644 index 7b01874..0000000 --- a/src/main/java/enedis/lab/io/channels/serialport/Parity.java +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -/** - * Enumeration representing parity settings for serial communication. - * - *

    Parity is used for error detection in serial communication by adding an extra bit to each data - * frame. The parity bit is calculated based on the number of 1-bits in the data and the selected - * parity type. - * - *

    This enum is used in {@link ChannelSerialPortConfiguration} to specify the parity setting for - * serial port communication channels. - * - * @author Enedis Smarties team - * @see ChannelSerialPortConfiguration - */ -public enum Parity { - - /** - * No parity bit is added to the data frame. - * - *

    This setting disables parity checking, which means no error detection is performed on the - * data bits. - */ - NONE, - - /** - * Even parity is used for error detection. - * - *

    The parity bit is set to 1 if the number of 1-bits in the data is odd, ensuring the total - * number of 1-bits (including parity) is even. - */ - EVEN, - - /** - * Odd parity is used for error detection. - * - *

    The parity bit is set to 1 if the number of 1-bits in the data is even, ensuring the total - * number of 1-bits (including parity) is odd. - */ - ODD, - - /** - * Mark parity is used for error detection. - * - *

    The parity bit is always set to 1, regardless of the data content. This is typically used - * for special communication protocols. - */ - MARK, - - /** - * Space parity is used for error detection. - * - *

    The parity bit is always set to 0, regardless of the data content. This is typically used - * for special communication protocols. - */ - SPACE -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataInputStream.java b/src/main/java/enedis/lab/io/datastreams/DataInputStream.java deleted file mode 100644 index 7fb9e44..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataInputStream.java +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.types.DataDictionary; - -/** - * Abstract base class for data input streams. - * - *

    This class provides the foundation for implementing input-only data streams that can read data - * from various sources. It extends {@link DataStreamBase} and enforces input-only operations by - * preventing write operations and validating that the stream direction is set to INPUT. - * - *

    Concrete implementations should override the read methods to provide specific input - * functionality for different data sources (files, network connections, serial ports, etc.). - * - * @author Enedis Smarties team - * @see DataStreamBase - * @see DataStreamConfiguration - * @see DataStreamDirection - */ -public abstract class DataInputStream extends DataStreamBase { - - /** - * Constructs a new DataInputStream with the specified configuration. - * - *

    The configuration must specify INPUT as the direction for this stream to be valid. - * - * @param configuration the configuration for this data input stream - * @throws DataStreamException if the configuration is invalid - */ - public DataInputStream(DataStreamConfiguration configuration) throws DataStreamException { - super(configuration); - } - - /** - * This method is overridden to prevent write operations on input streams. Calling this method - * will always throw a DataStreamException with an "operation denied" message. - * - * @param data the data dictionary to write (ignored) - * @throws DataStreamException always thrown with "operation denied" message - */ - @Override - public final void write(DataDictionary data) throws DataStreamException { - DataStreamException.raiseOperationDenied("write"); - } - - /** - * This method validates that the configuration direction is set to INPUT. If the direction is not - * INPUT, a DataStreamException is thrown with details about the expected and actual direction - * values. - * - * @param configuration the configuration to validate and apply - * @throws DataStreamException if the direction is not INPUT - */ - @Override - public void setup(DataStreamConfiguration configuration) throws DataStreamException { - super.setup(configuration); - - if (DataStreamDirection.INPUT != configuration.getDirection()) { - DataStreamException.raiseInvalidConfiguration( - DataStreamConfiguration.KEY_DIRECTION - + "=" - + configuration.getDirection().name() - + "(expected " - + DataStreamDirection.INPUT.name() - + ")"); - } - } -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStream.java b/src/main/java/enedis/lab/io/datastreams/DataStream.java deleted file mode 100644 index 7fc460b..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStream.java +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * Interface defining the lifecycle management operations for data streams. - * - *

    This interface extends {@link DataStreamUser} and provides the essential lifecycle methods for - * managing data streams. It defines the core operations that all data stream implementations must - * support: opening, closing, and configuration setup. - * - * @author Enedis Smarties team - * @see DataStreamException - */ -public interface DataStream extends DataStreamUser { - /** - * Opens the data stream and prepares it for data transmission. - * - *

    This method initializes the underlying communication channel and makes the stream ready to - * handle data operations. The specific implementation depends on the stream type and - * configuration (file, network, serial, etc.). - * - *

    After a successful open operation, the stream should be ready to perform read/write - * operations as defined by the stream direction. - * - * @throws DataStreamException if the stream cannot be opened due to configuration errors, - * resource unavailability, or communication failures - */ - public void open() throws DataStreamException; - - /** - * Closes the data stream and releases associated resources. - * - *

    This method performs cleanup operations and releases any system resources (file handles, - * network connections, serial ports, etc.) that were acquired during the stream's operation. - * - *

    After closing, the stream should not be used for further operations unless it is reopened. - * Multiple close operations should be safe to call. - * - * @throws DataStreamException if an error occurs during the cleanup process - */ - public void close() throws DataStreamException; - - /** - * Configures the data stream with the specified configuration parameters. - * - *

    This method applies the given configuration to the stream, setting up parameters such as - * direction (input/output), protocol type, connection details, and other stream-specific - * settings. - * - *

    The configuration must be valid for the specific stream implementation. Invalid - * configurations will result in a DataStreamException being thrown. - * - * @param configuration the configuration parameters for this data stream - * @throws DataStreamException if the configuration is invalid, incompatible with the stream type, - * or if an error occurs during setup - */ - public void setup(DataStreamConfiguration configuration) throws DataStreamException; -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java b/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java deleted file mode 100644 index 8bb9b3f..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java +++ /dev/null @@ -1,386 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.io.channels.Channel; -import enedis.lab.io.channels.ChannelListener; -import enedis.lab.io.channels.ChannelStatus; -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.NotifierBase; -import java.util.Collection; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Abstract base class providing common functionality for data stream implementations. - * - *

    This class implements both {@link DataStream} and {@link ChannelListener} interfaces, - * providing a foundation for concrete data stream implementations. It manages the stream lifecycle, - * status tracking, and event notification to registered listeners. - * - *

    The class handles channel status changes and automatically updates the stream status - * accordingly. It also provides notification mechanisms for data events and status changes to - * subscribed listeners. - * - * @author Enedis Smarties team - * @see DataStream - * @see ChannelListener - * @see DataStreamListener - * @see DataStreamConfiguration - * @see DataStreamStatus - */ -public abstract class DataStreamBase implements DataStream, ChannelListener { - protected NotifierBase notifier; - protected DataStreamConfiguration configuration; - private DataStreamStatus status; - protected Channel channel; - protected Logger logger; - - /** - * Constructs a new DataStreamBase with the specified configuration. - * - *

    The constructor initializes the stream with the given configuration, sets up the notifier - * for event handling, and configures logging. The stream status is initially set to UNKNOWN. - * - * @param configuration the configuration used to set up the stream - * @throws DataStreamException if the configuration is invalid or setup fails - */ - public DataStreamBase(DataStreamConfiguration configuration) throws DataStreamException { - this.init(configuration); - } - - /** - * Handles channel status changes and updates the stream status accordingly. - * - *

    This method is called when the associated channel's status changes. It maps channel statuses - * to appropriate stream statuses and notifies listeners if the status has changed. - * - * @param channelName the name of the channel that changed status - * @param channelStatus the new status of the channel - */ - @Override - public void onStatusChanged(String channelName, ChannelStatus channelStatus) { - DataStreamStatus status_cpy = this.status; - - switch (channelStatus) { - case STOPPED: - { - if (DataStreamStatus.CLOSED != this.status) { - this.setStatus(DataStreamStatus.ERROR); - } - break; - } - case STARTED: - { - if (DataStreamStatus.ERROR == this.status) { - this.setStatus(DataStreamStatus.OPENED); - } - break; - } - case ERROR: - { - this.setStatus(DataStreamStatus.ERROR); - break; - } - - default: - { - } - } - - if (!this.notifier.getSubscribers().isEmpty() && (status_cpy != this.status)) { - this.notifyOnStatusChanged(this.status); - } - } - - /** - * Opens the data stream and subscribes to the associated channel. - * - *

    This method attempts to open the stream by subscribing to the channel specified in the - * configuration. If the channel exists, the stream status is set to OPENED. If the channel does - * not exist, an error is raised and the status is set to ERROR. - * - * @throws DataStreamException if the channel does not exist or if an error occurs during the - * subscription process - */ - @Override - public void open() throws DataStreamException { - String channelName = this.configuration.getChannelName(); - - if (null != channelName) { - if (null != this.channel) { - this.channel.subscribe(this); - this.setStatus(DataStreamStatus.OPENED); - this.logger.info("Stream " + this.getName() + " start"); - } else { - this.setStatus(DataStreamStatus.ERROR); - DataStreamException.raiseChannelError(channelName, "channel does not exist"); - } - } - } - - /** - * Closes the data stream and unsubscribes from the associated channel. - * - *

    This method closes the stream by unsubscribing from the channel and setting the status to - * CLOSED. If the channel does not exist, an error is raised and the status is set to ERROR. - * - * @throws DataStreamException if the channel does not exist or if an error occurs during the - * unsubscription process - */ - @Override - public void close() throws DataStreamException { - String channelName = this.configuration.getChannelName(); - - if (null != channelName) { - if (null != this.channel) { - this.channel.unsubscribe(this); - this.setStatus(DataStreamStatus.CLOSED); - this.logger.info("Service " + this.getName() + " stop"); - } else { - this.setStatus(DataStreamStatus.ERROR); - DataStreamException.raiseChannelError(channelName, "channel does not exist"); - } - } - } - - /** - * Sets up the data stream with the specified configuration. - * - *

    This method updates the stream configuration. It cannot be called when the stream is already - * open, as this would be an invalid operation. - * - * @param configuration the new configuration for the stream - * @throws DataStreamException if the stream is already open or if the configuration is invalid - */ - @Override - public void setup(DataStreamConfiguration configuration) throws DataStreamException { - if (this.status == DataStreamStatus.OPENED) { - DataStreamException.raiseInternalError( - "Cannot setup stream " + this.getName() + " (already open)"); - } - this.configuration = configuration; - } - - /** - * Subscribes a listener to receive data stream events. - * - * @param listener the listener to subscribe to stream events - */ - @Override - public void subscribe(DataStreamListener listener) { - this.notifier.subscribe(listener); - } - - /** - * Unsubscribes a listener from receiving data stream events. - * - * @param listener the listener to unsubscribe from stream events - */ - @Override - public void unsubscribe(DataStreamListener listener) { - this.notifier.unsubscribe(listener); - } - - /** - * Checks if a specific listener is subscribed to this stream. - * - * @param subscriber the listener to check - * @return true if the listener is subscribed, false otherwise - */ - @Override - public boolean hasSubscriber(DataStreamListener subscriber) { - return this.notifier.hasSubscriber(subscriber); - } - - /** - * Returns a collection of all subscribed listeners. - * - * @return a collection of all subscribed listeners - */ - @Override - public Collection getSubscribers() { - return this.notifier.getSubscribers(); - } - - /** - * Returns the name of this data stream. - * - * @return the stream name from the configuration - */ - @Override - public String getName() { - return this.configuration.getName(); - } - - /** - * Returns a clone of the current stream configuration. - * - * @return a copy of the stream configuration - */ - @Override - public DataStreamConfiguration getConfiguration() { - return (DataStreamConfiguration) this.configuration.clone(); - } - - /** - * Returns the direction of this data stream. - * - * @return the stream direction from the configuration - */ - @Override - public DataStreamDirection getDirection() { - return this.configuration.getDirection(); - } - - /** - * Returns the type of this data stream. - * - * @return the stream type from the configuration - */ - @Override - public DataStreamType getType() { - return this.configuration.getType(); - } - - /** - * Returns the current status of this data stream. - * - * @return the current stream status - */ - @Override - public DataStreamStatus getStatus() { - return this.status; - } - - /** - * Sets the channel reference for this data stream. - * - *

    This method associates a channel with the stream, enabling communication through the - * specified channel. The channel must be compatible with the stream configuration. - * - * @param channel the channel to associate with this stream - */ - public void setChannel(Channel channel) { - this.channel = channel; - } - - /** - * Notifies all subscribed listeners that new data has been received. - * - *

    This method iterates through all subscribed listeners and calls their onDataReceived method - * with the stream name and the received data. - * - * @param data the new data that was received - */ - protected void notifyOnDataReceived(DataDictionary data) { - if (data != null) { - for (DataStreamListener listener : this.notifier.getSubscribers()) { - listener.onDataReceived(this.getName(), data); - } - } - } - - /** - * Notifies all subscribed listeners that data has been sent. - * - *

    This method iterates through all subscribed listeners and calls their onDataSent method with - * the stream name and the sent data. - * - * @param data the data that was sent - */ - protected void notifyOnDataSent(DataDictionary data) { - if (data != null) { - for (DataStreamListener listener : this.notifier.getSubscribers()) { - listener.onDataSent(this.getName(), data); - } - } - } - - /** - * Notifies all subscribed listeners that the stream status has changed. - * - *

    This method iterates through all subscribed listeners and calls their onStatusChanged method - * with the stream name and the new status. - * - * @param newStatus the new status of the stream - */ - protected void notifyOnStatusChanged(DataStreamStatus newStatus) { - if (newStatus != null) { - for (DataStreamListener listener : this.notifier.getSubscribers()) { - listener.onStatusChanged(this.getName(), newStatus); - } - } - } - - /** - * Notifies all subscribed listeners that an error has been detected. - * - *

    This method iterates through all subscribed listeners and calls their onErrorDetected method - * with the stream name, error details, and associated data. - * - * @param errorCode the error code identifying the type of error - * @param errorMessage a descriptive message about the error - * @param data the data associated with the error, if any - */ - protected void notifyOnErrorDetected(int errorCode, String errorMessage, DataDictionary data) { - for (DataStreamListener listener : this.notifier.getSubscribers()) { - listener.onErrorDetected(this.getName(), errorCode, errorMessage, data); - } - } - - /** - * Sets the stream status and notifies listeners of the change. - * - *

    This is a convenience method that calls setStatus(status, true) to update the status and - * notify all listeners. - * - * @param status the new status for the stream - */ - protected void setStatus(DataStreamStatus status) { - this.setStatus(status, true); - } - - /** - * Sets the stream status with optional listener notification. - * - *

    This method updates the internal status and optionally notifies all subscribed listeners of - * the status change. The notification only occurs if the status actually changes and the notify - * parameter is true. - * - * @param status the new status for the stream - * @param notify if true, listeners will be notified of the status change - */ - protected void setStatus(DataStreamStatus status, boolean notify) { - if (status != this.status) { - this.status = status; - - if (true == notify) { - this.notifyOnStatusChanged(this.status); - } - } - } - - /** - * Initializes the data stream with the specified configuration. - * - *

    This private method sets up the internal components of the stream, including the notifier, - * initial status, configuration, and logger. It also calls the setup method to apply the - * configuration. - * - * @param configuration the configuration to initialize the stream with - * @throws DataStreamException if the initialization fails - */ - private void init(DataStreamConfiguration configuration) throws DataStreamException { - this.notifier = new NotifierBase(); - this.status = DataStreamStatus.UNKNOWN; - this.configuration = configuration; - this.logger = LogManager.getLogger(this.getClass()); - this.setup(configuration); - } -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java b/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java deleted file mode 100644 index 77a8856..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.configuration.ConfigurationBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorString; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Configuration class for data streams. - * - *

    This class extends {@link ConfigurationBase} and provides configuration management for data - * streams. It defines the essential parameters needed to configure a data stream, including name, - * type, direction, and associated channel name. - * - *

    The configuration supports multiple construction methods: from maps, data dictionaries, files, - * or direct parameter specification. It includes validation and type conversion capabilities - * through the key descriptor system. - * - * @author Enedis Smarties team - * @see ConfigurationBase - * @see DataStreamType - * @see DataStreamDirection - * @see DataDictionaryException - */ -public class DataStreamConfiguration extends ConfigurationBase { - protected static final String KEY_NAME = "name"; - protected static final String KEY_TYPE = "type"; - protected static final String KEY_DIRECTION = "direction"; - protected static final String KEY_CHANNEL_NAME = "channelName"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kName; - protected KeyDescriptorEnum kType; - protected KeyDescriptorEnum kDirection; - protected KeyDescriptorString kChannelName; - - /** - * Protected default constructor for internal use. - * - *

    This constructor initializes the configuration with default values and loads the key - * descriptors for validation and type conversion. - */ - protected DataStreamConfiguration() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructs a DataStreamConfiguration from a map of key-value pairs. - * - *

    This constructor creates a configuration by copying values from the provided map. The map - * should contain keys corresponding to the configuration parameters (name, type, direction, - * channelName). - * - * @param map the map containing configuration parameters - * @throws DataDictionaryException if the map contains invalid values or missing required keys - */ - public DataStreamConfiguration(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a DataStreamConfiguration by copying from another DataDictionary. - * - *

    This constructor creates a new configuration by copying all values from the provided - * DataDictionary. This is useful for creating configurations from existing data structures. - * - * @param other the DataDictionary to copy configuration from - * @throws DataDictionaryException if the source dictionary contains invalid values - */ - public DataStreamConfiguration(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a DataStreamConfiguration with a name and configuration file. - * - *

    This constructor initializes the configuration with a specific name and loads additional - * parameters from the specified file. The file should contain valid configuration data that can - * be parsed and applied to this configuration. - * - * @param name the configuration name - * @param file the configuration file to load parameters from - */ - public DataStreamConfiguration(String name, File file) { - this(); - this.init(name, file); - } - - /** - * Constructs a DataStreamConfiguration with specific parameter values. - * - *

    This constructor creates a configuration with all parameters explicitly set. It validates - * the parameters and updates the configuration accordingly. - * - * @param name the name of the data stream - * @param type the type of the data stream - * @param direction the direction of the data stream - * @param channelName the name of the associated channel - * @throws DataDictionaryException if any of the parameters are invalid - */ - public DataStreamConfiguration( - String name, DataStreamType type, DataStreamDirection direction, String channelName) - throws DataDictionaryException { - this(); - - this.setName(name); - this.setType(type); - this.setDirection(direction); - this.setChannelName(channelName); - - this.checkAndUpdate(); - } - - /** - * Updates optional parameters in the configuration. - * - *

    This method is called during configuration setup to update any optional parameters that may - * depend on other configuration values. - * - * @throws DataDictionaryException if the parameter update fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - super.updateOptionalParameters(); - } - - /** - * Returns the name of the data stream. - * - * @return the name of the data stream - */ - public String getName() { - return (String) this.data.get(KEY_NAME); - } - - /** - * Returns the type of the data stream. - * - * @return the type of the data stream - */ - public DataStreamType getType() { - return (DataStreamType) this.data.get(KEY_TYPE); - } - - /** - * Returns the direction of the data stream. - * - * @return the direction of the data stream - */ - public DataStreamDirection getDirection() { - return (DataStreamDirection) this.data.get(KEY_DIRECTION); - } - - /** - * Returns the name of the associated channel. - * - * @return the name of the channel associated with this data stream - */ - public String getChannelName() { - return (String) this.data.get(KEY_CHANNEL_NAME); - } - - /** - * Sets the name of the data stream. - * - * @param name the name to set for the data stream - * @throws DataDictionaryException if the name is invalid or null - */ - public void setName(String name) throws DataDictionaryException { - this.setName((Object) name); - } - - /** - * Sets the type of the data stream. - * - * @param type the type to set for the data stream - * @throws DataDictionaryException if the type is invalid - */ - public void setType(DataStreamType type) throws DataDictionaryException { - this.setType((Object) type); - } - - /** - * Sets the direction of the data stream. - * - * @param direction the direction to set for the data stream - * @throws DataDictionaryException if the direction is invalid - */ - public void setDirection(DataStreamDirection direction) throws DataDictionaryException { - this.setDirection((Object) direction); - } - - /** - * Sets the name of the associated channel. - * - * @param channelName the name of the channel to associate with this data stream - * @throws DataDictionaryException if the channel name is invalid - */ - public void setChannelName(String channelName) throws DataDictionaryException { - this.setChannelName((Object) channelName); - } - - /** - * Sets the name using object conversion and validation. - * - *

    This protected method performs the actual name setting with type conversion and validation - * through the key descriptor system. - * - * @param name the name object to convert and set - * @throws DataDictionaryException if the name conversion or validation fails - */ - protected void setName(Object name) throws DataDictionaryException { - this.data.put(KEY_NAME, this.kName.convert(name)); - } - - /** - * Sets the type using object conversion and validation. - * - *

    This protected method performs the actual type setting with enum conversion and validation - * through the key descriptor system. - * - * @param type the type object to convert and set - * @throws DataDictionaryException if the type conversion or validation fails - */ - protected void setType(Object type) throws DataDictionaryException { - this.data.put(KEY_TYPE, this.kType.convert(type)); - } - - /** - * Sets the direction using object conversion and validation. - * - *

    This protected method performs the actual direction setting with enum conversion and - * validation through the key descriptor system. - * - * @param direction the direction object to convert and set - * @throws DataDictionaryException if the direction conversion or validation fails - */ - protected void setDirection(Object direction) throws DataDictionaryException { - this.data.put(KEY_DIRECTION, this.kDirection.convert(direction)); - } - - /** - * Sets the channel name using object conversion and validation. - * - *

    This protected method performs the actual channel name setting with string conversion and - * validation through the key descriptor system. - * - * @param channelName the channel name object to convert and set - * @throws DataDictionaryException if the channel name conversion or validation fails - */ - protected void setChannelName(Object channelName) throws DataDictionaryException { - this.data.put(KEY_CHANNEL_NAME, this.kChannelName.convert(channelName)); - } - - /** - * Loads and initializes the key descriptors for configuration validation. - * - *

    This private method sets up the key descriptors that define the structure and validation - * rules for the configuration parameters. It creates descriptors for name, type, direction, and - * channel name with appropriate validation rules. - * - *

    If any descriptor creation fails, a RuntimeException is thrown with the original exception - * as the cause. - */ - private void loadKeyDescriptors() { - try { - this.kName = new KeyDescriptorString(KEY_NAME, true, false); - this.keys.add(this.kName); - - this.kType = new KeyDescriptorEnum(KEY_TYPE, true, DataStreamType.class); - this.keys.add(this.kType); - - this.kDirection = - new KeyDescriptorEnum( - KEY_DIRECTION, true, DataStreamDirection.class); - this.keys.add(this.kDirection); - - this.kChannelName = new KeyDescriptorString(KEY_CHANNEL_NAME, false, false); - this.keys.add(this.kChannelName); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java b/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java deleted file mode 100644 index 4305ebf..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * Enumeration representing the data flow direction of a stream. - * - *

    This enum defines the possible directions for data transmission in a data stream, determining - * whether the stream handles incoming data, outgoing data, or both. The direction is a fundamental - * characteristic of a stream that affects its behavior and the operations it supports. - * - * @author Enedis Smarties team - * @see DataStreamConfiguration - * @see DataStream - */ -public enum DataStreamDirection { - /** - * Input direction for streams that receive data. Streams with this direction are configured to - * read incoming data from a source. - */ - INPUT, - - /** - * Output direction for streams that send data. Streams with this direction are configured to - * write outgoing data to a destination. - */ - OUTPUT, - - /** - * Bidirectional streams that can both receive and send data. Streams with this direction support - * both read and write operations, allowing two-way communication. - */ - INOUTPUT; -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamException.java b/src/main/java/enedis/lab/io/datastreams/DataStreamException.java deleted file mode 100644 index dd57d46..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamException.java +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * Exception class for data stream related errors. - * - *

    This exception is thrown when errors occur during data stream operations such as - * configuration, initialization, opening, closing, reading, or writing. It provides static factory - * methods for creating exceptions with standardized error messages for common error scenarios. - * - *

    The class includes utility methods to raise exceptions for specific error types: channel - * errors, configuration errors, operation denials, and creation failures. - * - * @author Enedis Smarties team - * @see DataStream - * @see DataStreamConfiguration - */ -public class DataStreamException extends Exception { - private static final long serialVersionUID = 4079445021346677107L; - - /** - * Raises a channel error exception. - * - *

    This method creates and throws an exception when an error occurs on a specific channel - * during stream operations. - * - * @param channelName the name of the channel where the error occurred - * @param info additional information about the error - * @throws DataStreamException always thrown with a formatted error message - */ - public static void raiseChannelError(String channelName, String info) throws DataStreamException { - throw new DataStreamException("Error occurs on channel " + channelName + " (" + info + ")"); - } - - /** - * Raises an unhandled type exception. - * - *

    This method creates and throws an exception when a data stream type is not supported or - * handled by the current implementation. - * - * @param type the type of data stream that is not handled - * @throws DataStreamException always thrown with a formatted error message - */ - public static void raiseUnhandledType(String type) throws DataStreamException { - throw new DataStreamException(type + " datastream is not handled"); - } - - /** - * Raises an internal error exception. - * - *

    This method creates and throws an exception for internal errors that occur during stream - * processing or operations. - * - * @param info information describing the internal error - * @throws DataStreamException always thrown with the provided error information - */ - public static void raiseInternalError(String info) throws DataStreamException { - throw new DataStreamException(info); - } - - /** - * Raises an invalid configuration exception with type validation. - * - *

    This method creates and throws an exception when the provided configuration is not of the - * expected type or is incompatible with the stream requirements. - * - * @param configuration the invalid configuration object - * @param expected_configuration_name the name of the expected configuration type - * @throws DataStreamException always thrown with details about the configuration mismatch - */ - public static void raiseInvalidConfiguration( - DataStreamConfiguration configuration, String expected_configuration_name) - throws DataStreamException { - throw new DataStreamException( - "Configuration " - + configuration.getClass().getSimpleName() - + " is not valid (" - + expected_configuration_name - + " expected)"); - } - - /** - * Raises an invalid configuration exception with custom message. - * - *

    This method creates and throws an exception when a configuration is invalid, with a custom - * error message describing the specific validation failure. - * - * @param info information describing why the configuration is invalid - * @throws DataStreamException always thrown with the provided error information - */ - public static void raiseInvalidConfiguration(String info) throws DataStreamException { - throw new DataStreamException(info); - } - - /** - * Raises an operation denied exception. - * - *

    This method creates and throws an exception when an operation is not allowed on the stream, - * such as attempting to write to an input-only stream. - * - * @param operation the name of the operation that was denied - * @throws DataStreamException always thrown with a formatted error message - */ - public static void raiseOperationDenied(String operation) throws DataStreamException { - throw new DataStreamException("Operation \'" + operation + "\' is not allowed"); - } - - /** - * Raises an unexpected error exception. - * - *

    This method creates and throws an exception when an unexpected error occurs during stream - * operations that doesn't fit into other specific error categories. - * - * @param info information describing the unexpected error - * @throws DataStreamException always thrown with a formatted error message - */ - public static void raiseUnexpectedError(String info) throws DataStreamException { - throw new DataStreamException("Unexpected error occurs : " + info); - } - - /** - * Raises an already exists exception. - * - *

    This method creates and throws an exception when attempting to create a data stream that - * already exists in the system. - * - * @param dataStream the name of the data stream that already exists - * @throws DataStreamException always thrown with a formatted error message - */ - public static void raiseAlreadyExists(String dataStream) throws DataStreamException { - throw new DataStreamException("DataStream " + dataStream + " already exists"); - } - - /** - * Raises a creation failed exception. - * - *

    This method creates and throws an exception when the creation of a data stream fails for any - * reason. - * - * @param dataStream the name of the data stream that failed to be created - * @param info additional information about why the creation failed - * @throws DataStreamException always thrown with a formatted error message - */ - public static void raiseCreationFailed(String dataStream, String info) - throws DataStreamException { - throw new DataStreamException( - "Creation of dataStream " + dataStream + " creation failed : " + info); - } - - /** - * Constructs a new DataStreamException with a message and a cause. - * - * @param message the detail message describing the exception - * @param cause the cause of this exception - */ - public DataStreamException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Constructs a new DataStreamException with a detail message. - * - * @param message the detail message describing the exception - */ - public DataStreamException(String message) { - super(message); - } - - /** - * Constructs a new DataStreamException with a cause. - * - * @param cause the cause of this exception - */ - public DataStreamException(Throwable cause) { - super(cause); - } -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java b/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java deleted file mode 100644 index e0aa95b..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.Subscriber; - -/** - * Listener interface for receiving data stream events and notifications. - * - *

    This interface extends {@link Subscriber} and defines callback methods that are invoked when - * specific events occur on a data stream. Implementations of this interface can be registered with - * data streams to receive notifications about data transmission, status changes, and error - * conditions. - * - *

    The listener provides four types of notifications: - * - *

      - *
    • Data reception events when new data arrives on the stream - *
    • Data transmission events when data is sent through the stream - *
    • Status change events when the stream status changes - *
    • Error detection events when errors occur during stream operations - *
    - * - * @author Enedis Smarties team - * @see DataStream - * @see DataStreamBase - * @see DataStreamStatus - * @see DataDictionary - */ -public interface DataStreamListener extends Subscriber { - /** - * Called when data has been received on the data stream. - * - *

    This method is invoked whenever new data arrives on the stream and has been successfully - * read. Implementations should handle the received data according to their specific requirements. - * - * @param dataStreamName the name of the stream that received the data - * @param data the data dictionary containing the received data - */ - public void onDataReceived(String dataStreamName, DataDictionary data); - - /** - * Called when data has been sent through the data stream. - * - *

    This method is invoked after data has been successfully written to the stream. - * Implementations can use this to track data transmission, log sent data, or perform - * post-transmission processing. - * - * @param dataStreamName the name of the stream that sent the data - * @param data the data dictionary containing the sent data - */ - public void onDataSent(String dataStreamName, DataDictionary data); - - /** - * Called when the status of the data stream changes. - * - *

    This method is invoked whenever the stream transitions to a new status or when an error - * state is entered. Implementations can use this to monitor stream health and react to status - * changes. - * - * @param dataStreamName the name of the stream whose status changed - * @param status the new status of the stream - */ - public void onStatusChanged(String dataStreamName, DataStreamStatus status); - - /** - * Called when an error is detected during data stream operations. - * - *

    This method is invoked when an error occurs during stream processing, such as communication - * failures, data validation errors, or protocol violations. The error code and message provide - * details about the error, and the data parameter may contain the data associated with the error - * for diagnostic purposes. - * - * @param dataStreamName the name of the stream where the error was detected - * @param errorCode the error code identifying the type of error - * @param errorMessage a descriptive message about the error - * @param data the data dictionary associated with the error, if any - */ - public void onErrorDetected( - String dataStreamName, int errorCode, String errorMessage, DataDictionary data); -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java b/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java deleted file mode 100644 index 512496c..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * Enumeration representing the operational status of a data stream. - * - *

    This enum defines the possible states that a data stream can be in during its lifecycle. The - * status reflects the current operational state of the stream and is used to monitor and manage - * stream health and availability. Status changes are typically notified to registered listeners - * through the {@link DataStreamListener} interface. - * - * @author Enedis Smarties team - * @see DataStream - * @see DataStreamBase - * @see DataStreamListener - */ -public enum DataStreamStatus { - /** - * Unknown status - the default initial state. This status is assigned when a stream is first - * created and before it has been properly initialized or configured. - */ - UNKNOWN, - - /** - * Closed status - the stream is not active. This status indicates that the stream has been closed - * and is not processing data. A stream can be reopened after being closed. - */ - CLOSED, - - /** - * Opened status - the stream is active and operational. This status indicates that the stream has - * been successfully opened and is ready to perform read/write operations according to its - * direction. - */ - OPENED, - - /** - * Error status - the stream has encountered an error. This status indicates that an error has - * occurred during stream operations and the stream may not be functioning properly. Recovery or - * intervention may be required. - */ - ERROR -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamType.java b/src/main/java/enedis/lab/io/datastreams/DataStreamType.java deleted file mode 100644 index 4d1461d..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamType.java +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * Enumeration representing different types of data streams based on protocol and data format. - * - *

    This enum defines the various data stream types supported by the system, each corresponding to - * a specific communication protocol or data format. The type determines how data is encoded, - * decoded, and processed by the stream. - * - *

    Stream types range from raw unformatted data to specialized industrial protocols like Modbus - * and IEC 61850, as well as specific product protocols like TIC and LEM DC. - * - * @author Enedis Smarties team - * @see DataStreamConfiguration - * @see DataStream - */ -public enum DataStreamType { - /** - * Raw data stream without specific protocol formatting. Data is transmitted without - * protocol-specific encoding or decoding, suitable for binary data or custom formats. - */ - RAW, - - /** - * TIC (Télé Information Client) protocol stream. Used for handling data from smart meters and - * energy management devices using the TIC protocol standard. - */ - TIC, - - /** - * IEC 61850 protocol stream. Used for communication with electrical substation automation systems - * following the IEC 61850 international standard. - */ - IEC61850, - - /** - * Modbus slave mode stream. The stream operates as a Modbus slave device, responding to requests - * from a Modbus master. - */ - MODBUS_SLAVE, - - /** - * Modbus master mode stream. The stream operates as a Modbus master device, initiating requests - * to Modbus slave devices. - */ - MODBUS_MASTER, - - /** - * LEM DC product protocol stream. Used for communication with LEM (Liaison - * Électronique-Mécanique) DC measurement devices. - */ - LEM_DC; -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java b/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java deleted file mode 100644 index a5221db..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.Notifier; - -/** - * Interface defining the user-facing operations for data stream interaction. - * - *

    This interface extends {@link Notifier} with {@link DataStreamListener} support, providing the - * core functionality for reading and writing data, as well as querying stream properties and - * configuration. It represents the primary interface through which users interact with data - * streams. - * - *

    The interface provides methods for: - * - *

      - *
    • Data operations (read/write) - *
    • Stream identification (name) - *
    • Configuration access - *
    • Stream property queries (direction, type, status) - *
    • Listener management (through Notifier interface) - *
    - * - * @author Enedis Smarties team - * @see DataStream - * @see DataStreamBase - * @see DataStreamListener - * @see DataStreamConfiguration - */ -public interface DataStreamUser extends Notifier { - /** - * Reads data from the stream. - * - *

    This method retrieves data from the stream and returns it as a DataDictionary. The method - * blocks until data is available or an error occurs. The availability and behavior of this - * operation depend on the stream direction and status. - * - * @return the data read from the stream as a DataDictionary - * @throws DataStreamException if the stream is not readable, an I/O error occurs, or the - * operation is not supported by the stream direction - */ - public DataDictionary read() throws DataStreamException; - - /** - * Writes data to the stream. - * - *

    This method sends the provided data through the stream. The data is encoded according to the - * stream type and transmitted to the configured destination. The availability of this operation - * depends on the stream direction and status. - * - * @param data the data to write to the stream - * @throws DataStreamException if the stream is not writable, an I/O error occurs, the data is - * invalid, or the operation is not supported by the stream direction - */ - public void write(DataDictionary data) throws DataStreamException; - - /** - * Returns the name of this data stream. - * - *

    The name uniquely identifies the stream within the system and is used for logging, - * monitoring, and listener notifications. - * - * @return the name of the data stream - */ - public String getName(); - - /** - * Returns the configuration of this data stream. - * - *

    The configuration contains all parameters that define how the stream operates, including - * name, type, direction, and channel association. - * - * @return the stream configuration - */ - public DataStreamConfiguration getConfiguration(); - - /** - * Returns the direction of this data stream. - * - *

    The direction indicates whether the stream is configured for input (reading), output - * (writing), or bidirectional (both reading and writing) operations. - * - * @return the stream direction - */ - public DataStreamDirection getDirection(); - - /** - * Returns the type of this data stream. - * - *

    The type identifies the protocol or data format used by the stream, such as RAW, TIC, - * Modbus, or IEC 61850. - * - * @return the stream type - */ - public DataStreamType getType(); - - /** - * Returns the current status of this data stream. - * - *

    The status indicates the operational state of the stream, such as UNKNOWN, CLOSED, OPENED, - * or ERROR. This is useful for monitoring stream health and availability. - * - * @return the current stream status - */ - public DataStreamStatus getStatus(); -} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java b/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java deleted file mode 100644 index 6e0746a..0000000 --- a/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java +++ /dev/null @@ -1,441 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.serialport; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorNumberMinMax; -import enedis.lab.types.datadictionary.KeyDescriptorString; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Descriptor class for serial port information and USB device properties. - * - *

    This class extends {@link DataDictionaryBase} and provides a structured way to store and - * access serial port information, including both native serial ports and USB-based serial devices. - * It encapsulates all relevant properties needed to identify, configure, and connect to serial - * ports. - * - *

    The descriptor includes the following information: - * - *

      - *
    • Port unique identifier - *
    • Port name used to open the serial port - *
    • Port description - *
    • USB device product identifier (PID) - *
    • USB device vendor identifier (VID) - *
    • USB device product name - *
    • USB device manufacturer - *
    • USB device serial number - *
    - * - *

    The class supports distinguishing between native serial ports (e.g., COM ports) and USB serial - * devices through the {@link #isNative()} method. - * - * @author Enedis Smarties team - * @see DataDictionaryBase - * @see DataDictionaryException - */ -public class SerialPortDescriptor extends DataDictionaryBase { - protected static final String KEY_PORT_ID = "portId"; - protected static final String KEY_PORT_NAME = "portName"; - protected static final String KEY_DESCRIPTION = "description"; - protected static final String KEY_PRODUCT_ID = "productId"; - protected static final String KEY_VENDOR_ID = "vendorId"; - protected static final String KEY_PRODUCT_NAME = "productName"; - protected static final String KEY_MANUFACTURER = "manufacturer"; - protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - - private static final Number PRODUCT_ID_MIN = 0; - private static final Number PRODUCT_ID_MAX = 65535; - private static final Number VENDOR_ID_MIN = 0; - private static final Number VENDOR_ID_MAX = 65535; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kPortId; - protected KeyDescriptorString kPortName; - protected KeyDescriptorString kDescription; - protected KeyDescriptorNumberMinMax kProductId; - protected KeyDescriptorNumberMinMax kVendorId; - protected KeyDescriptorString kProductName; - protected KeyDescriptorString kManufacturer; - protected KeyDescriptorString kSerialNumber; - - /** - * Constructs an empty SerialPortDescriptor. - * - *

    This constructor initializes the descriptor with default values and loads the key - * descriptors for validation and type conversion. - */ - public SerialPortDescriptor() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructs a SerialPortDescriptor from a map of key-value pairs. - * - *

    This constructor creates a descriptor by copying values from the provided map. The map - * should contain keys corresponding to the descriptor properties. - * - * @param map the map containing serial port descriptor parameters - * @throws DataDictionaryException if the map contains invalid values - */ - public SerialPortDescriptor(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a SerialPortDescriptor by copying from another DataDictionary. - * - *

    This constructor creates a new descriptor by copying all values from the provided - * DataDictionary. This is useful for creating descriptors from existing data structures. - * - * @param other the DataDictionary to copy descriptor data from - * @throws DataDictionaryException if the source dictionary contains invalid values - */ - public SerialPortDescriptor(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a SerialPortDescriptor with all parameters explicitly set. - * - *

    This constructor creates a descriptor with all serial port and USB device properties - * specified. It validates the parameters and updates the descriptor accordingly. - * - * @param portId the port unique identifier - * @param portName the port name used to open the serial port - * @param description the port description - * @param productId the USB device product identifier (PID), must be in range [0-65535] - * @param vendorId the USB device vendor identifier (VID), must be in range [0-65535] - * @param productName the USB device product name - * @param manufacturer the USB device manufacturer - * @param serialNumber the USB device serial number - * @throws DataDictionaryException if any parameter is invalid or out of range - */ - public SerialPortDescriptor( - String portId, - String portName, - String description, - Number productId, - Number vendorId, - String productName, - String manufacturer, - String serialNumber) - throws DataDictionaryException { - this(); - - this.setPortId(portId); - this.setPortName(portName); - this.setDescription(description); - this.setProductId(productId); - this.setVendorId(vendorId); - this.setProductName(productName); - this.setManufacturer(manufacturer); - this.setSerialNumber(serialNumber); - - this.checkAndUpdate(); - } - - /** - * Returns the port unique identifier. - * - * @return the port unique identifier - */ - public String getPortId() { - return (String) this.data.get(KEY_PORT_ID); - } - - /** - * Returns the port name used to open the serial port. - * - * @return the port name used to open the serial port - */ - public String getPortName() { - return (String) this.data.get(KEY_PORT_NAME); - } - - /** - * Returns the port description. - * - * @return the port description - */ - public String getDescription() { - return (String) this.data.get(KEY_DESCRIPTION); - } - - /** - * Returns the USB device product identifier (PID). - * - * @return the USB device product identifier in range [0-65535] - */ - public Number getProductId() { - return (Number) this.data.get(KEY_PRODUCT_ID); - } - - /** - * Returns the USB device vendor identifier (VID). - * - * @return the USB device vendor identifier in range [0-65535] - */ - public Number getVendorId() { - return (Number) this.data.get(KEY_VENDOR_ID); - } - - /** - * Returns the USB device product name. - * - * @return the USB device product name - */ - public String getProductName() { - return (String) this.data.get(KEY_PRODUCT_NAME); - } - - /** - * Returns the USB device manufacturer. - * - * @return the USB device manufacturer - */ - public String getManufacturer() { - return (String) this.data.get(KEY_MANUFACTURER); - } - - /** - * Returns the USB device serial number. - * - * @return the USB device serial number - */ - public String getSerialNumber() { - return (String) this.data.get(KEY_SERIAL_NUMBER); - } - - /** - * Sets the port unique identifier. - * - * @param portId the port unique identifier - * @throws DataDictionaryException if portId is invalid - */ - public void setPortId(String portId) throws DataDictionaryException { - this.setPortId((Object) portId); - } - - /** - * Sets the port name used to open the serial port. - * - * @param portName the port name used to open the serial port - * @throws DataDictionaryException if portName is invalid - */ - public void setPortName(String portName) throws DataDictionaryException { - this.setPortName((Object) portName); - } - - /** - * Sets the port description. - * - * @param description the port description - * @throws DataDictionaryException if description is invalid - */ - public void setDescription(String description) throws DataDictionaryException { - this.setDescription((Object) description); - } - - /** - * Sets the USB device product identifier (PID). - * - * @param productId the USB device product identifier - * @throws DataDictionaryException if productId is out of range [0-65535] - */ - public void setProductId(Number productId) throws DataDictionaryException { - this.setProductId((Object) productId); - } - - /** - * Sets the USB device vendor identifier (VID). - * - * @param vendorId the USB device vendor identifier - * @throws DataDictionaryException if vendorId is out of range [0-65535] - */ - public void setVendorId(Number vendorId) throws DataDictionaryException { - this.setVendorId((Object) vendorId); - } - - /** - * Sets the USB device product name. - * - * @param productName the USB device product name - * @throws DataDictionaryException if productName is invalid - */ - public void setProductName(String productName) throws DataDictionaryException { - this.setProductName((Object) productName); - } - - /** - * Sets the USB device manufacturer. - * - * @param manufacturer the USB device manufacturer - * @throws DataDictionaryException if manufacturer is invalid - */ - public void setManufacturer(String manufacturer) throws DataDictionaryException { - this.setManufacturer((Object) manufacturer); - } - - /** - * Sets the USB device serial number. - * - * @param serialNumber the USB device serial number - * @throws DataDictionaryException if serialNumber is invalid - */ - public void setSerialNumber(String serialNumber) throws DataDictionaryException { - this.setSerialNumber((Object) serialNumber); - } - - /** - * Checks if this serial port is a native port (not USB). - * - *

    A port is considered native if it has a port name but no port ID. This typically indicates a - * built-in serial port (e.g., COM1) rather than a USB-to-serial adapter. - * - * @return true if this is a native serial port, false if it's a USB device - */ - public boolean isNative() { - return this.getPortName() != null && !this.getPortName().isEmpty() && this.getPortId() == null; - } - - /** - * Sets the port ID using object conversion and validation. - * - * @param portId the port ID object to convert and set - * @throws DataDictionaryException if the conversion or validation fails - */ - protected void setPortId(Object portId) throws DataDictionaryException { - this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); - } - - /** - * Sets the port name using object conversion and validation. - * - * @param portName the port name object to convert and set - * @throws DataDictionaryException if the conversion or validation fails - */ - protected void setPortName(Object portName) throws DataDictionaryException { - this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); - } - - /** - * Sets the description using object conversion and validation. - * - * @param description the description object to convert and set - * @throws DataDictionaryException if the conversion or validation fails - */ - protected void setDescription(Object description) throws DataDictionaryException { - this.data.put(KEY_DESCRIPTION, this.kDescription.convert(description)); - } - - /** - * Sets the product ID using object conversion and validation. - * - * @param productId the product ID object to convert and set - * @throws DataDictionaryException if the conversion or validation fails - */ - protected void setProductId(Object productId) throws DataDictionaryException { - this.data.put(KEY_PRODUCT_ID, this.kProductId.convert(productId)); - } - - /** - * Sets the vendor ID using object conversion and validation. - * - * @param vendorId the vendor ID object to convert and set - * @throws DataDictionaryException if the conversion or validation fails - */ - protected void setVendorId(Object vendorId) throws DataDictionaryException { - this.data.put(KEY_VENDOR_ID, this.kVendorId.convert(vendorId)); - } - - /** - * Sets the product name using object conversion and validation. - * - * @param productName the product name object to convert and set - * @throws DataDictionaryException if the conversion or validation fails - */ - protected void setProductName(Object productName) throws DataDictionaryException { - this.data.put(KEY_PRODUCT_NAME, this.kProductName.convert(productName)); - } - - /** - * Sets the manufacturer using object conversion and validation. - * - * @param manufacturer the manufacturer object to convert and set - * @throws DataDictionaryException if the conversion or validation fails - */ - protected void setManufacturer(Object manufacturer) throws DataDictionaryException { - this.data.put(KEY_MANUFACTURER, this.kManufacturer.convert(manufacturer)); - } - - /** - * Sets the serial number using object conversion and validation. - * - * @param serialNumber the serial number object to convert and set - * @throws DataDictionaryException if the conversion or validation fails - */ - protected void setSerialNumber(Object serialNumber) throws DataDictionaryException { - this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); - } - - /** - * Loads and initializes the key descriptors for descriptor validation. - * - *

    This private method sets up the key descriptors that define the structure and validation - * rules for all serial port descriptor properties. It creates descriptors for both string and - * numeric fields with appropriate validation constraints (e.g., product and vendor IDs must be in - * range [0-65535]). - * - *

    If any descriptor creation fails, a RuntimeException is thrown with the original exception - * as the cause. - */ - private void loadKeyDescriptors() { - try { - this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); - this.keys.add(this.kPortId); - - this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); - this.keys.add(this.kPortName); - - this.kDescription = new KeyDescriptorString(KEY_DESCRIPTION, false, false); - this.keys.add(this.kDescription); - - this.kProductId = - new KeyDescriptorNumberMinMax(KEY_PRODUCT_ID, false, PRODUCT_ID_MIN, PRODUCT_ID_MAX); - this.keys.add(this.kProductId); - - this.kVendorId = - new KeyDescriptorNumberMinMax(KEY_VENDOR_ID, false, VENDOR_ID_MIN, VENDOR_ID_MAX); - this.keys.add(this.kVendorId); - - this.kProductName = new KeyDescriptorString(KEY_PRODUCT_NAME, false, false); - this.keys.add(this.kProductName); - - this.kManufacturer = new KeyDescriptorString(KEY_MANUFACTURER, false, false); - this.keys.add(this.kManufacturer); - - this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, false); - this.keys.add(this.kSerialNumber); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java b/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java deleted file mode 100644 index 99f5f82..0000000 --- a/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import enedis.lab.io.serialport.SerialPortDescriptor; -import enedis.lab.io.usb.USBPortDescriptor; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * TICPortDescriptor class - * - *

    Generated - */ -public class TICPortDescriptor extends SerialPortDescriptor { - protected static final String KEY_MODEM_TYPE = "modemType"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kModemType; - - protected TICPortDescriptor() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICPortDescriptor(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICPortDescriptor(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param portId - * @param portName - * @param description - * @param productName - * @param manufacturer - * @param serialNumber - * @param modemType - * @throws DataDictionaryException - */ - public TICPortDescriptor( - String portId, - String portName, - String description, - String productName, - String manufacturer, - String serialNumber, - TICModemType modemType) - throws DataDictionaryException { - this(); - - this.setPortId(portId); - this.setPortName(portName); - this.setDescription(description); - this.setProductName(productName); - this.setManufacturer(manufacturer); - this.setSerialNumber(serialNumber); - this.setModemType(modemType); - - this.checkAndUpdate(); - } - - /** - * Constructor setting parameters to specific values - * - * @param serialPortDescriptor - * @param modemType - * @throws DataDictionaryException - */ - public TICPortDescriptor(SerialPortDescriptor serialPortDescriptor, TICModemType modemType) - throws DataDictionaryException { - this(); - - this.checkProductId(serialPortDescriptor.getProductId(), modemType); - this.checkVendorId(serialPortDescriptor.getVendorId(), modemType); - - this.setPortId(serialPortDescriptor.getPortId()); - this.setPortName(serialPortDescriptor.getPortName()); - this.setDescription(serialPortDescriptor.getDescription()); - this.setProductId(serialPortDescriptor.getProductId()); - this.setVendorId(serialPortDescriptor.getVendorId()); - this.setProductName(serialPortDescriptor.getProductName()); - this.setManufacturer(serialPortDescriptor.getManufacturer()); - this.setSerialNumber(serialPortDescriptor.getSerialNumber()); - this.setModemType(modemType); - - this.checkAndUpdate(); - } - - /** - * Constructor setting parameters to specific values - * - * @param usbPortDescriptor - * @param modemType - * @throws DataDictionaryException - */ - public TICPortDescriptor(USBPortDescriptor usbPortDescriptor, TICModemType modemType) - throws DataDictionaryException { - this(); - - this.checkProductId(usbPortDescriptor.getIdProduct(), modemType); - this.checkVendorId(usbPortDescriptor.getIdVendor(), modemType); - - this.setProductId(usbPortDescriptor.getIdProduct()); - this.setVendorId(usbPortDescriptor.getIdVendor()); - this.setProductName(usbPortDescriptor.getProduct()); - this.setManufacturer(usbPortDescriptor.getManufacturer()); - this.setSerialNumber(usbPortDescriptor.getSerialNumber()); - this.setModemType(modemType); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (this.exists(KEY_MODEM_TYPE)) { - if (this.getModemType() != null) { - this.setProductId(this.getModemType().getProductId()); - this.setVendorId(this.getModemType().getVendorId()); - } else { - this.setProductId(null); - this.setVendorId(null); - } - } - super.updateOptionalParameters(); - } - - /** - * Get modem type - * - * @return the modem type - */ - public TICModemType getModemType() { - return (TICModemType) this.data.get(KEY_MODEM_TYPE); - } - - /** - * Set modem type - * - * @param modemType - * @throws DataDictionaryException - */ - public void setModemType(TICModemType modemType) throws DataDictionaryException { - this.setModemType((Object) modemType); - if (modemType != null) { - this.setProductId(modemType.getProductId()); - this.setVendorId(modemType.getVendorId()); - } else { - this.setProductId(null); - this.setVendorId(null); - } - } - - protected void setModemType(Object modemType) throws DataDictionaryException { - if (modemType == null) { - this.data.put(KEY_MODEM_TYPE, null); - } else { - this.data.put(KEY_MODEM_TYPE, this.kModemType.convert(modemType)); - } - } - - private void checkProductId(Number productId, TICModemType modemType) - throws DataDictionaryException { - if (modemType != null - && productId != null - && productId.intValue() != modemType.getProductId()) { - throw new DataDictionaryException("TIC modem productId is inconsistent with the given one"); - } - } - - private void checkVendorId(Number vendorId, TICModemType modemType) - throws DataDictionaryException { - if (modemType != null && vendorId != null && vendorId.intValue() != modemType.getVendorId()) { - throw new DataDictionaryException("TIC modem vendorId is inconsistent with the given one"); - } - } - - private void loadKeyDescriptors() { - try { - this.kModemType = - new KeyDescriptorEnum(KEY_MODEM_TYPE, false, TICModemType.class); - this.keys.add(this.kModemType); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/tic/TICPortFinder.java b/src/main/java/enedis/lab/io/tic/TICPortFinder.java deleted file mode 100644 index aefb888..0000000 --- a/src/main/java/enedis/lab/io/tic/TICPortFinder.java +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import enedis.lab.io.PortFinder; - -/** Interface used to find all TIC port descriptor */ -public interface TICPortFinder extends PortFinder { - /** - * Find TIC port descriptor matching with port id - * - * @param portId the unique port identifier desired - * @return TIC port descriptor found, or null if nothing matches with portId - */ - public default TICPortDescriptor findByPortId(String portId) { - return this.findAll().stream() - .filter(p -> (p.getPortId() != null) ? p.getPortId().equals(portId) : portId == null) - .findFirst() - .orElse(null); - } - - /** - * Find TIC port descriptor matching with port name - * - * @param portName the port name desired - * @return TIC port descriptor found, or null if nothing matches with portName - */ - public default TICPortDescriptor findByPortName(String portName) { - return this.findAll().stream() - .filter( - p -> (p.getPortName() != null) ? p.getPortName().equals(portName) : portName == null) - .findFirst() - .orElse(null); - } - - /** - * Find native TIC port (not USB) descriptor matching with port name - * - * @param portName the port name desired - * @return TIC port descriptor found, or null if nothing matches with portName - */ - public TICPortDescriptor findNative(String portName); - - /** - * Find TIC port descriptor matching with port id or port name - * - * @param portId the unique port identifier desired - * @param portName the port name desired - * @return TIC port descriptor found, or null if nothing matches with portName - */ - public default TICPortDescriptor findByPortIdOrPortName(String portId, String portName) { - TICPortDescriptor descriptor = this.findByPortId(portId); - - if (descriptor == null) { - descriptor = this.findByPortName(portId); - } - - return descriptor; - } -} diff --git a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java b/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java deleted file mode 100644 index eb40b6b..0000000 --- a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import enedis.lab.io.serialport.SerialPortDescriptor; -import enedis.lab.io.serialport.SerialPortFinder; -import enedis.lab.io.serialport.SerialPortFinderBase; -import enedis.lab.io.usb.USBPortDescriptor; -import enedis.lab.io.usb.USBPortFinder; -import enedis.lab.io.usb.USBPortFinderBase; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; -import java.util.List; - -/** Class used to find all TIC port descriptor */ -public class TICPortFinderBase implements TICPortFinder { - /** - * Program writing the TIC port descriptor list (JSON format) on the output stream - * - * @param args not used - */ - public static void main(String[] args) { - DataList descriptors = getInstance().findAll(); - - System.out.println(descriptors.toString(2)); - } - - /** - * Get instance - * - * @return Unique instance - */ - public static TICPortFinderBase getInstance() { - if (instance == null) { - instance = new TICPortFinderBase(); - } - - return instance; - } - - private static TICPortFinderBase instance; - - private SerialPortFinder serialPortFinder; - private USBPortFinder usbPortFinder; - - private TICPortFinderBase() { - this(SerialPortFinderBase.getInstance(), USBPortFinderBase.getInstance()); - } - - /** - * Constructor with finder parameters - * - * @param serialPortFinder the serial port finder interface - * @param usbPortFinder the USB port finder interface - */ - public TICPortFinderBase(SerialPortFinder serialPortFinder, USBPortFinder usbPortFinder) { - if (serialPortFinder == null) { - throw new IllegalArgumentException("Cannot set null serial port finder"); - } - this.serialPortFinder = serialPortFinder; - if (usbPortFinder == null) { - throw new IllegalArgumentException("Cannot set null USB port finder"); - } - this.usbPortFinder = usbPortFinder; - } - - @Override - public DataList findAll() { - DataList ticSerialPort = new DataArrayList(); - - for (TICModemType modemType : TICModemType.values()) { - List tmpSerialPort = - this.serialPortFinder.findByProductIdAndVendorId( - modemType.getProductId(), modemType.getVendorId()); - - if (tmpSerialPort.isEmpty()) { - List tmpUSBPort = - this.usbPortFinder.findByProductIdAndVendorId( - modemType.getProductId(), modemType.getVendorId()); - - for (USBPortDescriptor upd : tmpUSBPort) { - try { - TICPortDescriptor tic = new TICPortDescriptor(upd, modemType); - ticSerialPort.add(tic); - } catch (DataDictionaryException e) { - } - } - } else { - for (SerialPortDescriptor spd : tmpSerialPort) { - try { - TICPortDescriptor tic = new TICPortDescriptor(spd, modemType); - ticSerialPort.add(tic); - } catch (DataDictionaryException e) { - } - } - } - } - - return ticSerialPort; - } - - @Override - public TICPortDescriptor findNative(String portName) { - TICPortDescriptor ticPortDescriptor = null; - SerialPortDescriptor serialPortDescriptor = this.serialPortFinder.findNative(portName); - - if (serialPortDescriptor != null) { - try { - ticPortDescriptor = new TICPortDescriptor(serialPortDescriptor, null); - } catch (DataDictionaryException e) { - } - } - - return ticPortDescriptor; - } -} diff --git a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java b/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java deleted file mode 100644 index 1d229f9..0000000 --- a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import enedis.lab.io.PlugSubscriber; -import enedis.lab.io.PortPlugNotifier; -import org.json.JSONObject; - -/** Class used to notify when a TIC port has been plugged or unplugged */ -public class TICPortPlugNotifier extends PortPlugNotifier { - private static final int DEFAULT_JSON_INDENTATION = 2; - - /** - * Program writing on the output stream when an TIC port has been plugged or unplugged - * - * @param args not used - */ - public static void main(String[] args) { - /* 1. Create notification service */ - TICPortPlugNotifier notifier = new TICPortPlugNotifier(); - /* 2. Create subscriber to print when an TIC port has been plugged or unplugged */ - PlugSubscriber subscriber = - new PlugSubscriber() { - @Override - public void onPlugged(TICPortDescriptor descriptor) { - JSONObject jsonObject = new JSONObject(); - jsonObject.put(notifier.getClass().getSimpleName() + ".onPlugged", descriptor.toJSON()); - System.out.println(jsonObject.toString(DEFAULT_JSON_INDENTATION) + "\n"); - } - - @Override - public void onUnplugged(TICPortDescriptor descriptor) { - JSONObject jsonObject = new JSONObject(); - jsonObject.put( - notifier.getClass().getSimpleName() + ".onUnplugged", descriptor.toJSON()); - System.out.println(jsonObject.toString(DEFAULT_JSON_INDENTATION) + "\n"); - } - }; - /* 3. Run program printing when an TIC port has been plugged or unplugged until CTRL+C is pressed */ - PortPlugNotifier.main(notifier, subscriber); - } - - /** - * Constructor with default parameter - * - * @see #DEFAULT_PERIOD - * @see TICPortFinderBase - */ - public TICPortPlugNotifier() { - this(DEFAULT_PERIOD, TICPortFinderBase.getInstance()); - } - - /** - * Constructor with all parameters - * - * @param period the period (in milliseconds) used to look for plugged or unplugged TIC port - * @param finder the TIC port finder interface used to find all TIC port descriptors - */ - public TICPortPlugNotifier(long period, TICPortFinder finder) { - super(period, finder); - } -} diff --git a/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java b/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java deleted file mode 100644 index 0c26440..0000000 --- a/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java +++ /dev/null @@ -1,609 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.usb; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * USBPortDescriptor class - * - *

    Generated - */ -public class USBPortDescriptor extends DataDictionaryBase { - protected static final String KEY_BCD_DEVICE = "bcdDevice"; - protected static final String KEY_BCD_USB = "bcdUSB"; - protected static final String KEY_B_DESCRIPTOR_TYPE = "bDescriptorType"; - protected static final String KEY_B_DEVICE_CLASS = "bDeviceClass"; - protected static final String KEY_B_DEVICE_PROTOCOL = "bDeviceProtocol"; - protected static final String KEY_B_DEVICE_SUB_CLASS = "bDeviceSubClass"; - protected static final String KEY_B_LENGTH = "bLength"; - protected static final String KEY_B_MAX_PACKET_SIZE0 = "bMaxPacketSize0"; - protected static final String KEY_B_NUM_CONFIGURATIONS = "bNumConfigurations"; - protected static final String KEY_ID_PRODUCT = "idProduct"; - protected static final String KEY_ID_VENDOR = "idVendor"; - protected static final String KEY_I_MANUFACTURER = "iManufacturer"; - protected static final String KEY_I_PRODUCT = "iProduct"; - protected static final String KEY_I_SERIAL_NUMBER = "iSerialNumber"; - protected static final String KEY_MANUFACTURER = "manufacturer"; - protected static final String KEY_PRODUCT = "product"; - protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorNumber kBcdDevice; - protected KeyDescriptorNumber kBcdUSB; - protected KeyDescriptorNumber kBDescriptorType; - protected KeyDescriptorNumber kBDeviceClass; - protected KeyDescriptorNumber kBDeviceProtocol; - protected KeyDescriptorNumber kBDeviceSubClass; - protected KeyDescriptorNumber kBLength; - protected KeyDescriptorNumber kBMaxPacketSize0; - protected KeyDescriptorNumber kBNumConfigurations; - protected KeyDescriptorNumber kIdProduct; - protected KeyDescriptorNumber kIdVendor; - protected KeyDescriptorNumber kIManufacturer; - protected KeyDescriptorNumber kIProduct; - protected KeyDescriptorNumber kISerialNumber; - protected KeyDescriptorString kManufacturer; - protected KeyDescriptorString kProduct; - protected KeyDescriptorString kSerialNumber; - - protected USBPortDescriptor() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public USBPortDescriptor(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public USBPortDescriptor(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param bcdDevice - * @param bcdUSB - * @param bDescriptorType - * @param bDeviceClass - * @param bDeviceProtocol - * @param bDeviceSubClass - * @param bLength - * @param bMaxPacketSize0 - * @param bNumConfigurations - * @param idProduct - * @param idVendor - * @param iManufacturer - * @param iProduct - * @param iSerialNumber - * @param manufacturer - * @param product - * @param serialNumber - * @throws DataDictionaryException - */ - public USBPortDescriptor( - Number bcdDevice, - Number bcdUSB, - Number bDescriptorType, - Number bDeviceClass, - Number bDeviceProtocol, - Number bDeviceSubClass, - Number bLength, - Number bMaxPacketSize0, - Number bNumConfigurations, - Number idProduct, - Number idVendor, - Number iManufacturer, - Number iProduct, - Number iSerialNumber, - String manufacturer, - String product, - String serialNumber) - throws DataDictionaryException { - this(); - - this.setBcdDevice(bcdDevice); - this.setBcdUSB(bcdUSB); - this.setBDescriptorType(bDescriptorType); - this.setBDeviceClass(bDeviceClass); - this.setBDeviceProtocol(bDeviceProtocol); - this.setBDeviceSubClass(bDeviceSubClass); - this.setBLength(bLength); - this.setBMaxPacketSize0(bMaxPacketSize0); - this.setBNumConfigurations(bNumConfigurations); - this.setIdProduct(idProduct); - this.setIdVendor(idVendor); - this.setIManufacturer(iManufacturer); - this.setIProduct(iProduct); - this.setISerialNumber(iSerialNumber); - this.setManufacturer(manufacturer); - this.setProduct(product); - this.setSerialNumber(serialNumber); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - super.updateOptionalParameters(); - } - - /** - * Get bcd device - * - * @return the bcd device - */ - public Number getBcdDevice() { - return (Number) this.data.get(KEY_BCD_DEVICE); - } - - /** - * Get bcd u s b - * - * @return the bcd u s b - */ - public Number getBcdUSB() { - return (Number) this.data.get(KEY_BCD_USB); - } - - /** - * Get b descriptor type - * - * @return the b descriptor type - */ - public Number getBDescriptorType() { - return (Number) this.data.get(KEY_B_DESCRIPTOR_TYPE); - } - - /** - * Get b device class - * - * @return the b device class - */ - public Number getBDeviceClass() { - return (Number) this.data.get(KEY_B_DEVICE_CLASS); - } - - /** - * Get b device protocol - * - * @return the b device protocol - */ - public Number getBDeviceProtocol() { - return (Number) this.data.get(KEY_B_DEVICE_PROTOCOL); - } - - /** - * Get b device sub class - * - * @return the b device sub class - */ - public Number getBDeviceSubClass() { - return (Number) this.data.get(KEY_B_DEVICE_SUB_CLASS); - } - - /** - * Get b length - * - * @return the b length - */ - public Number getBLength() { - return (Number) this.data.get(KEY_B_LENGTH); - } - - /** - * Get b max packet size0 - * - * @return the b max packet size0 - */ - public Number getBMaxPacketSize0() { - return (Number) this.data.get(KEY_B_MAX_PACKET_SIZE0); - } - - /** - * Get b num configurations - * - * @return the b num configurations - */ - public Number getBNumConfigurations() { - return (Number) this.data.get(KEY_B_NUM_CONFIGURATIONS); - } - - /** - * Get id product - * - * @return the id product - */ - public Number getIdProduct() { - return (Number) this.data.get(KEY_ID_PRODUCT); - } - - /** - * Get id vendor - * - * @return the id vendor - */ - public Number getIdVendor() { - return (Number) this.data.get(KEY_ID_VENDOR); - } - - /** - * Get i manufacturer - * - * @return the i manufacturer - */ - public Number getIManufacturer() { - return (Number) this.data.get(KEY_I_MANUFACTURER); - } - - /** - * Get i product - * - * @return the i product - */ - public Number getIProduct() { - return (Number) this.data.get(KEY_I_PRODUCT); - } - - /** - * Get i serial number - * - * @return the i serial number - */ - public Number getISerialNumber() { - return (Number) this.data.get(KEY_I_SERIAL_NUMBER); - } - - /** - * Get manufacturer - * - * @return the manufacturer - */ - public String getManufacturer() { - return (String) this.data.get(KEY_MANUFACTURER); - } - - /** - * Get product - * - * @return the product - */ - public String getProduct() { - return (String) this.data.get(KEY_PRODUCT); - } - - /** - * Get serial number - * - * @return the serial number - */ - public String getSerialNumber() { - return (String) this.data.get(KEY_SERIAL_NUMBER); - } - - /** - * Set bcd device - * - * @param bcdDevice - * @throws DataDictionaryException - */ - public void setBcdDevice(Number bcdDevice) throws DataDictionaryException { - this.setBcdDevice((Object) bcdDevice); - } - - /** - * Set bcd u s b - * - * @param bcdUSB - * @throws DataDictionaryException - */ - public void setBcdUSB(Number bcdUSB) throws DataDictionaryException { - this.setBcdUSB((Object) bcdUSB); - } - - /** - * Set b descriptor type - * - * @param bDescriptorType - * @throws DataDictionaryException - */ - public void setBDescriptorType(Number bDescriptorType) throws DataDictionaryException { - this.setBDescriptorType((Object) bDescriptorType); - } - - /** - * Set b device class - * - * @param bDeviceClass - * @throws DataDictionaryException - */ - public void setBDeviceClass(Number bDeviceClass) throws DataDictionaryException { - this.setBDeviceClass((Object) bDeviceClass); - } - - /** - * Set b device protocol - * - * @param bDeviceProtocol - * @throws DataDictionaryException - */ - public void setBDeviceProtocol(Number bDeviceProtocol) throws DataDictionaryException { - this.setBDeviceProtocol((Object) bDeviceProtocol); - } - - /** - * Set b device sub class - * - * @param bDeviceSubClass - * @throws DataDictionaryException - */ - public void setBDeviceSubClass(Number bDeviceSubClass) throws DataDictionaryException { - this.setBDeviceSubClass((Object) bDeviceSubClass); - } - - /** - * Set b length - * - * @param bLength - * @throws DataDictionaryException - */ - public void setBLength(Number bLength) throws DataDictionaryException { - this.setBLength((Object) bLength); - } - - /** - * Set b max packet size0 - * - * @param bMaxPacketSize0 - * @throws DataDictionaryException - */ - public void setBMaxPacketSize0(Number bMaxPacketSize0) throws DataDictionaryException { - this.setBMaxPacketSize0((Object) bMaxPacketSize0); - } - - /** - * Set b num configurations - * - * @param bNumConfigurations - * @throws DataDictionaryException - */ - public void setBNumConfigurations(Number bNumConfigurations) throws DataDictionaryException { - this.setBNumConfigurations((Object) bNumConfigurations); - } - - /** - * Set id product - * - * @param idProduct - * @throws DataDictionaryException - */ - public void setIdProduct(Number idProduct) throws DataDictionaryException { - this.setIdProduct((Object) idProduct); - } - - /** - * Set id vendor - * - * @param idVendor - * @throws DataDictionaryException - */ - public void setIdVendor(Number idVendor) throws DataDictionaryException { - this.setIdVendor((Object) idVendor); - } - - /** - * Set i manufacturer - * - * @param iManufacturer - * @throws DataDictionaryException - */ - public void setIManufacturer(Number iManufacturer) throws DataDictionaryException { - this.setIManufacturer((Object) iManufacturer); - } - - /** - * Set i product - * - * @param iProduct - * @throws DataDictionaryException - */ - public void setIProduct(Number iProduct) throws DataDictionaryException { - this.setIProduct((Object) iProduct); - } - - /** - * Set i serial number - * - * @param iSerialNumber - * @throws DataDictionaryException - */ - public void setISerialNumber(Number iSerialNumber) throws DataDictionaryException { - this.setISerialNumber((Object) iSerialNumber); - } - - /** - * Set manufacturer - * - * @param manufacturer - * @throws DataDictionaryException - */ - public void setManufacturer(String manufacturer) throws DataDictionaryException { - this.setManufacturer((Object) manufacturer); - } - - /** - * Set product - * - * @param product - * @throws DataDictionaryException - */ - public void setProduct(String product) throws DataDictionaryException { - this.setProduct((Object) product); - } - - /** - * Set serial number - * - * @param serialNumber - * @throws DataDictionaryException - */ - public void setSerialNumber(String serialNumber) throws DataDictionaryException { - this.setSerialNumber((Object) serialNumber); - } - - protected void setBcdDevice(Object bcdDevice) throws DataDictionaryException { - this.data.put(KEY_BCD_DEVICE, this.kBcdDevice.convert(bcdDevice)); - } - - protected void setBcdUSB(Object bcdUSB) throws DataDictionaryException { - this.data.put(KEY_BCD_USB, this.kBcdUSB.convert(bcdUSB)); - } - - protected void setBDescriptorType(Object bDescriptorType) throws DataDictionaryException { - this.data.put(KEY_B_DESCRIPTOR_TYPE, this.kBDescriptorType.convert(bDescriptorType)); - } - - protected void setBDeviceClass(Object bDeviceClass) throws DataDictionaryException { - this.data.put(KEY_B_DEVICE_CLASS, this.kBDeviceClass.convert(bDeviceClass)); - } - - protected void setBDeviceProtocol(Object bDeviceProtocol) throws DataDictionaryException { - this.data.put(KEY_B_DEVICE_PROTOCOL, this.kBDeviceProtocol.convert(bDeviceProtocol)); - } - - protected void setBDeviceSubClass(Object bDeviceSubClass) throws DataDictionaryException { - this.data.put(KEY_B_DEVICE_SUB_CLASS, this.kBDeviceSubClass.convert(bDeviceSubClass)); - } - - protected void setBLength(Object bLength) throws DataDictionaryException { - this.data.put(KEY_B_LENGTH, this.kBLength.convert(bLength)); - } - - protected void setBMaxPacketSize0(Object bMaxPacketSize0) throws DataDictionaryException { - this.data.put(KEY_B_MAX_PACKET_SIZE0, this.kBMaxPacketSize0.convert(bMaxPacketSize0)); - } - - protected void setBNumConfigurations(Object bNumConfigurations) throws DataDictionaryException { - this.data.put(KEY_B_NUM_CONFIGURATIONS, this.kBNumConfigurations.convert(bNumConfigurations)); - } - - protected void setIdProduct(Object idProduct) throws DataDictionaryException { - this.data.put(KEY_ID_PRODUCT, this.kIdProduct.convert(idProduct)); - } - - protected void setIdVendor(Object idVendor) throws DataDictionaryException { - this.data.put(KEY_ID_VENDOR, this.kIdVendor.convert(idVendor)); - } - - protected void setIManufacturer(Object iManufacturer) throws DataDictionaryException { - this.data.put(KEY_I_MANUFACTURER, this.kIManufacturer.convert(iManufacturer)); - } - - protected void setIProduct(Object iProduct) throws DataDictionaryException { - this.data.put(KEY_I_PRODUCT, this.kIProduct.convert(iProduct)); - } - - protected void setISerialNumber(Object iSerialNumber) throws DataDictionaryException { - this.data.put(KEY_I_SERIAL_NUMBER, this.kISerialNumber.convert(iSerialNumber)); - } - - protected void setManufacturer(Object manufacturer) throws DataDictionaryException { - this.data.put(KEY_MANUFACTURER, this.kManufacturer.convert(manufacturer)); - } - - protected void setProduct(Object product) throws DataDictionaryException { - this.data.put(KEY_PRODUCT, this.kProduct.convert(product)); - } - - protected void setSerialNumber(Object serialNumber) throws DataDictionaryException { - this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); - } - - private void loadKeyDescriptors() { - try { - this.kBcdDevice = new KeyDescriptorNumber(KEY_BCD_DEVICE, true); - this.keys.add(this.kBcdDevice); - - this.kBcdUSB = new KeyDescriptorNumber(KEY_BCD_USB, true); - this.keys.add(this.kBcdUSB); - - this.kBDescriptorType = new KeyDescriptorNumber(KEY_B_DESCRIPTOR_TYPE, true); - this.keys.add(this.kBDescriptorType); - - this.kBDeviceClass = new KeyDescriptorNumber(KEY_B_DEVICE_CLASS, true); - this.keys.add(this.kBDeviceClass); - - this.kBDeviceProtocol = new KeyDescriptorNumber(KEY_B_DEVICE_PROTOCOL, true); - this.keys.add(this.kBDeviceProtocol); - - this.kBDeviceSubClass = new KeyDescriptorNumber(KEY_B_DEVICE_SUB_CLASS, true); - this.keys.add(this.kBDeviceSubClass); - - this.kBLength = new KeyDescriptorNumber(KEY_B_LENGTH, true); - this.keys.add(this.kBLength); - - this.kBMaxPacketSize0 = new KeyDescriptorNumber(KEY_B_MAX_PACKET_SIZE0, true); - this.keys.add(this.kBMaxPacketSize0); - - this.kBNumConfigurations = new KeyDescriptorNumber(KEY_B_NUM_CONFIGURATIONS, true); - this.keys.add(this.kBNumConfigurations); - - this.kIdProduct = new KeyDescriptorNumber(KEY_ID_PRODUCT, true); - this.keys.add(this.kIdProduct); - - this.kIdVendor = new KeyDescriptorNumber(KEY_ID_VENDOR, true); - this.keys.add(this.kIdVendor); - - this.kIManufacturer = new KeyDescriptorNumber(KEY_I_MANUFACTURER, true); - this.keys.add(this.kIManufacturer); - - this.kIProduct = new KeyDescriptorNumber(KEY_I_PRODUCT, true); - this.keys.add(this.kIProduct); - - this.kISerialNumber = new KeyDescriptorNumber(KEY_I_SERIAL_NUMBER, true); - this.keys.add(this.kISerialNumber); - - this.kManufacturer = new KeyDescriptorString(KEY_MANUFACTURER, false, true); - this.keys.add(this.kManufacturer); - - this.kProduct = new KeyDescriptorString(KEY_PRODUCT, false, true); - this.keys.add(this.kProduct); - - this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, true); - this.keys.add(this.kSerialNumber); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/usb/USBPortFinder.java b/src/main/java/enedis/lab/io/usb/USBPortFinder.java deleted file mode 100644 index 1f3a0ca..0000000 --- a/src/main/java/enedis/lab/io/usb/USBPortFinder.java +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.usb; - -import enedis.lab.io.PortFinder; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; -import java.util.stream.Collectors; - -/** Interface used to find all USB port descriptor */ -public interface USBPortFinder extends PortFinder { - /** - * Find USB device with the given product id - * - * @param idProduct the USB product identifier - * @return the USB descriptor data list of all USB device connected with the given product id - */ - public default DataList findByProductId(int idProduct) { - DataList descriptors = new DataArrayList(); - // @formatter:off - descriptors.addAll( - this.findAll().stream() - .filter(d -> d.getIdProduct().intValue() == idProduct) - .collect(Collectors.toList())); - // @formatter:on - return descriptors; - } - - /** - * Find USB device with the given vendor id - * - * @param idVendor the USB vendor identifier - * @return the USB descriptor data list of all USB device connected with the given vendor id - */ - public default DataList findByVendorId(int idVendor) { - DataList descriptors = new DataArrayList(); - // @formatter:off - descriptors.addAll( - this.findAll().stream() - .filter(d -> d.getIdVendor().intValue() == idVendor) - .collect(Collectors.toList())); - // @formatter:on - return descriptors; - } - - /** - * Find USB device with the given product id and vendor id - * - * @param idProduct the USB product identifier - * @param idVendor the USB vendor identifier - * @return the USB descriptor data list of all USB device connected with the given product id and - * vendor id - */ - public default DataList findByProductIdAndVendorId( - int idProduct, int idVendor) { - DataList descriptors = new DataArrayList(); - // @formatter:off - descriptors.addAll( - this.findAll().stream() - .filter(d -> d.getIdProduct().intValue() == idProduct) - .filter(d -> d.getIdVendor().intValue() == idVendor) - .collect(Collectors.toList())); - // @formatter:on - return descriptors; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/TICMode.java b/src/main/java/enedis/lab/protocol/tic/TICMode.java deleted file mode 100644 index a2df471..0000000 --- a/src/main/java/enedis/lab/protocol/tic/TICMode.java +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic; - -import enedis.lab.protocol.tic.frame.TICError; -import enedis.lab.protocol.tic.frame.standard.TICException; -import java.util.Arrays; - -/** - * Enumeration of available TIC modes and related utilities. - * - *

    This enum defines the supported TIC protocol modes (STANDARD, HISTORIC, AUTO, UNKNOWN) and - * provides utility methods for mode detection from frame or group buffers. It also defines - * protocol-specific separator and buffer start patterns. - * - *

    Key features: - * - *

      - *
    • Defines all supported TIC modes - *
    • Provides methods to detect mode from frame or group buffers - *
    • Defines protocol-specific separators and buffer start patterns - *
    - * - * @author Enedis Smarties team - */ -public enum TICMode { - /** Unknown mode. */ - UNKNOWN, - /** Standard mode. */ - STANDARD, - /** Historic mode. */ - HISTORIC, - /** Auto-detect mode. */ - AUTO; - - /** Separator character (space, 0x20) for historic TIC frames. */ - public static final char HISTORIC_SEPARATOR = ' '; - - /** Separator character (tab, 0x09) for standard TIC frames. */ - public static final char STANDARD_SEPARATOR = '\t'; - - /** Buffer start pattern for historic TIC frames. */ - public static final byte[] HISTORIC_BUFFER_START = {2, 10, 65, 68, 67, 79}; - - /** Buffer start pattern for standard TIC frames. */ - public static final byte[] STANDARD_BUFFER_START = {2, 10, 65, 68, 83, 67}; - - /** - * Detects the TIC mode from the given frame buffer. - * - * @param frameBuffer the byte array containing the frame start - * @return the detected {@link TICMode}, or null if not recognized - * @throws TICException if the buffer is too short or invalid - */ - public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) throws TICException { - byte[] frameBufferStart = new byte[HISTORIC_BUFFER_START.length]; - if (frameBuffer.length < frameBufferStart.length) { - throw new TICException( - "Tic frame read 0x" + bytesToHex(frameBuffer) + " too short to determine TIC Mode !", - TICError.TIC_READER_FRAME_DECODE_FAILED); - } - System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); - if (Arrays.equals(frameBufferStart, HISTORIC_BUFFER_START)) { - return HISTORIC; - } else { - if (STANDARD_BUFFER_START.length != HISTORIC_BUFFER_START.length) { - frameBufferStart = new byte[STANDARD_BUFFER_START.length]; - System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); - } - if (Arrays.equals(frameBufferStart, STANDARD_BUFFER_START)) { - return STANDARD; - } - return null; - } - } - - /** - * Detects the TIC mode from the given group buffer. - * - * @param groupBuffer the byte array containing the group data - * @return the detected {@link TICMode}, or null if not recognized - */ - public static TICMode findModeFromGroupBuffer(byte[] groupBuffer) { - for (int i = 0; i < groupBuffer.length; i++) { - if (groupBuffer[i] == HISTORIC_SEPARATOR) { - return HISTORIC; - } else if (groupBuffer[i] == STANDARD_SEPARATOR) { - return STANDARD; - } - } - return null; - } - - /** - * Converts a byte array to a hexadecimal string (replacement for - * DatatypeConverter.printHexBinary). - * - * @param bytes the byte array to convert - * @return the hexadecimal string representation - */ - private static String bytesToHex(byte[] bytes) { - StringBuilder result = new StringBuilder(); - for (byte b : bytes) { - result.append(String.format("%02X", b)); - } - return result.toString(); - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java deleted file mode 100644 index 336f5da..0000000 --- a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.channels; - -import enedis.lab.io.channels.ChannelConfiguration; -import enedis.lab.io.channels.ChannelException; -import enedis.lab.io.channels.serialport.ChannelSerialPort; -import enedis.lab.io.channels.serialport.ChannelSerialPortErrorCode; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.BytesArray; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.time.Time; -import java.util.Arrays; - -/** - * A specialized serial port channel implementation for TIC (Teleinformation Client) communication. - * This channel extends the base serial port functionality to handle TIC protocol-specific frame - * detection, mode auto-detection, and TIC data stream processing. - * - *

    The channel supports both HISTORIC and STANDARD TIC modes, with automatic mode detection - * capabilities. It handles TIC frame parsing with proper start/end pattern recognition and provides - * timeout handling for reliable data reception. - * - *

    Key features: - * - *

      - *
    • Automatic TIC mode detection (HISTORIC vs STANDARD) - *
    • TIC frame parsing with start/end pattern recognition - *
    • Timeout handling for reliable data reception - *
    • Port configuration and reconnection management - *
    - * - * @author Enedis Smarties team - */ -public class ChannelTICSerialPort extends ChannelSerialPort { - /** TIC frame start pattern (STX character) */ - private static final byte START_PATTERN = (byte) 0x02; - - /** TIC frame end pattern (ETX character) */ - private static final byte END_PATTERN = (byte) 0x03; - - /** Polling period in milliseconds for data reception */ - private static final int RECEIVE_DATA_POLLING_PERIOD = 100; - - /** Historic mode buffer start pattern */ - private static final byte[] HISTORIC_BUFFER_START = {2, 10, 65, 68, 67, 79}; - - /** Standard mode buffer start pattern */ - private static final byte[] STANDARD_BUFFER_START = {2, 10, 65, 68, 83, 67}; - - /** Current TIC mode detected during operation */ - private TICMode currentMode; - - /** - * Creates a new TIC serial port channel instance with the specified configuration. - * - * @param configuration the TIC serial port configuration containing port settings and TIC mode - * parameters - * @throws ChannelException if the configuration is invalid or channel creation fails - */ - public ChannelTICSerialPort(ChannelTICSerialPortConfiguration configuration) - throws ChannelException { - super(configuration); - this.currentMode = null; - } - - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException { - if (configuration == null) { - ChannelException.raiseInvalidConfiguration("null"); - } - if (!(configuration instanceof ChannelTICSerialPortConfiguration)) { - ChannelException.raiseInvalidConfigurationType( - configuration, ChannelTICSerialPortConfiguration.class.getSimpleName()); - } - super.setup(configuration); - } - - @Override - public ChannelTICSerialPortConfiguration getConfiguration() { - return (ChannelTICSerialPortConfiguration) this.configuration; - } - - /** - * Gets the current TIC mode being used by the channel. Returns the detected mode if available, - * otherwise returns the selected mode from configuration. - * - * @return the current TIC mode (HISTORIC, STANDARD, or AUTO) - */ - public TICMode getMode() { - return (this.getCurrentMode() != null) ? this.getCurrentMode() : this.getSelectedMode(); - } - - /** - * Gets the TIC mode selected in the channel configuration. - * - * @return the selected TIC mode from configuration - */ - public TICMode getSelectedMode() { - return this.getConfiguration().getTicMode(); - } - - /** - * Gets the currently detected TIC mode during operation. This may differ from the selected mode - * if auto-detection is enabled. - * - * @return the currently detected TIC mode, or null if not yet detected - */ - public TICMode getCurrentMode() { - return this.currentMode; - } - - @Override - public byte[] read() throws ChannelException { - long beginTime = System.currentTimeMillis(); - long elapsedTime = 0; - long timeout = this.getSyncReadTimeout(); - BytesArray buffer = new BytesArray(); - byte[] ticFrame = null; - int startOfFrame = -1, endOfFrame = -1; - - while (elapsedTime < timeout || timeout == 0) { - if (this.available() > 0) { - buffer.addAll(super.read(1)); - if (startOfFrame == -1) { - startOfFrame = buffer.indexOf(START_PATTERN); - if (startOfFrame != -1) { - endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); - if (endOfFrame != -1) { - ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); - break; - } - } - } else { - endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); - if (endOfFrame != -1) { - ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); - break; - } - } - } else { - Time.sleep(RECEIVE_DATA_POLLING_PERIOD); - elapsedTime = (System.currentTimeMillis() - beginTime); - } - } - - return ticFrame; - } - - @Override - protected void receiveData() { - if (this.currentMode == null) { - if (this.autoDetectMode() == null) { - this.onReadTimeout(); - return; - } - } - try { - byte[] ticFrame = this.read(); - - if (ticFrame == null) { - this.onReadTimeout(); - } else { - this.notifyOnDataRead(ticFrame); - } - } catch (ChannelException e) { - this.notifyOnErrorDetected( - ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), - "TIC read failed: " + e.getErrorInfo()); - } - } - - private void onReadTimeout() { - this.notifyOnErrorDetected( - ChannelSerialPortErrorCode.READ_TIMEOUT.getCode(ERROR_CODE_OFFSET), "TIC read timeout"); - this.configurePort(); - this.currentMode = null; - } - - private void configurePort() { - this.closePort(); - this.openPort(); - try { - this.flush(); - } catch (ChannelException e) { - } - } - - private void setSelectedMode(TICMode ticMode) { - try { - this.getConfiguration().setTicMode(ticMode); - } catch (DataDictionaryException e) { - this.logger.error("Cannot set TIC mode " + ticMode, e); - } - } - - private boolean checkAndUpdateMode(TICMode ticMode) { - boolean result = false; - this.setSelectedMode(ticMode); - this.configurePort(); - try { - byte[] ticFrame = this.read(); - if (ticFrame != null && ticFrame.length > HISTORIC_BUFFER_START.length) { - byte[] ticFrameStart = new byte[HISTORIC_BUFFER_START.length]; - System.arraycopy(ticFrame, 0, ticFrameStart, 0, ticFrameStart.length); - if (ticMode == TICMode.HISTORIC) { - if (Arrays.equals(ticFrameStart, HISTORIC_BUFFER_START)) { - result = true; - } - } else if (ticMode == TICMode.STANDARD) { - if (Arrays.equals(ticFrameStart, STANDARD_BUFFER_START)) { - result = true; - } - } - } - } catch (ChannelException e) { - this.notifyOnErrorDetected( - ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), - "TIC read failed: " + e.getErrorInfo()); - } - if (result) { - this.currentMode = ticMode; - } - this.setSelectedMode(TICMode.AUTO); - - return result; - } - - private TICMode autoDetectMode() { - if (this.getSelectedMode() != TICMode.AUTO) { - this.currentMode = this.getSelectedMode(); - return this.getSelectedMode(); - } - this.logger.debug("Auto detecting TIC Mode"); - if (!this.checkAndUpdateMode(TICMode.HISTORIC)) { - if (!this.checkAndUpdateMode(TICMode.STANDARD)) { - this.logger.debug("TIC Mode not detected"); - - return null; - } - this.logger.debug("TIC Mode STANDARD detected"); - - return TICMode.STANDARD; - } - this.logger.debug("TIC Mode HISTORIC detected"); - - return TICMode.HISTORIC; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java deleted file mode 100644 index 064013d..0000000 --- a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.channels; - -import enedis.lab.io.channels.serialport.ChannelSerialPortConfiguration; -import enedis.lab.io.channels.serialport.Parity; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Configuration class for TIC serial port channels. - * - *

    This class extends {@link ChannelSerialPortConfiguration} to provide specific configuration - * for TIC protocol communication over serial ports. It handles TIC mode selection (HISTORIC, - * STANDARD, or AUTO) and automatically configures the appropriate serial port parameters based on - * the selected TIC mode. - * - *

    The class supports both manual TIC mode selection and automatic detection, with different baud - * rates for HISTORIC (1200 bps) and STANDARD (9600 bps) modes. - * - * @author Enedis Smarties team - */ -public class ChannelTICSerialPortConfiguration extends ChannelSerialPortConfiguration { - - protected static final String KEY_TIC_MODE = "ticMode"; - - protected static final Number BAUDRATE_HISTORIC = 1200; - protected static final Number BAUDRATE_STANDARD = 9600; - protected static final Number[] BAUDRATE_ACCEPTED_VALUES = {BAUDRATE_HISTORIC, BAUDRATE_STANDARD}; - protected static final Parity PARITY_ACCEPTED_VALUE = Parity.EVEN; - protected static final Number DATA_BITS_ACCEPTED_VALUE = 7; - protected static final Number STOP_BITS_ACCEPTED_VALUE = 1.0d; - protected static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kTicMode; - - protected ChannelTICSerialPortConfiguration() { - super(); - this.loadKeyDescriptors(); - - this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); - this.kParity.setAcceptedValues(PARITY_ACCEPTED_VALUE); - this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUE); - this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUE); - } - - /** - * Creates a new TIC serial port configuration from a map of parameters. - * - *

    The map should contain configuration parameters that will be copied to this configuration - * object. The TIC mode and serial port parameters will be automatically configured based on the - * provided values. - * - * @param map the map containing configuration parameters - * @throws DataDictionaryException if the map contains invalid parameters - */ - public ChannelTICSerialPortConfiguration(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Creates a new TIC serial port configuration by copying from another data dictionary. - * - *

    This constructor creates a new configuration object by copying all parameters from the - * provided data dictionary. The TIC mode and serial port parameters will be automatically - * configured based on the copied values. - * - * @param other the data dictionary to copy configuration from - * @throws DataDictionaryException if the data dictionary contains invalid parameters - */ - public ChannelTICSerialPortConfiguration(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Creates a new TIC serial port configuration with the specified name and file. - * - *

    This constructor initializes the configuration with default values and associates it with - * the provided name and configuration file. The TIC mode will be set to AUTO by default, allowing - * automatic detection of the appropriate mode. - * - * @param name the configuration name - * @param file the configuration file - */ - public ChannelTICSerialPortConfiguration(String name, File file) { - this(); - this.init(name, file); - } - - /** - * Creates a new TIC serial port configuration with specific parameters. - * - *

    This constructor creates a configuration with the specified name, port name, and TIC mode. - * The serial port parameters (baud rate, parity, data bits, stop bits) will be automatically - * configured based on the selected TIC mode. - * - * @param name the configuration name - * @param portName the serial port name (e.g., "/dev/ttyUSB0" on Linux, "COM1" on Windows) - * @param ticMode the TIC mode (HISTORIC, STANDARD, or AUTO) - * @throws DataDictionaryException if the provided parameters are invalid - */ - public ChannelTICSerialPortConfiguration(String name, String portName, TICMode ticMode) - throws DataDictionaryException { - this(); - - this.setName(name); - this.setPortName(portName); - this.setTicMode(ticMode); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_TIC_MODE)) { - this.setTicMode(TIC_MODE_DEFAULT_VALUE); - } - switch (this.getTicMode()) { - case HISTORIC: - { - this.setBaudrate(BAUDRATE_HISTORIC); - this.setParity(PARITY_ACCEPTED_VALUE); - this.setDataBits(DATA_BITS_ACCEPTED_VALUE); - this.setStopBits(STOP_BITS_ACCEPTED_VALUE); - break; - } - case STANDARD: - case AUTO: - default: - { - this.setBaudrate(BAUDRATE_STANDARD); - this.setParity(PARITY_ACCEPTED_VALUE); - this.setDataBits(DATA_BITS_ACCEPTED_VALUE); - this.setStopBits(STOP_BITS_ACCEPTED_VALUE); - } - } - super.updateOptionalParameters(); - } - - /** - * Gets the current TIC mode configuration. - * - *

    Returns the TIC mode that has been set for this configuration. The mode determines the - * communication protocol and serial port parameters used for TIC communication. - * - * @return the current TIC mode (HISTORIC, STANDARD, or AUTO) - */ - public TICMode getTicMode() { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Sets the TIC mode for this configuration. - * - *

    When the TIC mode is set, the serial port parameters are automatically updated to match the - * requirements of the selected mode: - * - *

      - *
    • HISTORIC: 1200 bps, 7 data bits, even parity, 1 stop bit - *
    • STANDARD: 9600 bps, 7 data bits, even parity, 1 stop bit - *
    • AUTO: Automatically detects the appropriate mode - *
    - * - * @param ticMode the TIC mode to set - * @throws DataDictionaryException if the TIC mode is invalid - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException { - this.setTicMode((Object) ticMode); - } - - /** - * Sets the TIC mode using an object value and automatically configures the baud rate based on the - * selected mode. - * - *

    This protected method is used internally to set the TIC mode from various object types and - * automatically adjusts the baud rate: - * - *

      - *
    • HISTORIC mode: sets baud rate to 1200 bps - *
    • STANDARD or AUTO mode: sets baud rate to 9600 bps - *
    - * - * @param ticMode the TIC mode object to set - * @throws DataDictionaryException if the TIC mode object is invalid - */ - protected void setTicMode(Object ticMode) throws DataDictionaryException { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - switch (this.getTicMode()) { - case HISTORIC: - this.setBaudrate(BAUDRATE_HISTORIC); - break; - case STANDARD: - case AUTO: - this.setBaudrate(BAUDRATE_STANDARD); - break; - default: - break; - } - } - - /** - * Loads and initializes the key descriptors for TIC mode configuration. - * - *

    This private method sets up the key descriptor for the TIC mode parameter, which is used for - * validation and conversion of TIC mode values. It creates a key descriptor for the TICMode enum - * and adds it to the list of keys managed by this configuration object. - * - *

    If an error occurs during initialization, it wraps the DataDictionaryException in a - * RuntimeException to maintain the constructor's contract. - */ - private void loadKeyDescriptors() { - try { - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); - this.keys.add(this.kTicMode); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java deleted file mode 100644 index 8b45d44..0000000 --- a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java +++ /dev/null @@ -1,1201 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -import enedis.lab.io.channels.ChannelConfiguration; -import enedis.lab.io.channels.ChannelException; -import enedis.lab.io.channels.ChannelPhysical; -import enedis.lab.io.channels.ChannelStatus; -import enedis.lab.io.serialport.SerialPortDescriptor; -import enedis.lab.io.serialport.SerialPortFinderBase; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.SystemError; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import jssc.SerialPort; -import jssc.SerialPortEvent; -import jssc.SerialPortEventListener; -import jssc.SerialPortException; -import jssc.SerialPortTimeoutException; -import org.apache.commons.lang3.SystemUtils; - -/** - * Serial port communication channel implementation for TIC protocol. - * - *

    This class extends {@link ChannelPhysical} and implements {@link SerialPortEventListener} to - * provide a robust serial port communication channel. It handles automatic port discovery, - * configuration, connection management, and real-time data transmission for TIC-compliant devices. - * - *

    Key features include: - * - *

      - *
    • Automatic port detection and reconnection when devices are plugged/unplugged - *
    • Support for both port ID and port name identification methods - *
    • Configurable serial communication parameters (baud rate, parity, data bits, stop bits) - *
    • Event-driven data reception with optional timeout handling - *
    • Symbolic link resolution for Linux systems - *
    • Error detection and automatic recovery mechanisms - *
    - * - *

    The channel operates in a periodic polling mode to monitor port availability and automatically - * handles port changes, busy conditions, and communication errors. It supports both synchronous and - * asynchronous data operations with proper exception handling. - * - * @author Enedis Smarties team - * @see ChannelPhysical - * @see ChannelSerialPortConfiguration - * @see SerialPortEventListener - * @see ChannelStatus - * @see ChannelSerialPortErrorCode - */ -public class ChannelSerialPort extends ChannelPhysical implements SerialPortEventListener { - /** Error code offset for serial port specific errors. */ - protected static final int ERROR_CODE_OFFSET = 1000; - - /** Default polling delay in milliseconds for port monitoring. */ - private static final int DEFAULT_DELAY = 500; - - /** Extended delay in milliseconds for port reopening attempts. */ - private static final int DELAY_REOPEN = DEFAULT_DELAY * 10; - - /** - * Checks if a serial port is available on the system. - * - *

    This method queries the system to determine if a port with the specified identifier or name - * is currently available. The port ID takes precedence over port name if both are provided. - * - * @param portId the unique port identifier (may be null) - * @param portName the port name or path (may be null) - * @return true if the port is found and available, false otherwise - * @see SerialPortFinderBase#findByPortIdOrPortName(String, String) - */ - public static boolean isPortFound(String portId, String portName) { - return SerialPortFinderBase.getInstance().findByPortIdOrPortName(portId, portName) != null; - } - - /** - * Finds the actual port name corresponding to the given port ID or port name. - * - *

    This method resolves the port identifier to its actual system port name. The port ID - * parameter takes priority over the port name parameter if both are provided. This is useful for - * converting abstract port identifiers to concrete system paths. - * - * @param portId the unique port identifier (has priority if not null) - * @param portName the port name or path (used as fallback if portId is null) - * @return the resolved port name, or null if no matching port is found - * @see SerialPortFinderBase#findByPortId(String) - * @see SerialPortFinderBase#findByPortName(String) - */ - public static String findPortName(String portId, String portName) { - SerialPortDescriptor descriptor = null; - if (portId != null) { - descriptor = SerialPortFinderBase.getInstance().findByPortId(portId); - } else { - descriptor = SerialPortFinderBase.getInstance().findByPortName(portName); - } - - return (descriptor != null) ? descriptor.getPortName() : null; - } - - /** - * Converts a {@link Parity} enumeration value to the corresponding JSSC library constant. - * - *

    This utility method maps the application-level parity enumeration to the underlying serial - * communication library constants required for port configuration. - * - * @param parity the parity setting from the configuration - * @return the corresponding JSSC SerialPort parity constant, or -1 for unsupported values - * @see SerialPort#PARITY_EVEN - * @see SerialPort#PARITY_ODD - * @see SerialPort#PARITY_NONE - * @see SerialPort#PARITY_MARK - * @see SerialPort#PARITY_SPACE - */ - public static int parityFromConfiguration(Parity parity) { - int retVal = -1; - - switch (parity) { - case EVEN: - retVal = SerialPort.PARITY_EVEN; - break; - case MARK: - retVal = SerialPort.PARITY_MARK; - break; - case NONE: - retVal = SerialPort.PARITY_NONE; - break; - case ODD: - retVal = SerialPort.PARITY_ODD; - break; - case SPACE: - retVal = SerialPort.PARITY_SPACE; - break; - default: /* Cas non atteignable */ - } - - return retVal; - } - - /** - * Converts a stop bits configuration value to the corresponding JSSC library constant. - * - *

    This utility method maps the configured stop bits value (1.0, 1.5, or 2.0) to the underlying - * serial communication library constants required for port configuration. - * - * @param stop_bits the stop bits value from the configuration (1.0, 1.5, or 2.0) - * @return the corresponding JSSC SerialPort stop bits constant, or -1 for unsupported values - * @see SerialPort#STOPBITS_1 - * @see SerialPort#STOPBITS_1_5 - * @see SerialPort#STOPBITS_2 - */ - public static int stopBitsFromConfiguration(float stop_bits) { - int retVal = -1; - - if (1 == stop_bits) { - retVal = SerialPort.STOPBITS_1; - } else if (1.5 == stop_bits) { - retVal = SerialPort.STOPBITS_1_5; - } else if (2 == stop_bits) { - retVal = SerialPort.STOPBITS_2; - } - - return retVal; - } - - /** JSSC SerialPort instance for handling low-level serial communication. */ - private SerialPort portHandler = null; - - /** Previously detected port name, used for change detection. */ - private String previousPortName; - - /** Currently detected port name, used for change detection. */ - private String currentPortName; - - /** - * Constructs a new serial port channel with the specified configuration. - * - *

    This constructor initializes the channel with the provided configuration and sets up the - * initial port detection. The channel will automatically resolve the port name based on the - * configuration's port ID or port name settings. - * - *

    The constructor sets up the periodic polling mechanism with a default delay and initializes - * the port change detection system. - * - * @param configuration the channel configuration containing port settings and parameters - * @throws ChannelException if the configuration is invalid or port cannot be resolved - * @throws IllegalArgumentException if configuration is null or not a - * ChannelSerialPortConfiguration - * @see ChannelSerialPortConfiguration - */ - public ChannelSerialPort(ChannelConfiguration configuration) throws ChannelException { - super(configuration); - this.setPeriod(DEFAULT_DELAY); - - this.currentPortName = this.findPortName(); - this.previousPortName = this.currentPortName; - } - - /** - * Starts the serial port channel and begins communication. - * - *

    This method initiates the channel by opening the serial port connection and starting the - * underlying periodic task. The port is opened with the configuration parameters specified during - * construction. - * - *

    If the port cannot be opened, the channel will enter an error state and attempt to reconnect - * periodically. - * - * @see #stop() - * @see #openPort() - */ - @Override - public void start() { - this.logger.info("Channel " + this.getName() + " start (" + this.findPortName() + ")"); - this.openPort(); - super.start(); - } - - /** - * Stops the serial port channel and closes the connection. - * - *

    This method gracefully stops the channel by first stopping the underlying periodic task and - * then closing the serial port connection. All event listeners are removed and the port is - * properly released. - * - *

    The channel can be restarted by calling {@link #start()} again. - * - * @see #start() - * @see #closePort() - */ - @Override - public void stop() { - this.logger.info("Channel " + this.getName() + " stop (" + this.findPortName() + ")"); - super.stop(); - this.closePort(); - } - - /** - * Configures the channel with a new configuration. - * - *

    This method validates and applies a new configuration to the channel. The configuration must - * be a valid {@link ChannelSerialPortConfiguration} instance. If the channel is currently - * started, it will be stopped and restarted with the new configuration. - * - *

    The configuration update includes port parameter changes, which may require reopening the - * serial port connection. - * - * @param configuration the new channel configuration to apply - * @throws ChannelException if the configuration is null, invalid, or not a serial port - * configuration - * @see ChannelSerialPortConfiguration - */ - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException { - if (configuration == null) { - ChannelException.raiseInvalidConfiguration("null"); - } - if (!(configuration instanceof ChannelSerialPortConfiguration)) { - ChannelException.raiseInvalidConfigurationType( - configuration, ChannelSerialPortConfiguration.class.getSimpleName()); - } - super.setup(configuration); - } - - /** - * Reads available data from the serial port. - * - *

    This method performs a non-blocking read operation to retrieve all available data from the - * serial port input buffer. The method returns immediately with whatever data is currently - * available, or an empty array if no data is present. - * - *

    The channel must be in a STARTED state and the port must be open for this operation to - * succeed. - * - * @return array of bytes read from the port, or empty array if no data available - * @throws ChannelException if the channel is not ready, port is not open, or read operation fails - * @see #read(int) - * @see #read(int, int) - */ - @Override - public byte[] read() throws ChannelException { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try { - buffer = this.portHandler.readBytes(); - } catch (SerialPortException exception) { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - /** - * Writes data to the serial port. - * - *

    This method transmits the provided data bytes to the serial port. The operation is - * synchronous and will block until all data has been written to the port buffer. - * - *

    The channel must be in a STARTED state and the port must be open for this operation to - * succeed. - * - * @param data the byte array to write to the port (must not be null) - * @throws ChannelException if the channel is not ready, port is not open, or write operation - * fails - * @throws IllegalArgumentException if data parameter is null - */ - @Override - public void write(byte[] data) throws ChannelException { - if (this.portHandler == null || !this.portHandler.isOpened()) { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try { - this.portHandler.writeBytes(data); - } catch (SerialPortException exception) { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot write bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - } - - /** - * Returns a copy of the current channel configuration. - * - *

    This method returns a defensive copy of the channel's configuration to prevent external - * modification of the internal state. The returned configuration contains all current settings - * including port identification, communication parameters, and timeout values. - * - * @return a copy of the current channel configuration - * @see ChannelSerialPortConfiguration - */ - @Override - public ChannelSerialPortConfiguration getConfiguration() { - return (ChannelSerialPortConfiguration) this.configuration.clone(); - } - - /** - * Sets up the channel with updated configuration. - * - *

    This protected method handles the internal setup process when the channel configuration is - * updated. It saves the current status, stops the channel, updates the real path for symbolic - * links on Linux systems, creates a new port handler if needed, and restores the previous state. - * - *

    This method ensures that configuration changes are applied atomically and that the channel - * state is properly maintained throughout the update process. - * - * @throws ChannelException if the setup process fails or configuration is invalid - * @see #updateRealPath() - */ - @Override - protected void setup() throws ChannelException { - /* 1. Save current state */ - ChannelStatus currentStatus = this.status; - /* 2. Stop serial port */ - this.stop(); - /* 3. update path if symbolic link */ - this.updateRealPath(); - /* 4. Create serial port handler */ - if ((null == this.portHandler) - || (false == this.getPortName().equals(this.portHandler.getPortName()))) { - this.portHandler = new SerialPort(this.getPortName()); - } - /* 5. Restore current state */ - if (ChannelStatus.STARTED == currentStatus) { - this.start(); - } - } - - /** - * Handles serial port events from the JSSC library. - * - *

    This method is called by the JSSC library when serial port events occur. Currently, it only - * handles RXCHAR events (data received) by triggering the data reception process. Other event - * types are ignored as they are not relevant for TIC communication. - * - *

    The event handling is designed to be lightweight and delegates the actual data processing to - * the {@link #receiveData()} method. - * - * @param event the serial port event that occurred - * @see SerialPortEvent#RXCHAR - * @see #receiveData() - */ - @Override - public void serialEvent(SerialPortEvent event) { - switch (event.getEventType()) { - case SerialPortEvent.RXCHAR: - this.receiveData(); - break; - default: /* No action for other event types */ - } - } - - /** - * Main processing loop for the serial port channel. - * - *

    This method is called periodically to monitor the port status and handle various scenarios - * including port detection, connection management, and data reception. - * - *

    When the channel is STARTED, it: - * - *

      - *
    • Checks if the port is still available and handles port not found scenarios - *
    • Detects port name changes (e.g., due to USB device reconnection) - *
    • Receives data if no event listener is configured (polling mode) - *
    - * - *

    When the channel is not STARTED, it attempts to open the port if it becomes available, with - * extended delay on error conditions. - * - *

    The processing delay is dynamically adjusted based on the current state and error conditions - * to optimize performance and responsiveness. - * - * @see #onPortNotFound() - * @see #onPortNameChanged() - * @see #receiveData() - * @see #openPort() - */ - @Override - protected void process() { - int delay = DEFAULT_DELAY; - - if (this.status == ChannelStatus.STARTED) { - if (!this.isPortFound()) { - this.onPortNotFound(); - } else if (this.hasPortChanged()) { - this.onPortNameChanged(); - } else if (!this.hasEventListener()) { - this.receiveData(); - } - } else { - if (this.isPortFound()) { - this.openPort(); - if (this.status == ChannelStatus.ERROR) { - delay = DELAY_REOPEN; - } - } - } - this.setPeriod(delay); - } - - /** - * Performs a blocking read operation for a specific number of bytes. - * - *

    This method reads exactly the specified number of bytes from the serial port. The operation - * will block until the requested number of bytes is available or until the operation times out. - * - *

    The channel must be in a STARTED state and the port must be open for this operation to - * succeed. - * - * @param bytesCount the exact number of bytes to read - * @return array containing the read bytes, or null if the read operation fails - * @throws ChannelException if the channel is not ready, port is not open, or read operation fails - * @throws IllegalArgumentException if bytesCount is negative or zero - * @see #read() - * @see #read(int, int) - */ - public byte[] read(int bytesCount) throws ChannelException { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try { - buffer = this.portHandler.readBytes(bytesCount); - } catch (SerialPortException exception) { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - /** - * Performs a blocking read operation with a specified timeout. - * - *

    This method reads exactly the specified number of bytes from the serial port within the - * given timeout period. If the timeout expires before the requested number of bytes is available, - * a timeout exception is thrown. - * - *

    The channel must be in a STARTED state and the port must be open for this operation to - * succeed. - * - * @param bytesCount the exact number of bytes to read - * @param timeout the timeout in milliseconds for the read operation - * @return array containing the read bytes, or null if the read operation fails - * @throws ChannelException if the channel is not ready, port is not open, read operation fails, - * or timeout occurs - * @throws IllegalArgumentException if bytesCount is negative or zero, or timeout is negative - * @see #read() - * @see #read(int) - */ - public byte[] read(int bytesCount, int timeout) throws ChannelException { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try { - buffer = this.portHandler.readBytes(bytesCount, timeout); - } catch (SerialPortException exception) { - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } catch (SerialPortTimeoutException exception) { - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - /** - * Returns the number of bytes available for reading from the serial port. - * - *

    This method queries the serial port input buffer to determine how many bytes are currently - * available for reading. This information is useful for determining whether data is available - * before performing a read operation. - * - *

    The channel must be in a STARTED state and the port must be open for this operation to - * succeed. - * - * @return the number of bytes available in the input buffer - * @throws ChannelException if the channel is not ready, port is not open, or the operation fails - * @see #read() - */ - public int available() throws ChannelException { - if (this.portHandler == null || !this.portHandler.isOpened()) { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try { - return this.portHandler.getInputBufferBytesCount(); - } catch (SerialPortException exception) { - this.logger.error("Cannot get bytes available", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return 0; - } - - /** - * Flushes all input and output buffers of the serial port. - * - *

    This method clears both the input and output buffers of the serial port, discarding any - * pending data. This is useful for ensuring a clean communication state, particularly after - * errors or before starting a new communication session. - * - *

    The flush operation includes clearing receive buffers, transmit buffers, and aborting any - * pending operations. - * - *

    The channel must be in a STARTED state and the port must be open for this operation to - * succeed. - * - * @throws ChannelException if the channel is not ready, port is not open, or flush operation - * fails - * @see SerialPort#PURGE_RXCLEAR - * @see SerialPort#PURGE_TXCLEAR - */ - public void flush() throws ChannelException { - if (this.portHandler == null || !this.portHandler.isOpened()) { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try { - int mask = - SerialPort.PURGE_RXCLEAR - | SerialPort.PURGE_RXABORT - | SerialPort.PURGE_TXCLEAR - | SerialPort.PURGE_TXABORT; - if (!this.portHandler.purgePort(mask)) { - this.logger.error( - "Channel " - + this.getName() - + " failed to purge port " - + this.getPortName() - + " (" - + SystemError.getMessage() - + ") "); - } - } catch (SerialPortException exception) { - this.logger.error("Cannot purge port", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - } - - /** - * Returns the port identifier from the channel configuration. - * - *

    The port ID is an alternative way to identify serial ports, typically used when the system - * provides unique identifiers for ports. This identifier takes precedence over the port name when - * both are specified. - * - * @return the port identifier, or null if not set in the configuration - * @see ChannelSerialPortConfiguration#getPortId() - * @see #getPortName() - */ - public String getPortId() { - return this.getChannelConfiguration().getPortId(); - } - - /** - * Returns the port name from the channel configuration. - * - *

    The port name is the system path or name used to identify the serial port. This is typically - * a device path like "/dev/ttyUSB0" on Linux or "COM1" on Windows. - * - * @return the port name, or null if not set in the configuration - * @see ChannelSerialPortConfiguration#getPortName() - * @see #getPortId() - */ - public String getPortName() { - return this.getChannelConfiguration().getPortName(); - } - - /** - * Returns the name of the currently opened serial port. - * - *

    This method returns the actual port name that is currently opened by the JSSC library. This - * may differ from the configured port name if symbolic links were resolved or if the port was - * dynamically assigned. - * - * @return the name of the opened port, or null if no port is currently open - * @see #getPortName() - * @see #getPortId() - */ - public String getPortNameOpened() { - return (this.portHandler != null) ? this.portHandler.getPortName() : null; - } - - /** - * Finds the port name associated with the current channel configuration. - * - *

    This method resolves the port name based on the configuration's port ID or port name - * settings. It uses the same resolution logic as the static {@link #findPortName(String, String)} - * method but operates on the current configuration. - * - * @return the resolved port name, or null if no matching port is found - * @see #findPortName(String, String) - * @see #getPortId() - * @see #getPortName() - */ - public String findPortName() { - return findPortName(this.getPortId(), this.getPortName()); - } - - /** - * Returns the current baud rate configured for the serial port. - * - *

    The baud rate determines the speed of serial communication in bits per second. - * - * @return the configured baud rate value - * @see ChannelSerialPortConfiguration#getBaudrate() - */ - public int getBaudrate() { - return this.getChannelConfiguration().getBaudrate().intValue(); - } - - /** - * Returns the number of data bits configured for serial communication. - * - *

    Data bits define the number of bits used to represent each character transmitted. Standard - * values are 7 (for ASCII) or 8 (for binary data). TIC protocol typically uses 7 data bits. - * - * @return the number of data bits (5, 6, 7, or 8) - * @see ChannelSerialPortConfiguration#getDataBits() - */ - public int getDataBits() { - return this.getChannelConfiguration().getDataBits().intValue(); - } - - /** - * Returns the number of stop bits configured for serial communication. - * - *

    Stop bits mark the end of each data frame. Standard values are 1.0, 1.5, or 2.0. Most serial - * communications use 1.0 stop bits. - * - * @return the number of stop bits (1.0, 1.5, or 2.0) - * @see ChannelSerialPortConfiguration#getStopBits() - */ - public float getStopBits() { - return this.getChannelConfiguration().getStopBits().floatValue(); - } - - /** - * Returns the parity setting configured for serial communication. - * - *

    Parity is used for error detection by adding an extra bit to each data frame. TIC protocol - * typically uses EVEN parity for reliable data transmission. - * - * @return the parity setting from the configuration - * @see Parity - * @see ChannelSerialPortConfiguration#getParity() - */ - public Parity getParity() { - return this.getChannelConfiguration().getParity(); - } - - /** - * Returns the timeout value for synchronous read operations. - * - *

    This timeout determines how long a synchronous read operation will wait for data before - * timing out. The value is specified in milliseconds. - * - * @return the timeout value in milliseconds for synchronous read operations - * @see ChannelSerialPortConfiguration#getSyncReadTimeout() - * @see #read(int, int) - */ - public int getSyncReadTimeout() { - return this.getChannelConfiguration().getSyncReadTimeout().intValue(); - } - - /** - * Determines if the channel has an event listener configured. - * - *

    This method indicates whether the channel is configured to use event-driven data reception - * (via SerialPortEventListener) or polling-based reception. - * - *

    The base implementation returns false, indicating polling mode. Subclasses can override this - * method to enable event-driven mode. - * - * @return true if event listener is configured, false for polling mode - * @see #addEventListener() - * @see #removeEventListener() - */ - protected boolean hasEventListener() { - return false; - } - - /** - * Adds a serial port event listener for asynchronous data reception. - * - *

    This method configures the serial port to generate events when data is received, control - * signals change, or other port events occur. It sets up the event mask to listen for RXCHAR - * (data received), CTS (clear to send), and DSR (data set ready) events. - * - *

    The channel instance is registered as the event listener, so {@link - * #serialEvent(SerialPortEvent)} will be called when events occur. - * - * @return true if the event listener was successfully added, false otherwise - * @see #removeEventListener() - * @see #serialEvent(SerialPortEvent) - * @see SerialPort#MASK_RXCHAR - */ - protected boolean addEventListener() { - if (this.portHandler == null) { - return false; - } - int mask = SerialPort.MASK_RXCHAR + SerialPort.MASK_CTS + SerialPort.MASK_DSR; - try { - if (!this.portHandler.setEventsMask(mask)) { - this.logger.error( - "Channel " - + this.getName() - + " failed to set event mask on port " - + this.getPortName() - + " (" - + SystemError.getMessage() - + ") "); - return false; - } - } catch (SerialPortException exception) { - this.logger.error( - "Channel " - + this.getName() - + " failed to set event mask on port " - + this.getPortName() - + " : " - + exception.getMessage()); - return false; - } - try { - this.portHandler.addEventListener(this); - } catch (SerialPortException e) { - this.logger.error( - "Channel " + this.getName() + " failed to add listener :" + e.getMessage(), e); - return false; - } - - return true; - } - - /** - * Removes the serial port event listener. - * - *

    This method unregisters the channel instance as an event listener from the serial port. - * After calling this method, the channel will no longer receive asynchronous events and will - * operate in polling mode. - * - *

    This method is typically called when stopping the channel or switching to polling-based data - * reception. - * - * @return true if the event listener was successfully removed, false otherwise - * @see #addEventListener() - * @see #hasEventListener() - */ - protected boolean removeEventListener() { - if (this.portHandler == null) { - return false; - } - try { - if (!this.portHandler.removeEventListener()) { - this.logger.error("Channel " + this.getName() + " failed to remove listener"); - return false; - } - } catch (SerialPortException e) { - this.logger.error( - "Channel " + this.getName() + " failed to remove listener :" + e.getMessage(), e); - return false; - } - - return true; - } - - /** - * Receives and processes data from the serial port. - * - *

    This method checks for available data in the serial port input buffer and reads it if - * present. The received data is then passed to registered listeners via the notification system. - * - *

    If no data is available, the method returns immediately without error. If a read error - * occurs, it notifies listeners with an appropriate error code. - * - *

    This method is called both from the periodic processing loop (polling mode) and from the - * serial event handler (event-driven mode). - * - * @see #read() - * @see #available() - * @see #notifyOnDataRead(byte[]) - * @see ChannelSerialPortErrorCode#READ_FAILED - */ - protected void receiveData() { - try { - if (this.available() > 0) { - byte[] buffer = this.read(); - this.notifyOnDataRead(buffer); - } - } catch (ChannelException exception) { - this.notifyOnErrorDetected( - ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), - exception.getMessage()); - } - } - - /** - * Checks if the port name has changed since the last check. - * - *

    This method compares the currently detected port name with the previously detected port name - * to determine if the port has changed. This typically occurs when a USB device is unplugged and - * plugged back in, causing the system to assign a new device path. - * - *

    The comparison is case-insensitive to handle variations in how the system reports port - * names. - * - * @return true if the port name has changed, false otherwise - * @see #onPortNameChanged() - */ - private boolean hasPortChanged() { - boolean portChanged = false; - - this.currentPortName = this.findPortName(); - - if (this.currentPortName != null - && !this.currentPortName.equalsIgnoreCase(this.previousPortName)) { - portChanged = true; - } - - return portChanged; - } - - /** - * Handles the event when the port name has changed. - * - *

    This method is called when a port name change is detected. It logs the change, sets the - * channel status to ERROR, forcibly closes the current port connection, notifies listeners of the - * error, and updates the previous port name for future comparisons. - * - *

    Port name changes typically occur when USB devices are reconnected and the system assigns a - * new device path (e.g., /dev/ttyUSB0 → /dev/ttyUSB1). - * - * @see #hasPortChanged() - * @see ChannelSerialPortErrorCode#PORT_NAME_HAS_CHANGED - */ - private void onPortNameChanged() { - String errorMessage = - "Port name has changed ( " + this.previousPortName + " --> " + this.currentPortName + " )"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected( - ChannelSerialPortErrorCode.PORT_NAME_HAS_CHANGED.getCode(ERROR_CODE_OFFSET), errorMessage); - this.previousPortName = this.currentPortName; - } - - /** - * Updates the port name to its real path by resolving symbolic links. - * - *

    This method is only executed on Linux systems to resolve symbolic links to their actual - * device paths. This is important for USB devices that may be represented by symbolic links in - * /dev/disk/by-id/ or similar locations. - * - *

    The method uses the system 'realpath' command to resolve the symbolic link and updates the - * channel configuration with the resolved path. - * - * @throws ChannelException if the realpath resolution fails or configuration update fails - * @see SystemUtils#IS_OS_LINUX - */ - private void updateRealPath() throws ChannelException { - if (SystemUtils.IS_OS_LINUX) { - String realPortName; - try { - Process p = Runtime.getRuntime().exec("realpath " + this.getPortName()); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream())); - realPortName = stdInput.readLine(); - this.getChannelConfiguration().setPortName(realPortName); - } catch (IOException | DataDictionaryException e) { - throw new ChannelException( - ChannelException.ERRCODE_INVALID_CONFIGURATION, - "Cannot resolve realpath (" + e.getMessage() + ")"); - } - } - } - - /** - * Returns the channel configuration cast to the specific serial port configuration type. - * - *

    This helper method provides type-safe access to the serial port specific configuration - * without requiring explicit casting throughout the code. - * - * @return the channel configuration as a ChannelSerialPortConfiguration instance - */ - private ChannelSerialPortConfiguration getChannelConfiguration() { - return (ChannelSerialPortConfiguration) this.configuration; - } - - /** - * Opens and configures the serial port connection. - * - *

    This method performs the complete process of opening a serial port connection: - * - *

      - *
    1. Checks if the port is already open to avoid duplicate operations - *
    2. Creates a new SerialPort handler if needed - *
    3. Opens the port using the resolved port name - *
    4. Configures the port with the specified communication parameters - *
    5. Adds event listener if configured for event-driven mode - *
    6. Sets the channel status to STARTED - *
    - * - *

    If any step fails, the method logs the error, handles specific error conditions (like - * PORT_BUSY), and ensures the channel enters an appropriate error state. - * - * @see #closePort() - * @see #findPortName() - * @see #hasEventListener() - * @see #addEventListener() - * @see ChannelSerialPortErrorCode#PORT_BUSY - */ - protected void openPort() { - /* 1. Check if serial port is already opened */ - if (this.portHandler != null && this.portHandler.isOpened()) { - return; - } - /* 2. Open serial port */ - String portNameFound = this.findPortName(); - try { - this.logger.info("Channel " + this.getName() + " opening port " + portNameFound); - if (this.portHandler == null) { - this.portHandler = new SerialPort(portNameFound); - } - if (!this.portHandler.openPort()) { - this.logger.error( - "Channel " - + this.getName() - + " failed to open port " - + this.getPortNameOpened() - + " (" - + SystemError.getMessage() - + ") "); - return; - } - this.logger.info("Channel " + this.getName() + " configuring port " + portNameFound); - if (!this.portHandler.setParams( - this.getBaudrate(), - this.getDataBits(), - stopBitsFromConfiguration(this.getStopBits()), - parityFromConfiguration(this.getParity()))) { - this.logger.error( - "Channel " - + this.getName() - + " failed to configure port " - + this.getPortNameOpened() - + " (" - + SystemError.getMessage() - + ") "); - return; - } - } catch (SerialPortException exception) { - String errorMessage = - "Channel " - + this.getName() - + " failed to open port " - + this.getPortNameOpened() - + " : " - + exception.getMessage(); - this.logger.error(errorMessage); - if (exception.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) { - this.notifyOnErrorDetected( - ChannelSerialPortErrorCode.PORT_BUSY.getCode(ERROR_CODE_OFFSET), errorMessage); - } - this.closePortForced(); - return; - } - /* 3. Add serial event listener */ - if (this.hasEventListener()) { - if (!this.addEventListener()) { - return; - } - } - this.setStatus(ChannelStatus.STARTED); - } - - /** - * Closes the serial port connection gracefully. - * - *

    This method performs the complete process of closing a serial port connection: - * - *

      - *
    1. Checks if the port is already closed to avoid duplicate operations - *
    2. Removes event listener if one was configured - *
    3. Closes the port and releases system resources - *
    4. Sets the channel status to STOPPED - *
    - * - *

    If any step fails, the method logs the error but continues with the remaining cleanup steps - * to ensure the channel is in a consistent state. - * - * @see #openPort() - * @see #closePortForced() - * @see #hasEventListener() - * @see #removeEventListener() - */ - protected void closePort() { - /* 1. Check if serial port is already closed */ - if (this.portHandler == null || !this.portHandler.isOpened()) { - return; - } - /* 2. Remove serial event listener */ - if (this.hasEventListener()) { - this.removeEventListener(); - } - /* 3. Close serial port */ - try { - this.logger.info("Channel " + this.getName() + " closing port " + this.getPortNameOpened()); - if (!this.portHandler.closePort()) { - this.logger.error( - "Channel " - + this.getName() - + " failed to close port " - + this.getPortNameOpened() - + " (" - + SystemError.getMessage() - + ") "); - return; - } - } catch (SerialPortException exception) { - this.logger.error( - "Channel " - + this.getName() - + " failed to close port " - + this.getPortNameOpened() - + " : " - + exception.getMessage()); - return; - } - this.setStatus(ChannelStatus.STOPPED); - } - - /** - * Forcefully closes the serial port and resets the port handler. - * - *

    This method performs an emergency closure of the serial port connection without the normal - * cleanup procedures. It attempts to close the port and then creates a new SerialPort handler - * instance for future use. - * - *

    This method is typically used in error conditions where the normal close procedure may fail - * or when the port needs to be forcibly reset. - * - *

    Unlike {@link #closePort()}, this method does not remove event listeners or update the - * channel status, making it suitable for error recovery scenarios. - * - * @see #closePort() - * @see #findPortName() - */ - protected void closePortForced() { - try { - this.portHandler.closePort(); - } catch (SerialPortException exception) { - this.logger.error( - "Channel " - + this.getName() - + " fail on close : close " - + this.getPortNameOpened() - + " failed due to " - + exception.getMessage()); - } - this.portHandler = new SerialPort(this.findPortName()); - } - - /** - * Checks if the configured port is currently available on the system. - * - *

    This method uses the current channel configuration to determine if the specified port is - * available. It delegates to the static {@link #isPortFound(String, String)} method with the - * current port ID and port name. - * - * @return true if the configured port is found and available, false otherwise - * @see #isPortFound(String, String) - * @see #getPortId() - * @see #getPortName() - */ - protected boolean isPortFound() { - return isPortFound(this.getPortId(), this.getPortName()); - } - - /** - * Checks if the currently opened port matches the expected port. - * - *

    This method compares the name of the currently opened port with the port name that should be - * opened according to the current configuration. This is useful for detecting situations where - * the port has changed or become unavailable. - * - * @return true if the opened port matches the expected port, false otherwise - * @see #getPortNameOpened() - * @see #findPortName() - */ - protected boolean isPortOpenedFound() { - String portNameOpened = this.getPortNameOpened(); - if (portNameOpened == null) { - return false; - } - String portNameFound = this.findPortName(); - - return portNameOpened.equals(portNameFound); - } - - /** - * Handles the event when the configured port is not found. - * - *

    This method is called when the system cannot locate the configured serial port. It logs the - * error, sets the channel status to ERROR, forcibly closes any existing connection, and notifies - * listeners of the error condition. - * - *

    This typically occurs when a USB device is unplugged or the device driver is not properly - * installed. - * - * @see #isPortFound() - * @see ChannelSerialPortErrorCode#PORT_NOT_FOUND - */ - protected void onPortNotFound() { - String errorMessage = - "Channel " + this.getName() + " : serial port " + this.findPortName() + " not found"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected( - ChannelSerialPortErrorCode.PORT_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); - } - - /** - * Handles the event when the currently opened port is no longer found. - * - *

    This method is called when the system can no longer locate the port that was previously - * opened by the channel. This indicates that the device has been physically disconnected or the - * system has lost access to it. - * - *

    It logs the error, sets the channel status to ERROR, forcibly closes the connection, and - * notifies listeners of the error condition. - * - * @see #isPortOpenedFound() - * @see ChannelSerialPortErrorCode#PORT_OPENED_NOT_FOUND - */ - protected void onPortOpenedNotFound() { - String errorMessage = - "Channel " - + this.getName() - + " : serial port " - + this.getPortNameOpened() - + " opened not found"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected( - ChannelSerialPortErrorCode.PORT_OPENED_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java deleted file mode 100644 index 20adc5c..0000000 --- a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -/** - * Enumeration of error codes for serial port channel operations. - * - *

    This enum defines specific error codes that can occur during serial port communication - * operations. Each error code is associated with a unique numeric identifier that can be used for - * error reporting, logging, and debugging purposes. - * - *

    The error codes are designed to be used with an offset mechanism, allowing different - * components or modules to have their own error code ranges while maintaining uniqueness across the - * entire system. - * - * @author Enedis Smarties team - */ -public enum ChannelSerialPortErrorCode { - /** - * Error code indicating that the specified serial port was not found on the system. This - * typically occurs when the port identifier or name does not correspond to any available serial - * port. - */ - PORT_NOT_FOUND(1), - - /** - * Error code indicating that a previously opened serial port is no longer available. This can - * happen when the port is disconnected or becomes unavailable after being opened. - */ - PORT_OPENED_NOT_FOUND(2), - - /** - * Error code indicating that the serial port name has changed during operation. This typically - * occurs when the port is reconnected and gets assigned a different name. - */ - PORT_NAME_HAS_CHANGED(3), - - /** - * Error code indicating that the serial port is currently busy and cannot be accessed. This - * typically occurs when another application or process is already using the port. - */ - PORT_BUSY(4), - - /** - * Error code indicating that a read operation on the serial port has failed. This can be due to - * various reasons such as hardware issues, communication errors, or port configuration problems. - */ - READ_FAILED(5), - - /** - * Error code indicating that a read operation on the serial port has timed out. This occurs when - * no data is received within the specified timeout period. - */ - READ_TIMEOUT(6); - - private int code; - - /** - * Constructs a new error code with the specified numeric identifier. - * - * @param code the numeric identifier for this error code - */ - private ChannelSerialPortErrorCode(int code) { - this.code = code; - } - - /** - * Returns the base error code without any offset applied. - * - * @return the base error code value - */ - public int getCode() { - return this.code; - } - - /** - * Returns the error code with the specified offset added. - * - *

    This method allows different components or modules to have their own error code ranges by - * applying an offset to the base error code. This helps maintain uniqueness across the entire - * system while allowing for organized error code management. - * - * @param offset the offset to add to the base error code - * @return the error code with the specified offset applied - */ - public int getCode(int offset) { - return offset + this.code; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java deleted file mode 100644 index c4d4a28..0000000 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; -import enedis.lab.types.BytesArray; -import java.util.List; - -/** - * Codec for TIC Historic frames. - * - *

    This codec handles the encoding and decoding of TIC Historic frames, which are used in the - * French electricity meter communication protocol. Historic frames contain historical consumption - * data and are part of the TIC protocol specification. - * - *

    The codec processes byte arrays containing TIC Historic frame data and converts them to - * TICFrameHistoric objects, and vice versa. It validates frame structure, handles data set parsing, - * and ensures proper checksum validation. - * - * @author Enedis Smarties team - */ -public class CodecTICFrameHistoric implements Codec { - - /** - * Separator character used in TIC Historic frames. This is the space character (0x20) used to - * separate different parts of the frame structure in the TIC protocol. - */ - public static final byte SEPARATOR = 0x20; // SP - - /** - * Decodes a byte array containing TIC Historic frame data into a TICFrameHistoric object. - * - *

    This method validates the frame structure by checking for proper beginning and end patterns, - * extracts individual data sets from the frame, and decodes each data set using the - * CodecTICFrameHistoricDataSet codec. If any data set fails to decode, the error is collected and - * thrown as a CodecException. - * - * @param bytesBuffer the byte array containing the TIC Historic frame data - * @return a TICFrameHistoric object containing the decoded frame data - * @throws CodecException if the frame structure is invalid or if any data set fails to decode - */ - @Override - public TICFrameHistoric decode(byte[] bytesBuffer) throws CodecException { - String errorMessage = ""; - BytesArray rawDataSet = null; - TICFrameHistoric ticFrame = null; - CodecTICFrameHistoricDataSet codecTICFrameHistoricDataSet = new CodecTICFrameHistoricDataSet(); - - BytesArray bytesArray = new BytesArray(bytesBuffer); - - if ((true == bytesArray.startsWith(TICFrame.BEGINNING_PATTERN)) - && (true == bytesArray.endsWith(TICFrame.END_PATTERN)) - && (bytesArray.contains(TICFrame.EOT) == false)) { - bytesArray.remove(0); - bytesArray.remove(bytesArray.size() - 1); - - ticFrame = new TICFrameHistoric(); - - List datasetList = - bytesArray.slice( - TICFrameDataSet.BEGINNING_PATTERN, - TICFrameDataSet.END_PATTERN, - BytesArray.CONTIGUOUS); - - if (datasetList.isEmpty() == false) { - for (int i = 0; i < datasetList.size(); i++) { - TICFrameHistoricDataSet dataSet = null; - rawDataSet = datasetList.get(i); - byte[] rawDataSetByte = rawDataSet.getBytes(); - - try { - dataSet = codecTICFrameHistoricDataSet.decode(rawDataSetByte); - ticFrame.addDataSet(dataSet); - } catch (CodecException exception) { - errorMessage += - exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; - } - } - } - } - - if (errorMessage.isEmpty()) { - return ticFrame; - } else { - throw new CodecException(errorMessage, ticFrame); - } - } - - /** - * Encodes a TICFrameHistoric object into a byte array representing a TIC Historic frame. - * - *

    This method takes a TICFrameHistoric object and converts it into the byte representation of - * a TIC Historic frame. It processes each data set in the frame, encodes them using the - * CodecTICFrameHistoricDataSet codec, and wraps the result with proper frame delimiters. - * - * @param ticFrameHistoric the TICFrameHistoric object to encode - * @return a byte array containing the encoded TIC Historic frame - * @throws CodecException if the frame is null, empty, or if any data set fails to encode - */ - @Override - public byte[] encode(TICFrameHistoric ticFrameHistoric) throws CodecException { - String errorMessage = ""; - CodecTICFrameHistoricDataSet codec = new CodecTICFrameHistoricDataSet(); - BytesArray dataSet = new BytesArray(); - - List ticFrameHistoricList = ticFrameHistoric.getDataSetList(); - - if (ticFrameHistoric != null && !ticFrameHistoricList.isEmpty()) { - List groups = ticFrameHistoric.getDataSetList(); - dataSet.add(TICFrame.BEGINNING_PATTERN); - for (int i = 0; i < groups.size(); i++) { - try { - byte[] buff = codec.encode((TICFrameHistoricDataSet) groups.get(i)); - dataSet.addAll(buff); - } catch (CodecException e) { - errorMessage += e.getMessage() + " : " + new String(ticFrameHistoric.getBytes()) + "\n"; - } - } - dataSet.add(TICFrame.END_PATTERN); - } else { - return null; - } - - if (errorMessage.isEmpty()) { - return dataSet.getBytes(); - } else { - throw new CodecException(errorMessage, dataSet.getBytes()); - } - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java deleted file mode 100644 index ff8c40f..0000000 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; -import enedis.lab.types.BytesArray; -import java.util.List; - -/** - * Codec for encoding and decoding TIC historic frame data sets. - * - *

    This class implements the {@link Codec} interface to provide serialization and deserialization - * of {@link TICFrameHistoricDataSet} objects to and from their byte array representation, according - * to the historic TIC protocol format. It ensures the correct structure, delimiters, and checksum - * validation for each data set. - * - *

    Main features: - * - *

      - *
    • Encodes a {@link TICFrameHistoricDataSet} into a byte array with proper delimiters, - * separators, and checksum. - *
    • Decodes a byte array into a {@link TICFrameHistoricDataSet}, validating structure and - * checksum. - *
    • Handles TIC historic data set format: LF LABEL SP DATA SP CHECKSUM CR. - *
    • Throws {@link CodecException} on invalid format or checksum. - *
    - * - * @author Enedis Smarties team - * @see TICFrameHistoricDataSet - * @see CodecTICFrameHistoric - * @see Codec - * @see CodecException - */ -public class CodecTICFrameHistoricDataSet implements Codec { - - /** - * Encode a TICFrameHistoricDataSet into its byte array representation. - * - *

    Format: LF LABEL SP DATA SP CHECKSUM CR - * - * @param ticFrameHistoricDataSet the data set to encode - * @return the encoded byte array - * @throws CodecException if the data set is invalid or checksum is incorrect - */ - @Override - public byte[] encode(TICFrameHistoricDataSet ticFrameHistoricDataSet) throws CodecException { - BytesArray dataSet = new BytesArray(); - - if ((ticFrameHistoricDataSet.getLabel() != null) - && (ticFrameHistoricDataSet.getData() != null)) { - dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); - dataSet.addAll(ticFrameHistoricDataSet.getLabel().getBytes()); - - dataSet.add(TICFrameHistoricDataSet.SEPARATOR); - dataSet.addAll(ticFrameHistoricDataSet.getData().getBytes()); - - dataSet.add(TICFrameHistoricDataSet.SEPARATOR); - - if (!ticFrameHistoricDataSet.isValid()) { - throw new CodecException("Invalid Checksum value"); - } - dataSet.add(ticFrameHistoricDataSet.getChecksum()); - dataSet.add(TICFrameDataSet.END_PATTERN); - } - - return dataSet.getBytes(); - } - - /** - * Decode a byte array into a TICFrameHistoricDataSet. - * - *

    Validates delimiters, structure, and checksum. - * - * @param bytes the byte array to decode - * @return the decoded TICFrameHistoricDataSet - * @throws CodecException if the format or checksum is invalid - */ - @Override - public TICFrameHistoricDataSet decode(byte[] bytes) throws CodecException { - BytesArray bytesArray = new BytesArray(bytes); - TICFrameHistoricDataSet dataSet = null; - - if (this.isStartStopDelimiterPresent(bytesArray)) { - this.removeStartStopDelimiter(bytesArray); - } - - List parts = this.splitFrame(bytesArray); - - // Structure "LABEL/DATA/CHECKSUM" : - if (this.isStructureLabelDataChecksum(parts)) { - - if (this.isChecksumInOneByte(parts.get(2))) { - - dataSet = new TICFrameHistoricDataSet(); - dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); - - } else { - throw new CodecException( - "Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" - + "The Label part must not exceed 8 bytes "); - } - } else { - throw new CodecException("Invalid format of TICFrameHistoricDataSet"); - } - - if (dataSet.isValid() == false) { - throw new CodecException("Invalid Checksum value - Historic"); - } - - return dataSet; - } - - /** - * Checks if the byte array starts and ends with the expected TIC delimiters. - * - * @param bytes the byte array to check - * @return true if delimiters are present, false otherwise - */ - private boolean isStartStopDelimiterPresent(BytesArray bytes) { - return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) - && bytes.endsWith(TICFrameDataSet.END_PATTERN); - } - - /** - * Removes the start and stop delimiters from the byte array (LF and CR). - * - * @param bytes the byte array to modify - */ - private void removeStartStopDelimiter(BytesArray bytes) { - - bytes.remove(0); - bytes.remove(bytes.size() - 1); - } - - /** - * Initializes a TICFrameHistoricDataSet from label, data, and checksum parts. - * - * @param parts the list of BytesArray: label, data, checksum - * @param dataSet the data set to initialize - * @return the initialized TICFrameHistoricDataSet - */ - private TICFrameHistoricDataSet createDataSetLabelDataChecksum( - List parts, TICFrameHistoricDataSet dataSet) { - - dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(2).get(0)); - return dataSet; - } - - /** - * Checks if the checksum part is exactly one byte. - * - * @param part the BytesArray to check - * @return true if the part size is 1, false otherwise - */ - private boolean isChecksumInOneByte(BytesArray part) { - return part.size() == 1; - } - - /** - * Checks if the split frame has the expected structure: label, data, checksum. - * - * @param parts the list of BytesArray - * @return true if the structure matches, false otherwise - */ - private boolean isStructureLabelDataChecksum(List parts) { - return parts.size() == 3; - } - - /** - * Splits the frame into its parts (label, data, checksum) using the TIC separator. - * - * @param bytesArray the byte array to split - * @return a list of BytesArray: label, data, checksum - * @throws CodecException if the frame is too short or has an invalid format - */ - private List splitFrame(BytesArray bytesArray) throws CodecException { - List parts; - - if (bytesArray.size() < 5) { - throw new CodecException("Not enough bytes in TICFrameHistoricDataSet"); - } - if (bytesArray.get(bytesArray.size() - 1) == TICFrameHistoricDataSet.SEPARATOR) { - BytesArray subList = bytesArray.subList(0, bytesArray.size() - 2); - parts = subList.split(TICFrameHistoricDataSet.SEPARATOR); - if (parts.size() < 1) { - throw new CodecException("Invalid format of TICFrameHistoricDataSet"); - } - BytesArray checksum = parts.get(parts.size() - 1); - checksum.addAll(new byte[] {TICFrameHistoricDataSet.SEPARATOR}); - } else { - parts = bytesArray.split(TICFrameHistoricDataSet.SEPARATOR); - } - - return parts; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java deleted file mode 100644 index 6a4f33c..0000000 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; -import enedis.lab.types.BytesArray; -import java.util.List; - -/** - * Codec for encoding and decoding TIC standard frame data sets. - * - *

    This class implements the {@link Codec} interface to provide serialization and deserialization - * of {@link TICFrameStandard} objects to and from their byte array representation, according to the - * standard TIC protocol format. It ensures the correct structure, delimiters, and checksum - * validation for each data set in a frame. - * - *

    Main features: - * - *

      - *
    • Encodes a {@link TICFrameStandard} into a byte array with proper delimiters, separators, - * and checksum. - *
    • Decodes a byte array into a {@link TICFrameStandard}, validating structure and checksum of - * each data set. - *
    • Handles TIC standard data set format, including support for multiple data sets per frame. - *
    • Throws {@link CodecException} on invalid format or checksum. - *
    - * - * @author Enedis Smarties team - * @see TICFrameStandard - * @see TICFrameStandardDataSet - * @see CodecTICFrameStandardDataSet - * @see Codec - * @see CodecException - */ -public class CodecTICFrameStandard implements Codec { - /** - * Decode a byte array into a TICFrameStandard. - * - *

    Validates delimiters, structure, and checksum of each data set. - * - * @param bytes the byte array to decode - * @return the decoded TICFrameStandard - * @throws CodecException if the format or checksum is invalid - */ - @Override - public TICFrameStandard decode(byte[] bytes) throws CodecException { - String errorMessage = ""; - BytesArray rawDataSet = null; - TICFrameStandard ticFrame = null; - CodecTICFrameStandardDataSet codecTICFrameStandardDataSet = new CodecTICFrameStandardDataSet(); - - BytesArray bytesArray = new BytesArray(bytes); - - // Extract the body of the frame - // NB: the presence of an EOT character makes the content of a frame invalid - if ((bytesArray.startsWith(TICFrame.BEGINNING_PATTERN) == true) - && (bytesArray.endsWith(TICFrame.END_PATTERN) == true) - && (bytesArray.contains(TICFrame.EOT) == false)) { - bytesArray.remove(0); - bytesArray.remove(bytesArray.size() - 1); - - ticFrame = new TICFrameStandard(); - - // Isolate each memory area supposed to correspond to an Information Group - // (DataSet) - List datasetList = - bytesArray.slice( - TICFrameDataSet.BEGINNING_PATTERN, - TICFrameDataSet.END_PATTERN, - BytesArray.CONTIGUOUS); - - // Analyze each memory area to extract the controls of the Group of information - // If the format of a zone is invalid, then the browse is interrupted and the - // whole frame is - // invalid - // (null) - if (datasetList.isEmpty() == false) { - for (int i = 0; i < datasetList.size(); i++) { - TICFrameStandardDataSet dataSet = null; - rawDataSet = datasetList.get(i); - byte[] rawDataSetByte = rawDataSet.getBytes(); - try { - dataSet = codecTICFrameStandardDataSet.decode(rawDataSetByte); - ticFrame.addDataSet(dataSet); - } catch (CodecException exception) { - errorMessage += - exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; - } - } - } - } - - if (errorMessage.isEmpty()) { - return ticFrame; - } else { - throw new CodecException(errorMessage, ticFrame); - } - } - - /** - * Encode a TICFrameStandard into its byte array representation. - * - *

    Format: LF [DataSet1] ... [DataSetN] CR - * - * @param ticFrameStandard the frame to encode - * @return the encoded byte array, or null if the frame is empty - * @throws CodecException if a data set is invalid or checksum is incorrect - */ - @Override - public byte[] encode(TICFrameStandard ticFrameStandard) throws CodecException { - CodecTICFrameStandardDataSet codec = new CodecTICFrameStandardDataSet(); - BytesArray dataSet = new BytesArray(); - - List ticFrameStandardList = ticFrameStandard.getDataSetList(); - - if (ticFrameStandard != null && !ticFrameStandardList.isEmpty()) { - List groups = ticFrameStandard.getDataSetList(); - dataSet.add(TICFrame.BEGINNING_PATTERN); - for (int i = 0; i < groups.size(); i++) { - byte[] buff = codec.encode((TICFrameStandardDataSet) groups.get(i)); - dataSet.addAll(buff); - } - dataSet.add(TICFrame.END_PATTERN); - return dataSet.getBytes(); - } else { - return null; - } - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java deleted file mode 100644 index 03fd402..0000000 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; -import enedis.lab.types.BytesArray; -import java.util.List; - -/** - * Codec for encoding and decoding TIC standard frame data sets. - * - *

    This class implements the {@link Codec} interface to provide serialization and deserialization - * of {@link TICFrameStandardDataSet} objects to and from their byte array representation, according - * to the standard TIC protocol format. - * - *

    Main features: - * - *

      - *
    • Encodes a {@link TICFrameStandardDataSet} into a byte array with proper delimiters, - * separators, and checksum. - *
    • Decodes a byte array into a {@link TICFrameStandardDataSet}, validating structure and - * checksum. - *
    • Handles both "LABEL/DATA/CHECKSUM" and "LABEL/DATETIME/DATA/CHECKSUM" formats. - *
    • Throws {@link CodecException} on invalid format or checksum. - *
    - * - * @author Enedis Smarties team - * @see TICFrameStandardDataSet - * @see CodecTICFrameStandard - * @see Codec - * @see CodecException - */ -public class CodecTICFrameStandardDataSet implements Codec { - /** - * Encode a TICFrameStandardDataSet into its byte array representation. - * - * @param ticFrameStandardDataSet the data set to encode - * @return the encoded byte array - * @throws CodecException if the data set is invalid or checksum is incorrect - */ - @Override - public byte[] encode(TICFrameStandardDataSet ticFrameStandardDataSet) throws CodecException { - BytesArray dataSet = new BytesArray(); - - if ((null != ticFrameStandardDataSet.getLabel()) - && (null != ticFrameStandardDataSet.getData())) { - dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); - dataSet.addAll(ticFrameStandardDataSet.getLabel().getBytes()); - - if (ticFrameStandardDataSet.checkDateTime() == true) { - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - dataSet.addAll(ticFrameStandardDataSet.getDateTime().getBytes()); - } - - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - dataSet.addAll(ticFrameStandardDataSet.getData().getBytes()); - - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - if (!ticFrameStandardDataSet.isValid()) { - throw new CodecException("Invalid Checksum value"); - } - - dataSet.add(ticFrameStandardDataSet.getChecksum()); - dataSet.add(TICFrameDataSet.END_PATTERN); - } - - return dataSet.getBytes(); - } - - /** - * Decode a byte array into a TICFrameStandardDataSet. - * - *

    Supports both classic and datetime-extended formats. Validates delimiters, structure, and - * checksum. - * - * @param bytes the byte array to decode - * @return the decoded TICFrameStandardDataSet - * @throws CodecException if the format or checksum is invalid - */ - @Override - public TICFrameStandardDataSet decode(byte[] bytes) throws CodecException { - BytesArray bytesArray = new BytesArray(bytes); - TICFrameStandardDataSet dataSet = null; - - if (this.isStartStopDelimiterPresent(bytesArray)) { - this.removeStartStopDelimiter(bytesArray); - } - - List parts = bytesArray.split(TICFrameStandardDataSet.SEPARATOR); - - // Structure "LABEL/DATA/CHECKSUM" : - if (this.isStructureLabelDataChecksum(parts)) { - if (this.isChecksumInOneByte(parts.get(2))) { - dataSet = new TICFrameStandardDataSet(); - dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); - } else { - throw new CodecException( - "Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" - + "The Label part must not exceed 8 bytes "); - } - } - // Structure "LABEL/DATETIME/DATA/CHECKSUM" : - else if (this.isStructureLabelDateTimeDataChecksum(parts)) { - if (this.isChecksumInOneByte(parts.get(3))) { - dataSet = new TICFrameStandardDataSet(); - dataSet = this.createDataSetLabelDatetimeDataChecksum(parts, dataSet); - } else { - throw new CodecException( - "Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" - + "The Label part must not exceed 8 bytes "); - } - } else { - throw new CodecException("Invalid bytes for decode - Standard"); - } - - if (dataSet.isValid() == false) { - throw new CodecException("Invalid Checksum value"); - } - - return dataSet; - } - - /** - * Checks if the split frame has the expected structure: label, datetime, data, checksum. - * - * @param parts the list of BytesArray - * @return true if the structure matches, false otherwise - */ - private boolean isStructureLabelDateTimeDataChecksum(List parts) { - return parts.size() == 4; - } - - /** - * Checks if the split frame has the expected structure: label, data, checksum. - * - * @param parts the list of BytesArray - * @return true if the structure matches, false otherwise - */ - private boolean isStructureLabelDataChecksum(List parts) { - return parts.size() == 3; - } - - /** - * Checks if the checksum part is exactly one byte. - * - * @param part the BytesArray to check - * @return true if the part size is 1, false otherwise - */ - private boolean isChecksumInOneByte(BytesArray part) { - return part.size() == 1; - } - - /** - * Removes the start and stop delimiters from the byte array (LF and CR). - * - * @param bytes the byte array to modify - */ - private void removeStartStopDelimiter(BytesArray bytes) { - bytes.remove(0); - bytes.remove(bytes.size() - 1); - } - - /** - * Checks if the byte array starts and ends with the expected TIC delimiters. - * - * @param bytes the byte array to check - * @return true if delimiters are present, false otherwise - */ - private boolean isStartStopDelimiterPresent(BytesArray bytes) { - return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) - && bytes.endsWith(TICFrameDataSet.END_PATTERN); - } - - /** - * Initializes a TICFrameStandardDataSet from label, data, and checksum parts. - * - * @param parts the list of BytesArray: label, data, checksum - * @param dataSet the data set to initialize - * @return the initialized TICFrameStandardDataSet - */ - private TICFrameStandardDataSet createDataSetLabelDataChecksum( - List parts, TICFrameStandardDataSet dataSet) { - dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(2).get(0)); - return dataSet; - } - - /** - * Initializes a TICFrameStandardDataSet from label, datetime, data, and checksum parts. - * - * @param parts the list of BytesArray: label, datetime, data, checksum - * @param dataSet the data set to initialize - * @return the initialized TICFrameStandardDataSet - */ - private TICFrameStandardDataSet createDataSetLabelDatetimeDataChecksum( - List parts, TICFrameStandardDataSet dataSet) { - dataSet.setup(parts.get(0).getBytes(), parts.get(2).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(3).get(0)); - return dataSet; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java deleted file mode 100644 index fba799b..0000000 --- a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; -import enedis.lab.protocol.tic.frame.standard.TICException; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; -import enedis.lab.types.BytesArray; - -/** - * Main codec for encoding and decoding TIC frames (standard and historic). - * - *

    This class implements the {@link Codec} interface to provide serialization and deserialization - * of {@link TICFrame} objects to and from their byte array representation, supporting both standard - * and historic TIC protocol formats. It delegates the actual encoding/decoding to the appropriate - * sub-codec depending on the frame mode. - * - *

    Main features: - * - *

      - *
    • Encodes and decode both {@link TICFrameStandard} and {@link TICFrameHistoric} frames. - *
    • Automatically detects the TIC mode (standard/historic/auto) when decoding. - *
    • Maintains an internal buffer and mode state for incremental operations. - *
    • Throws {@link CodecException} on invalid format or checksum. - *
    - * - * @author Enedis Smarties team - * @see TICFrame - * @see TICFrameStandard - * @see TICFrameHistoric - * @see CodecTICFrameStandard - * @see CodecTICFrameHistoric - * @see Codec - * @see CodecException - */ -public class TICCodec implements Codec { - private static final TICMode DEFAULT_MODE = TICMode.STANDARD; - - private BytesArray Buffer; - private TICMode mode = TICMode.UNKNOWN; - private TICMode currentMode = TICMode.UNKNOWN; - - /** - * Constructs a new TICCodec with default mode (AUTO) and empty buffer. - * - *

    Initializes the codec to auto-detect TIC mode and prepares the internal buffer for decoding - * operations. - */ - public TICCodec() { - /** Current TIC mode (STANDARD, HISTORIC, or AUTO for auto-detection). */ - this.mode = TICMode.UNKNOWN; - this.currentMode = TICMode.UNKNOWN; - this.Buffer = new BytesArray(); - } - - /** - * Decodes a byte array into a TICFrame (standard or historic). - * - *

    Automatically detects the TIC mode if set to AUTO, and delegates decoding to the appropriate - * codec. - * - * @param newData the byte array containing the TIC frame data - * @return the decoded {@link TICFrame} (either {@link TICFrameStandard} or {@link - * TICFrameHistoric}) - * @throws CodecException if the data is invalid, the mode cannot be determined, or decoding fails - */ - @Override - public TICFrame decode(byte[] newData) throws CodecException { - CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); - /** Codec for standard TIC frames (historic). */ - CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); - TICFrameStandard ticFrameStandard = null; - TICFrameHistoric ticFrameHistoric = null; - - TICFrame ticFrame = null; - TICMode currentTICMode = null; - - try { - /** Internal buffer for accumulating incoming bytes during decoding. */ - switch (this.currentMode) { - case STANDARD: - { - ticFrameStandard = codecStandard.decode(newData); - ticFrame = ticFrameStandard; - break; - } - case HISTORIC: - { - ticFrameHistoric = codecHistoric.decode(newData); - ticFrame = ticFrameHistoric; - break; - } - - case AUTO: - { - try { - currentTICMode = TICMode.findModeFromFrameBuffer(newData); - } catch (TICException exception) { - /** Codec for historic TIC frames (historic). */ - throw new CodecException("can't determinated TIC Mode"); - } - - if (currentTICMode == TICMode.STANDARD) { - ticFrameStandard = codecStandard.decode(newData); - ticFrame = ticFrameStandard; - break; - } else if (currentTICMode == TICMode.HISTORIC) { - ticFrameHistoric = codecHistoric.decode(newData); - ticFrame = ticFrameHistoric; - } else { - throw new CodecException("can't decode TIC, unable to find TIC Modem"); - } - } - - default: - { - /**/ - } - } - } catch (CodecException exception) { - throw new CodecException(exception.getMessage(), exception.getData()); - } - return ticFrame; - } - - /** - * Encodes a TICFrame (standard or historic) into a byte array. - * - *

    Delegates encoding to the appropriate codec based on the frame's mode. - * - * @param ticFrame the TIC frame to encode (must be {@link TICFrameStandard} or {@link - * TICFrameHistoric}) - * @return the encoded byte array representing the TIC frame - * @throws CodecException if encoding fails or the frame type is unsupported - */ - @Override - public byte[] encode(TICFrame ticFrame) throws CodecException { - CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); - CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); - - byte[] bytesBuffer = new byte[0]; - - try { - switch (ticFrame.getMode()) { - case STANDARD: - { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } - case HISTORIC: - { - bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); - break; - } - default: - { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } - } - } catch (CodecException exception) { - throw new CodecException("Can't encode TICFrame" + exception.getMessage()); - } - return bytesBuffer; - } - - /** - * Resets the internal buffer, clearing any accumulated data. - * - *

    Should be called before starting a new decoding operation or when reusing the codec. - */ - public void reset() { - this.Buffer.clear(); - } - - /** - * Appends a byte array to the internal buffer. - * - * @param data the byte array to append - */ - public void append(byte[] data) { - this.Buffer.addAll(data); - } - - /** - * Appends a single byte to the internal buffer. - * - * @param data the byte to append - */ - public void append(byte data) { - this.Buffer.add(data); - } - - /** - * Appends the contents of a {@link BytesArray} to the internal buffer. - * - * @param data the {@link BytesArray} to append - */ - public void append(BytesArray data) { - this.Buffer.addAll(data.getBytes()); - } - - /** - * Returns the configured TIC mode (STANDARD, HISTORIC, or AUTO). - * - * @return the current configured {@link TICMode} - */ - public TICMode getMode() { - return this.mode; - } - - /** - * Sets the TIC mode (STANDARD, HISTORIC, or AUTO). - * - *

    If set to AUTO, the codec will attempt to auto-detect the mode during decoding. - * - * @param mode the TIC mode to set - */ - public void setMode(TICMode mode) { - if (this.mode != mode) { - this.mode = mode; - - if (TICMode.AUTO != mode) { - this.setCurrentMode(mode); - } else { - this.setCurrentMode(DEFAULT_MODE); - } - } - } - - /** - * Returns the currently active TIC mode (used for decoding). - * - * @return the current active {@link TICMode} - */ - public TICMode getCurrentMode() { - return this.currentMode; - } - - /** - * Sets the currently active TIC mode (used for decoding). - * - * @param currentMode the TIC mode to set as active - */ - public void setCurrentMode(TICMode currentMode) { - if (currentMode != this.currentMode) { - this.currentMode = currentMode; - } - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java deleted file mode 100644 index 3c0f294..0000000 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.datastreams; - -import enedis.lab.codec.CodecException; -import enedis.lab.io.datastreams.DataInputStream; -import enedis.lab.io.datastreams.DataStreamException; -import enedis.lab.io.datastreams.DataStreamType; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.channels.ChannelTICSerialPort; -import enedis.lab.protocol.tic.codec.TICCodec; -import enedis.lab.protocol.tic.frame.TICError; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; - -/** - * Data input stream for TIC (Teleinformation Client) frames. - * - *

    This class extends {@link DataInputStream} to provide decoding and event handling for TIC - * frames received from a configured channel. It uses a {@link TICCodec} to decode raw byte arrays - * into {@link TICFrame} objects and exposes them as {@link DataDictionary} instances. The stream - * supports error handling, event notification, and mode management for both standard and historic - * TIC protocols. - * - *

    Key features: - * - *

      - *
    • Decodes incoming TIC frames using {@link TICCodec} - *
    • Notifies subscribers on new data or errors - *
    • Supports both standard and historic TIC modes - *
    • Provides access to the current TIC mode - *
    - * - * @author Enedis Smarties team - * @see TICCodec - * @see TICFrame - * @see DataInputStream - * @see DataDictionary - */ -public class TICInputStream extends DataInputStream { - /** Key for the timestamp field in the decoded data dictionary. */ - public static final String KEY_TIMESTAMP = "timestamp"; - - /** Key for the channel name field in the decoded data dictionary. */ - public static final String KEY_CHANNEL = "channel"; - - /** Key for the TIC frame data field in the decoded data dictionary. */ - public static final String KEY_DATA = "data"; - - /** Codec used to decode TIC frames from byte arrays. */ - protected TICCodec codec; - - /** - * Constructs a new TICInputStream with the specified configuration. - * - *

    Initializes the codec and sets the TIC mode according to the configuration. - * - * @param configuration the TIC stream configuration - * @throws DataStreamException if the configuration is invalid - */ - public TICInputStream(TICStreamConfiguration configuration) throws DataStreamException { - super(configuration); - this.codec = new TICCodec(); - this.codec.setCurrentMode(configuration.getTicMode()); - } - - /** - * Reads a TIC frame from the input stream and returns it as a {@link DataDictionary}. - * - *

    This method should be implemented to provide actual reading logic. Currently returns null. - * - * @return the decoded TIC frame as a {@link DataDictionary}, or null if not implemented - * @throws DataStreamException if a read error occurs - */ - @Override - public DataDictionary read() throws DataStreamException { - return null; - } - - /** - * Returns the type of this data stream (TIC). - * - * @return {@link DataStreamType#TIC} - */ - @Override - public DataStreamType getType() { - return DataStreamType.TIC; - } - - /** - * Handles incoming data read from the channel. - * - *

    Decodes the TIC frame and notifies subscribers. If decoding fails, notifies error - * subscribers. - * - * @param channelName the name of the channel - * @param data the raw byte array received - */ - @Override - public void onDataRead(String channelName, byte[] data) { - if (!this.notifier.getSubscribers().isEmpty()) { - DataDictionary ticFrame = null; - try { - ticFrame = this.decodeTICFrame(data); - if (ticFrame != null) { - this.notifyOnDataReceived(ticFrame); - } - } catch (DataDictionaryException exception) { - this.logger.error(exception.getMessage(), exception); - } catch (CodecException exception) { - DataDictionaryBase errorDataDictionary = new DataDictionaryBase(); - - String KEY_PARTIAL_FRAME = "partialTICFrame"; - - errorDataDictionary.addKey(KEY_PARTIAL_FRAME); - - try { - Object exceptionData = exception.getData(); - if (exceptionData != null && exceptionData instanceof TICFrame) { - try { - DataDictionary frameDataDictionary = ((TICFrame) exceptionData).getDataDictionary(); - if (frameDataDictionary != null) { - errorDataDictionary.set(KEY_PARTIAL_FRAME, frameDataDictionary); - } else { - this.logger.warn("Frame data dictionary is null"); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } catch (DataDictionaryException frameDataDictionaryException) { - this.logger.warn( - "Can't get TICFrame data dictionary: " - + frameDataDictionaryException.getMessage()); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } else { - this.logger.error("No frame data available"); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } catch (DataDictionaryException dataDictionaryException) { - this.logger.error( - "Can't convert TICFrame to DataDictonary" + dataDictionaryException.getMessage()); - } - - this.logger.error(exception.getMessage() + errorDataDictionary.toString()); - this.onErrorDetected( - channelName, - TICError.TIC_READER_READ_FRAME_CHECKSUM_INVALID.getValue(), - exception.getMessage(), - errorDataDictionary); - } - } - } - - /** - * Not used. TICInputStream does not handle data written events. - * - * @param channelName the name of the channel - * @param data the data written - */ - @Override - public void onDataWritten(String channelName, byte[] data) { - // Not used - } - - /** - * Handles error events detected on the channel (without error data). - * - * @param channelName the name of the channel - * @param errorCode the error code - * @param errorMessage the error message - */ - @Override - public void onErrorDetected(String channelName, int errorCode, String errorMessage) { - this.notifyOnErrorDetected(errorCode, errorMessage, null); - } - - /** - * Handles error events detected on the channel (with error data). - * - * @param channelName the name of the channel - * @param errorCode the error code - * @param errorMessage the error message - * @param errorData additional error data - */ - @Override - public void onErrorDetected( - String channelName, int errorCode, String errorMessage, DataDictionary errorData) { - this.notifyOnErrorDetected(errorCode, errorMessage, errorData); - } - - /** - * Returns the current TIC mode of the underlying channel. - * - * @return the current {@link TICMode} - */ - public TICMode getMode() { - ChannelTICSerialPort channelTIC = (ChannelTICSerialPort) this.channel; - return channelTIC.getMode(); - } - - /** - * Decodes a raw TIC frame byte array into a {@link DataDictionary}. - * - *

    Uses the internal {@link TICCodec} to decode the frame, adds timestamp and channel info. - * - * @param data the raw TIC frame bytes - * @return a {@link DataDictionary} containing the decoded frame, timestamp, and channel name - * @throws DataDictionaryException if the data dictionary cannot be created - * @throws CodecException if decoding fails - */ - protected DataDictionary decodeTICFrame(byte[] data) - throws DataDictionaryException, CodecException { - TICFrame ticFrame; - try { - ticFrame = this.codec.decode(data); - } catch (CodecException exception) { - throw new CodecException(exception.getMessage(), exception.getData()); - } - - long timestamp = System.currentTimeMillis(); - - DataDictionaryBase decodedTICFrame = new DataDictionaryBase(); - - decodedTICFrame.set(KEY_TIMESTAMP, timestamp); - decodedTICFrame.set(KEY_CHANNEL, this.getConfiguration().getChannelName()); - decodedTICFrame.set(KEY_DATA, ticFrame); - - return decodedTICFrame; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java deleted file mode 100644 index 775dd1c..0000000 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.datastreams; - -import enedis.lab.io.datastreams.DataStreamConfiguration; -import enedis.lab.io.datastreams.DataStreamDirection; -import enedis.lab.io.datastreams.DataStreamType; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Configuration class for TIC data streams. - * - *

    This class extends {@link DataStreamConfiguration} to provide configuration management for TIC - * data streams, including TIC mode selection, type, direction, and channel name. It supports - * construction from maps, data dictionaries, or direct parameters, and ensures that all required - * configuration keys are present and valid for TIC input/output streams. - * - *

    Key features: - * - *

      - *
    • Supports both standard and historic TIC modes - *
    • Validates configuration parameters for TIC data streams - *
    • Provides accessors for TIC mode and other stream properties - *
    - * - * @author Enedis Smarties team - * @see DataStreamConfiguration - * @see TICMode - */ -public class TICStreamConfiguration extends DataStreamConfiguration { - /** Key for the TIC mode parameter in the configuration. */ - protected static final String KEY_TIC_MODE = "ticMode"; - - /** Accepted value for the stream type (TIC). */ - private static final DataStreamType TYPE_ACCEPTED_VALUE = DataStreamType.TIC; - - /** Accepted values for the stream direction (INPUT or OUTPUT). */ - private static final DataStreamDirection[] DIRECTION_ACCEPTED_VALUES = { - DataStreamDirection.OUTPUT, DataStreamDirection.INPUT - }; - - /** Default value for the TIC mode (AUTO). */ - private static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; - - /** List of key descriptors for configuration validation. */ - private List> keys = new ArrayList>(); - - /** Key descriptor for the TIC mode parameter. */ - protected KeyDescriptorEnum kTicMode; - - /** Protected default constructor. Initializes key descriptors and sets default values. */ - protected TICStreamConfiguration() { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUES); - this.kChannelName.setMandatory(true); - } - - /** - * Constructs a TICStreamConfiguration from a map of parameters. - * - * @param map the map containing configuration parameters - * @throws DataDictionaryException if the configuration is invalid - */ - public TICStreamConfiguration(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a TICStreamConfiguration from a {@link DataDictionary}. - * - * @param other the data dictionary containing configuration parameters - * @throws DataDictionaryException if the configuration is invalid - */ - public TICStreamConfiguration(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a TICStreamConfiguration with the specified name and file, using default parameters. - * - * @param name the configuration name - * @param file the configuration file - */ - public TICStreamConfiguration(String name, File file) { - this(); - this.init(name, file); - } - - /** - * Constructs a TICStreamConfiguration with the specified parameters. - * - * @param name the configuration name - * @param direction the data stream direction (INPUT or OUTPUT) - * @param channelName the channel name - * @param ticMode the TIC mode (STANDARD, HISTORIC, or AUTO) - * @throws DataDictionaryException if the configuration is invalid - */ - public TICStreamConfiguration( - String name, DataStreamDirection direction, String channelName, TICMode ticMode) - throws DataDictionaryException { - this(); - - this.setName(name); - this.setDirection(direction); - this.setChannelName(channelName); - this.setTicMode(ticMode); - - this.checkAndUpdate(); - } - - /** - * Updates optional configuration parameters to their default values if not set. - * - * @throws DataDictionaryException if an error occurs during update - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_TYPE)) { - this.setType(TYPE_ACCEPTED_VALUE); - } - if (!this.exists(KEY_TIC_MODE)) { - this.setTicMode(TIC_MODE_DEFAULT_VALUE); - } - super.updateOptionalParameters(); - } - - /** - * Returns the configured TIC mode for this stream. - * - * @return the TIC mode (STANDARD, HISTORIC, or AUTO) - */ - public TICMode getTicMode() { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Sets the TIC mode for this stream. - * - * @param ticMode the TIC mode to set (STANDARD, HISTORIC, or AUTO) - * @throws DataDictionaryException if the value is invalid - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException { - this.setTicMode((Object) ticMode); - } - - /** - * Sets the TIC mode for this stream using a generic object. - * - * @param ticMode the TIC mode as an object - * @throws DataDictionaryException if the value is invalid - */ - protected void setTicMode(Object ticMode) throws DataDictionaryException { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - } - - /** Loads key descriptors for configuration validation and type conversion. */ - private void loadKeyDescriptors() { - try { - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); - this.keys.add(this.kTicMode); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java deleted file mode 100644 index c6c6d6d..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame; - -import jssc.SerialPortException; - -/** - * Enumeration of error codes for TIC frame and serial port operations. - * - *

    This enum defines error codes for serial port issues and TIC frame processing errors. It - * provides a mapping from {@link SerialPortException} types to TIC error codes for unified error - * handling. - * - *

    Key features: - * - *

      - *
    • Defines error codes for serial port and TIC frame errors - *
    • Provides a method to map {@link SerialPortException} to TICError - *
    • Each error code has an associated integer value - *
    - * - * @author Enedis Smarties team - * @see SerialPortException - */ -public enum TICError { - /** No error. */ - NO_ERROR(0), - /** Serial port not found. */ - SERIAL_PORT_NOT_FOUND(17), - /** Incorrect serial port. */ - SERIAL_PORT_INCORRECT_SERIAL_PORT(18), - /** Null not permitted for serial port. */ - SERIAL_PORT_NULL_NOT_PERMITTED(19), - /** Serial port already opened. */ - SERIAL_PORT_ALREADY_OPENED(20), - /** Serial port is busy. */ - SERIAL_PORT_SERIAL_PORT_BUSY(21), - /** Serial port configuration failure. */ - SERIAL_PORT_CONFIGURATION_FAILURE(22), - /** Default error for TIC reader. */ - TIC_READER_DEFAULT_ERROR(23), - /** TIC reader label name not found. */ - TIC_READER_LABEL_NAME_NOT_FOUND(24), - /** TIC reader frame decode failed. */ - TIC_READER_FRAME_DECODE_FAILED(25), - /** TIC reader read frame timeout. */ - TIC_READER_READ_FRAME_TIMEOUT(26), - /** TIC reader read frame with checksum invalid. */ - TIC_READER_READ_FRAME_CHECKSUM_INVALID(27), - ; - - /** Integer value associated with the error code. */ - private int value; - - /** - * Constructs a TICError with the specified integer value. - * - * @param value the integer value for the error code - */ - private TICError(int value) { - this.value = value; - } - - /** - * Returns the integer value associated with this error code. - * - * @return the error code value - */ - public int getValue() { - return this.value; - } - - /** - * Maps a {@link SerialPortException} to the corresponding {@link TICError} code. - * - * @param serialPortException the serial port exception to map - * @return the corresponding {@link TICError}, or null if not recognized - */ - public static TICError getSerialPortError(SerialPortException serialPortException) { - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) { - return TICError.SERIAL_PORT_SERIAL_PORT_BUSY; - } - if (serialPortException - .getExceptionType() - .equals(SerialPortException.TYPE_INCORRECT_SERIAL_PORT)) { - return TICError.SERIAL_PORT_INCORRECT_SERIAL_PORT; - } - if (serialPortException - .getExceptionType() - .equals(SerialPortException.TYPE_NULL_NOT_PERMITTED)) { - return TICError.SERIAL_PORT_NULL_NOT_PERMITTED; - } - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_NOT_FOUND)) { - return TICError.SERIAL_PORT_NOT_FOUND; - } - if (serialPortException - .getExceptionType() - .equals(SerialPortException.TYPE_PORT_ALREADY_OPENED)) { - return TICError.SERIAL_PORT_ALREADY_OPENED; - } - return null; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java deleted file mode 100644 index 042ddcc..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java +++ /dev/null @@ -1,427 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.BytesArray; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import org.json.JSONObject; - -/** - * Abstract base class for TIC frames. - * - *

    This class provides the structure and common operations for TIC frames, including data set - * management, byte serialization, and data dictionary conversion. Subclasses implement - * protocol-specific details for standard and historic TIC formats. - * - *

    Key features: - * - *

      - *
    • Defines constants for frame delimiters and options - *
    • Manages a list of labeled data sets - *
    • Provides methods for adding, removing, and retrieving data sets - *
    • Supports conversion to byte arrays and data dictionaries - *
    • Abstract methods for protocol-specific data set and mode handling - *
    - * - * @author Enedis Smarties team - * @see TICFrameDataSet - * @see TICMode - */ -public abstract class TICFrame { - /** Frame start delimiter (STX, 0x02). */ - public static final byte BEGINNING_PATTERN = 0x02; // STX - - /** Frame end delimiter (ETX, 0x03). */ - public static final byte END_PATTERN = 0x03; // ETX - - /** End of transmission delimiter (EOT, 0x04). */ - public static final byte EOT = 0x04; // EOT - - // Frame options and keys - /** No specific options. */ - public static final int EXPLICIT = 0x0000; - - /** Hide checksums in data dictionary. */ - public static final int NOCHECKSUM = 0x0001; - - /** Hide date/time fields in data dictionary. */ - public static final int NODATETIME = 0x0002; - - /** Hide TIC mode in data dictionary. */ - public static final int NOTICMODE = 0x0004; - - /** Filter invalid data sets in data dictionary. */ - public static final int NOINVALID = 0x0008; - - /** Trimmed options: hide checksum, date/time, and invalid data sets. */ - public static final int TRIMMED = NOCHECKSUM | NODATETIME | NOINVALID; - - /** Key for TIC frame in data dictionary. */ - public static final String KEY_TICFRAME = "ticFrame"; - - /** Key for TIC mode in data dictionary. */ - public static final String KEY_TICMODE = "ticMode"; - - /** Key for label in data dictionary. */ - public static final String KEY_LABEL = "label"; - - /** Key for data in data dictionary. */ - public static final String KEY_DATA = "data"; - - /** Key for checksum in data dictionary. */ - public static final String KEY_CHECKSUM = "checksum"; - - /** Key for date/time in data dictionary. */ - public static final String KEY_DATETIME = "dateTime"; - - /** List of data sets contained in this TIC frame. */ - protected List DataSetList; - - /** Constructs an empty TIC frame with an empty data set list. */ - public TICFrame() { - this.DataSetList = new ArrayList(); - } - - @Override - public int hashCode() { - return Objects.hash(this.DataSetList); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (this.getClass() != obj.getClass()) return false; - TICFrame other = (TICFrame) obj; - return Objects.equals(this.DataSetList, other.DataSetList); - } - - /** - * Returns the list of data sets contained in this frame. - * - * @return the list of data sets - */ - public List getDataSetList() { - return this.DataSetList; - } - - /** - * Sets the list of data sets for this frame. - * - * @param dataSetList the list of data sets to set - */ - public void setDataSetList(List dataSetList) { - this.DataSetList = dataSetList; - } - - /** - * Returns the index of the data set with the given label. - * - * @param label the label of the data set - * @return the index of the data set, or -1 if not found - */ - public int indexOf(String label) { - int index = -1; - - for (int i = 0; i < this.DataSetList.size(); i++) { - if (this.DataSetList.get(i).getLabel().equals(label)) { - index = i; - break; - } - } - - return index; - } - - /** - * Returns the data set with the given label, or null if not found. - * - * @param label the label of the data set - * @return the data set, or null if not found - */ - public TICFrameDataSet getDataSet(String label) { - TICFrameDataSet dataSet = null; - - for (int i = 0; i < this.DataSetList.size(); i++) { - if (this.DataSetList.get(i).getLabel().equals(label)) { - dataSet = this.DataSetList.get(i); - break; - } - } - - return dataSet; - } - - /** - * Returns the data value for the data set with the given label, or null if not found. - * - * @param label the label of the data set - * @return the data value, or null if not found - */ - public String getData(String label) { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) { - return dataSet.getData(); - } else { - return null; - } - } - - /** - * Adds the given data set to the end of the list. If a data set with the same label exists, its - * data and checksum are updated. - * - * @param dataSet the data set to add or update - */ - public void addDataSet(TICFrameDataSet dataSet) { - TICFrameDataSet oldDataSet = this.getDataSet(dataSet.getLabel()); - - if (oldDataSet == null) { - this.DataSetList.add(dataSet); - } else { - oldDataSet.setData(dataSet.getData()); - oldDataSet.setChecksum(dataSet.getChecksum()); - } - } - - /** - * Adds the given data set at the specified index. If a data set with the same label exists, its - * data and checksum are updated and it is moved to the new index. - * - * @param index the position to insert the data set - * @param dataSet the data set to add or update - */ - public void addDataSet(int index, TICFrameDataSet dataSet) { - int dataSetIndex = this.indexOf(dataSet.getLabel()); - - if (dataSetIndex < 0) { - this.DataSetList.add(index, dataSet); - } else { - if (dataSetIndex != index) { - this.removeDataSet(dataSetIndex); - this.DataSetList.add(index, dataSet); - } else { - this.DataSetList.get(index).setData(dataSet.getData()); - this.DataSetList.get(index).setChecksum(dataSet.getChecksum()); - } - } - } - - /** - * Adds a new data set with the given label and data to the frame. - * - * @param label the label for the data set - * @param data the data value - * @return the created or updated data set - */ - public abstract TICFrameDataSet addDataSet(String label, String data); - - /** - * Adds a new data set with the given label and data at the specified index in the frame. - * - * @param index the position to insert the data set - * @param label the label for the data set - * @param data the data value - * @return the created or updated data set - */ - public abstract TICFrameDataSet addDataSet(int index, String label, String data); - - /** - * Moves the data set with the given label to the specified index. - * - * @param label the label of the data set to move - * @param index the new index for the data set - * @return true if the operation succeeded, false otherwise - */ - public boolean moveDataSet(String label, int index) { - int previous_index = this.indexOf(label); - - // Si l'ensemble des conditions suivantes sont vérifiées : - // - le Groupe d'Information existe, - // - l'index donné est cohérent - // - l'index donné diffère de l'index actuel - if ((previous_index >= 0) - && (index >= 0) - && (index < this.DataSetList.size()) - && (index != previous_index)) { - // Alors déplacer le Groupe d'Information à l'index donné: - - TICFrameDataSet dataSet = this.DataSetList.remove(previous_index); - - if (index < this.DataSetList.size()) { - this.DataSetList.add(index, dataSet); - } else { - this.DataSetList.add(dataSet); - } - - return true; - } else { - // Sinon, l'opération n'est pas effectuée - return false; - } - } - - /** - * Sets the data value for the data set with the given label. - * - * @param label the label of the data set - * @param data the new data value - * @return true if the data set was found and updated, false otherwise - */ - public boolean setDataSet(String label, String data) { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - return true; - } else { - return false; - } - } - - /** - * Sets the data value and checksum for the data set with the given label. - * - * @param label the label of the data set - * @param data the new data value - * @param checksum the new checksum value - * @return true if the data set was found and updated, false otherwise - */ - public boolean setDataSet(String label, String data, byte checksum) { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) { - dataSet.setData(data); - dataSet.setChecksum(checksum); - - return true; - } else { - return false; - } - } - - /** - * Removes the data set with the given label from the frame. - * - * @param label the label of the data set to remove - * @return the removed data set, or null if not found - */ - public TICFrameDataSet removeDataSet(String label) { - TICFrameDataSet dataSet = null; - - int index = this.indexOf(label); - - if (index >= 0) { - dataSet = this.DataSetList.remove(index); - } - - return dataSet; - } - - /** - * Removes the data set at the specified index from the frame. - * - * @param index the index of the data set to remove - * @return the removed data set, or null if not found - */ - public TICFrameDataSet removeDataSet(int index) { - TICFrameDataSet dataSet = null; - - if ((index >= 0) && (index < this.DataSetList.size())) { - dataSet = this.DataSetList.remove(index); - } - - return dataSet; - } - - /** - * Serializes this frame to a byte array according to the TIC protocol. - * - * @return the byte array representation of the frame - */ - public byte[] getBytes() { - BytesArray bytesFrame = new BytesArray(); - - bytesFrame.add(BEGINNING_PATTERN); - - for (int i = 0; i < this.DataSetList.size(); i++) { - bytesFrame.addAll(this.DataSetList.get(i).getBytes()); - } - - bytesFrame.add(END_PATTERN); - - return bytesFrame.getBytes(); - } - - /** - * Returns a data dictionary representation of this frame with default options. - * - * @return the data dictionary - * @throws DataDictionaryException if conversion fails - */ - public DataDictionary getDataDictionary() throws DataDictionaryException { - return this.getDataDictionary(0); - } - - /** - * Returns a data dictionary representation of this frame with the specified options. - * - * @param options options to DataDictionary render (use like bits fields) : - EXPLICIT : frame is - * detailed - NOCHECKSUM : DataSet checksum are hidden - NODATETIME : DataSet dateTime fields - * are hidden - NOTICMODE : Tic mode is hidden; - TRIMMED : NOCHECKSUM | NODATETIME - - * FILTER_INVALID : Invalid DataSet are filtered - * @return data dictionary - * @throws DataDictionaryException - */ - public DataDictionary getDataDictionary(int options) throws DataDictionaryException { - DataDictionaryBase dictionary = new DataDictionaryBase(); - - JSONObject jsonObject = new JSONObject(); - - if (0 == (options & NOTICMODE)) { - dictionary.addKey(KEY_TICMODE); - dictionary.set(KEY_TICMODE, this.getMode().name()); - } - - for (int i = 0; i < this.DataSetList.size(); i++) { - TICFrameDataSet dataSet = this.DataSetList.get(i); - - // Si le DataSet n'est pas valid et que l'option "filtrer les - // groupes invalides" est activée, - if ((false == dataSet.isValid()) && ((options & NOINVALID) > 0)) { - // Ignorer le DataSet - } - - // Sinon, - else { - jsonObject.append(KEY_TICFRAME, dataSet.toJSON(options)); - } - } - - dictionary.addKey(KEY_TICFRAME); - dictionary.set(KEY_TICFRAME, jsonObject.get(KEY_TICFRAME)); - - return dictionary; - } - - /** - * Returns the TIC mode for this frame (protocol-specific). - * - * @return the TIC mode - */ - public abstract TICMode getMode(); -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java deleted file mode 100644 index 1664b87..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame; - -import enedis.lab.types.BytesArray; -import java.util.Objects; -import org.json.JSONObject; - -/** - * Abstract base class for data sets in TIC frames. - * - *

    This class provides the structure and common operations for TIC frame data sets, including - * label/data management, checksum calculation, and serialization. Subclasses implement - * protocol-specific details for standard and historic TIC formats. - * - *

    Key features: - * - *

      - *
    • Defines constants for data set delimiters - *
    • Manages label, data, and checksum fields - *
    • Provides methods for setting and validating fields - *
    • Abstract methods for protocol-specific serialization and checksum - *
    - * - * @author Enedis Smarties team - * @see BytesArray - */ -public abstract class TICFrameDataSet { - /** Data set start delimiter (LF, 0x0A). */ - public static final byte BEGINNING_PATTERN = 0x0A; // LF - - /** Data set end delimiter (CR, 0x0D). */ - public static final byte END_PATTERN = 0x0D; // CR - - /** Label field for the data set. */ - protected BytesArray label = null; - - /** Data field for the data set. */ - protected BytesArray data = null; - - /** Checksum value for the data set. */ - protected byte checksum; - - /** Constructs an empty TIC frame data set. */ - public TICFrameDataSet() { - this.init(); - } - - @Override - public int hashCode() { - return Objects.hash(this.checksum, this.data, this.label); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (this.getClass() != obj.getClass()) return false; - TICFrameDataSet other = (TICFrameDataSet) obj; - return this.checksum == other.checksum - && Objects.equals(this.data, other.data) - && Objects.equals(this.label, other.label); - } - - /** - * Sets up the data set with the given label and data strings. - * - * @param label the label string - * @param data the data string - */ - public void setup(String label, String data) { - this.setup(label.getBytes(), data.getBytes()); - } - - /** - * Sets up the data set with the given label and data byte arrays. - * - * @param label the label bytes - * @param data the data bytes - */ - public void setup(byte[] label, byte[] data) { - this.label = new BytesArray(label); - this.data = new BytesArray(data); - } - - /** - * Returns the label string for this data set. - * - * @return the label string - */ - public String getLabel() { - return new String(this.label.getBytes()); - } - - /** - * Sets the label field from a string value. - * - * @param label the label string - */ - public void setLabel(String label) { - this.setLabel(label.getBytes()); - } - - /** - * Sets the label field from a byte array value. - * - * @param label the label bytes - */ - public void setLabel(byte[] label) { - this.setLabel(new BytesArray(label)); - } - - /** - * Sets the label field from a {@link BytesArray} value and updates the checksum. - * - * @param label the label as a {@link BytesArray} - */ - public void setLabel(BytesArray label) { - this.label = label; - this.setChecksum(); - } - - /** - * Returns the data string for this data set. - * - * @return the data string - */ - public String getData() { - return new String(this.data.getBytes()); - } - - /** - * Sets the data field from a string value. - * - * @param data the data string - */ - public void setData(String data) { - this.setData(data.getBytes()); - } - - /** - * Sets the data field from a byte array value. - * - * @param data the data bytes - */ - public void setData(byte[] data) { - this.setData(new BytesArray(data)); - } - - /** - * Sets the data field from a {@link BytesArray} value and updates the checksum. - * - * @param data the data as a {@link BytesArray} - */ - public void setData(BytesArray data) { - this.data = data; - this.setChecksum(); - } - - /** - * Returns the checksum value for this data set. - * - * @return the checksum value - */ - public byte getChecksum() { - return this.checksum; - } - - /** - * Computes and sets the checksum for this data set (label and data must be set). - * - * @return true if the operation succeeds, false otherwise - */ - public boolean setChecksum() { - Byte checksum = this.getConsistentChecksum(); - - if (checksum != null) { - this.checksum = checksum; - return true; - } else { - return false; - } - } - - /** - * Sets the checksum to the given value. - * - * @param checksum the checksum value to set - */ - public void setChecksum(byte checksum) { - this.checksum = checksum; - } - - /** - * Checks if the label, data, and checksum fields are consistent. - * - * @return true if the fields are consistent with the checksum, false otherwise - */ - public boolean isValid() { - Byte consistentChecksum = this.getConsistentChecksum(); - - if ((consistentChecksum != null) - && (consistentChecksum.byteValue() == (this.checksum & (byte) 0xFF))) { - return true; - } else { - return false; - } - } - - /** - * Serializes this data set to a byte array according to the TIC protocol. - * - * @return the byte array representation of the data set - */ - public abstract byte[] getBytes(); - - /** - * Converts this data set to a JSON object using the default options. - * - * @return the JSON representation of the data set - */ - public abstract JSONObject toJSON(); - - /** - * Converts this data set to a JSON object with the specified options. - * - * @param option bitmask of options (e.g., hide checksum, date/time) - * @return the JSON representation of the data set - */ - public abstract JSONObject toJSON(int option); - - /** - * Computes the consistent checksum for this data set according to the protocol. - * - * @return the consistent checksum value, or null if not computable - */ - protected abstract Byte getConsistentChecksum(); - - /** Initializes the data set, clearing label, data, and checksum fields. */ - private void init() { - this.label = null; - this.data = null; - this.checksum = -1; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java deleted file mode 100644 index 20995f8..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.historic; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.frame.TICFrame; - -/** - * TIC frame representation for historic TIC protocol. - * - *

    This class extends {@link TICFrame} to provide support for historic TIC frames, including - * separator definition and data set management. It allows adding labeled data sets to the frame and - * always reports its mode as {@link TICMode#HISTORIC}. - * - *

    Key features: - * - *

      - *
    • Defines the separator for historic TIC frames - *
    • Supports adding labeled data sets at specific positions - *
    • Always returns HISTORIC as the TIC mode - *
    - * - * @author Enedis Smarties team - * @see TICFrame - * @see TICFrameHistoricDataSet - * @see TICMode - */ -public class TICFrameHistoric extends TICFrame { - /** Separator character (space, 0x20) used in historic TIC frames. */ - public static final byte SEPARATOR = 0x20; // SP - - /** Constructs an empty historic TIC frame. */ - public TICFrameHistoric() { - super(); - } - - /** - * Adds a new data set with the given label and data to the end of the frame. - * - * @param label the label for the data set - * @param data the data value - * @return the created or updated {@link TICFrameHistoricDataSet} - */ - @Override - public TICFrameHistoricDataSet addDataSet(String label, String data) { - return this.addDataSet(this.DataSetList.size(), label, data); - } - - /** - * Adds a new data set with the given label and data at the specified index in the frame. - * - *

    If a data set with the same label exists, it is updated and moved to the new index. - * - * @param index the position to insert the data set - * @param label the label for the data set - * @param data the data value - * @return the created or updated {@link TICFrameHistoricDataSet} - */ - @Override - public TICFrameHistoricDataSet addDataSet(int index, String label, String data) { - TICFrameHistoricDataSet dataSet = (TICFrameHistoricDataSet) this.getDataSet(label); - - if (dataSet == null) { - dataSet = new TICFrameHistoricDataSet(); - dataSet.setLabel(label); - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - if ((index >= 0) && (index < this.DataSetList.size())) { - this.DataSetList.add(index, dataSet); - } else { - this.DataSetList.add(dataSet); - } - } else { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - this.DataSetList.remove(dataSet); - - if (index >= this.DataSetList.size()) { - this.DataSetList.add(dataSet); - } else { - this.DataSetList.add(index, dataSet); - } - } - - return dataSet; - } - - /** - * Returns the TIC mode for this frame (always HISTORIC). - * - * @return {@link TICMode#HISTORIC} - */ - @Override - public TICMode getMode() { - return TICMode.HISTORIC; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java deleted file mode 100644 index 8bfa7bd..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.historic; - -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.types.BytesArray; -import org.json.JSONObject; - -/** - * Data set representation for historic TIC frames. - * - *

    This class extends {@link TICFrameDataSet} to provide checksum calculation, byte - * serialization, and JSON conversion for historic TIC data sets. It defines the separator and - * implements the protocol-specific checksum logic. - * - *

    Key features: - * - *

      - *
    • Defines the separator for historic TIC data sets - *
    • Implements checksum calculation for label and data - *
    • Serializes the data set to bytes and JSON - *
    - * - * @author Enedis Smarties team - * @see TICFrameDataSet - * @see TICFrame - */ -public class TICFrameHistoricDataSet extends TICFrameDataSet { - /** Separator character (space, 0x20) used in historic TIC data sets. */ - public static final byte SEPARATOR = 0x20; // SP - - /** Constructs an empty historic TIC data set. */ - public TICFrameHistoricDataSet() { - super(); - } - - /** - * Computes the consistent checksum for this data set according to the historic TIC protocol. - * - *

    The checksum is calculated over the label, separator, and data fields. - * - * @return the computed checksum, or null if label or data is missing - */ - @Override - public Byte getConsistentChecksum() { - Byte crc = 0; - byte[] labelByte; - byte[] dataByte; - if ((this.label != null) && (this.data != null)) { - labelByte = this.label.getBytes(); - for (int i = 0; i < labelByte.length; i++) { - crc = this.computeUpdate(crc, labelByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - dataByte = this.data.getBytes(); - - for (int i = 0; i < dataByte.length; i++) { - crc = this.computeUpdate(crc, dataByte[i]); - } - - return this.computeEnd(crc); - } else { - return null; - } - } - - /** - * Updates the checksum value with the given byte. - * - * @param crc the current checksum value - * @param octet the byte to add - * @return the updated checksum value - */ - public Byte computeUpdate(Byte crc, byte octet) { - return (byte) (crc + (octet & 0xff)); - } - - /** - * Finalizes the checksum value according to the historic TIC protocol. - * - * @param crc the current checksum value - * @return the finalized checksum value - */ - public Byte computeEnd(Byte crc) { - return (byte) ((crc & 0x3F) + 0x20); - } - - /** - * Serializes this data set to a byte array according to the historic TIC protocol. - * - * @return the byte array representation of the data set - */ - @Override - public byte[] getBytes() { - BytesArray dataSet = new BytesArray(); - - if ((this.label != null) && (this.data != null)) { - dataSet.add(BEGINNING_PATTERN); - dataSet.addAll(this.label.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.addAll(this.data.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.add(this.checksum); - - dataSet.add(END_PATTERN); - } - - return dataSet.getBytes(); - } - - /** - * Converts this data set to a JSON object using the default options. - * - * @return the JSON representation of the data set - */ - @Override - public JSONObject toJSON() { - return this.toJSON(TICFrame.TRIMMED); - } - - /** - * Converts this data set to a JSON object with the specified options. - * - * @param option bitmask of options (e.g., {@link TICFrame#NOCHECKSUM}) - * @return the JSON representation of the data set - */ - @Override - public JSONObject toJSON(int option) { - JSONObject jsonObject = new JSONObject(); - - if ((option & TICFrame.NOCHECKSUM) > 0) { - jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); - } else { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - - return jsonObject; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java deleted file mode 100644 index 1374d66..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.standard; - -import enedis.lab.protocol.tic.frame.TICError; - -/** - * Exception class for errors encountered during TIC frame processing. - * - *

    This exception is thrown when a TIC frame cannot be parsed, validated, or processed correctly. - * It encapsulates a {@link TICError} code to provide additional context about the error type. - * - *

    Key features: - * - *

      - *
    • Associates a {@link TICError} with each exception instance - *
    • Supports standard exception message and error code - *
    • Provides a reset method to restore default error state - *
    - * - * @author Enedis Smarties team - * @see TICError - * @see Exception - */ -public class TICException extends Exception { - /** Unique identifier used for serialization. */ - private static final long serialVersionUID = -2780151361870269473L; - - /** The TIC error code associated with this exception. */ - private TICError error; - - /** Constructs a new TICException with default error code. */ - public TICException() { - super(); - this.reset(); - } - - /** - * Constructs a new TICException with the specified error message and default error code. - * - * @param message the detail message - */ - public TICException(String message) { - super(message); - this.error = TICError.TIC_READER_DEFAULT_ERROR; - } - - /** - * Constructs a new TICException with the specified error message and error code. - * - * @param message the detail message - * @param readerError the TIC error code - */ - public TICException(String message, TICError readerError) { - super(message); - this.error = readerError; - } - - /** Resets the error code to the default value. */ - public void reset() { - this.error = TICError.TIC_READER_DEFAULT_ERROR; - } - - /** - * Returns the TIC error code associated with this exception. - * - * @return the TIC error code - */ - public TICError getError() { - return this.error; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java deleted file mode 100644 index fee7b1b..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.standard; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.frame.TICFrame; -import java.time.LocalDateTime; - -/** - * TIC frame representation for standard TIC protocol. - * - *

    This class extends {@link TICFrame} to provide support for standard TIC frames, including info - * group delimiters, data set management, and date/time handling. It allows adding labeled data sets - * and retrieving data or date/time values by label. Always reports its mode as {@link - * TICMode#STANDARD}. - * - *

    Key features: - * - *

      - *
    • Defines info group delimiters for standard TIC frames - *
    • Supports adding and retrieving labeled data sets - *
    • Handles date/time fields as strings or {@link LocalDateTime} - *
    • Always returns STANDARD as the TIC mode - *
    - * - * @author Enedis Smarties team - * @see TICFrame - * @see TICFrameStandardDataSet - * @see TICMode - */ -public class TICFrameStandard extends TICFrame { - /** Info group begin delimiter (LF, 0x03) for standard TIC frames. */ - public static final byte INFOGROUP_BEGIN = 0x03; // LF - - /** Info group end delimiter (CR, 0x03) for standard TIC frames. */ - public static final byte INFOGROUP_END = 0x03; // CR - - /** Info group separator (HT, 0x03) for standard TIC frames. */ - public static final byte INFOGROUP_SEP = 0x03; // HT - - /** Minimum size of an info group in bytes (LF + Label + HT + Data + HT + ...). */ - public static final int INFOGROUP_MIN_SIZE = 7; // LF + Label + HT + Data + HT + - - /** Minimum size of a standard TIC frame (info group + ETX + STX). */ - public static final int MIN_SIZE = INFOGROUP_MIN_SIZE + 2; // ETX + INFGROUP_MIN_SIZE + STX - - /** Constructs an empty standard TIC frame. */ - public TICFrameStandard() { - super(); - } - - /** - * Adds a new data set with the given label and data to the frame. - * - *

    If a data set with the same label exists, it is updated and added again. - * - * @param label the label for the data set - * @param data the data value - * @return the created or updated {@link TICFrameStandardDataSet} - */ - @Override - public TICFrameStandardDataSet addDataSet(String label, String data) { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - - if (dataSet == null) { - dataSet = new TICFrameStandardDataSet(); - dataSet.setLabel(label); - dataSet.setData(data); - dataSet.getConsistentChecksum(); - } else { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - } - - this.DataSetList.add(dataSet); - - return dataSet; - } - - /** - * Adds a new data set with the given label and data at the specified index in the frame. - * - *

    If a data set with the same label exists, it is updated and moved to the new index. - * - * @param index the position to insert the data set - * @param label the label for the data set - * @param data the data value - * @return the created or updated {@link TICFrameStandardDataSet} - */ - @Override - public TICFrameStandardDataSet addDataSet(int index, String label, String data) { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - - if (dataSet == null) { - dataSet = new TICFrameStandardDataSet(); - dataSet.setLabel(label); - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - if ((index >= 0) && (index < this.DataSetList.size())) { - this.DataSetList.add(index, dataSet); - } else { - this.DataSetList.add(dataSet); - } - } else { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - this.DataSetList.remove(dataSet); - - if (index >= this.DataSetList.size()) { - this.DataSetList.add(dataSet); - } else { - this.DataSetList.add(index, dataSet); - } - } - - return dataSet; - } - - /** - * Returns the TIC mode for this frame (always STANDARD). - * - * @return {@link TICMode#STANDARD} - */ - @Override - public TICMode getMode() { - return TICMode.STANDARD; - } - - /** - * Returns the data value for the given label, or the date/time if the label is "DATE". - * - * @param label the label to search for - * @return the data value or date/time string, or null if not found - */ - @Override - public String getData(String label) { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - - if (dataSet != null) { - if (dataSet.getLabel().equals("DATE")) { - return dataSet.getDateTime(); - } else { - return dataSet.getData(); - } - } else { - return null; - } - } - - /** - * Adds a new data set with the given label, data, and date/time value. - * - * @param label the label for the data set - * @param data the data value - * @param dateTime the date/time string - * @return the created or updated {@link TICFrameStandardDataSet} - */ - public TICFrameStandardDataSet addDataSet(String label, String data, String dateTime) { - TICFrameStandardDataSet dataSet = this.addDataSet(label, data); - - dataSet.setDateTime(dateTime); - - return dataSet; - } - - /** - * Returns the date/time string for the given label, or null if not found. - * - * @param label the label to search for - * @return the date/time string, or null if not found - */ - public String getDateTime(String label) { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - if (dataSet != null) { - return dataSet.getDateTime(); - } else { - return null; - } - } - - /** - * Returns the date/time as a {@link LocalDateTime} for the given label, or null if not found. - * - * @param label the label to search for - * @return the date/time as {@link LocalDateTime}, or null if not found - */ - public LocalDateTime getDateTimeAsLocalDateTime(String label) { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - if (dataSet != null) { - return dataSet.getDateTimeAsLocalDateTime(); - } else { - return null; - } - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java deleted file mode 100644 index 5db717d..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.standard; - -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.types.BytesArray; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import org.json.JSONObject; - -/** - * Data set representation for standard TIC frames. - * - *

    This class extends {@link TICFrameDataSet} to provide checksum calculation, byte - * serialization, date/time handling, and JSON conversion for standard TIC data sets. It defines the - * separator and implements the protocol-specific logic for standard frames. - * - *

    Key features: - * - *

      - *
    • Defines the separator for standard TIC data sets - *
    • Implements checksum calculation for label, data, and optional date/time - *
    • Handles date/time fields as strings or {@link LocalDateTime} - *
    • Serializes the data set to bytes and JSON - *
    - * - * @author Enedis Smarties team - * @see TICFrameDataSet - * @see TICFrame - */ -public class TICFrameStandardDataSet extends TICFrameDataSet { - /** Separator character (horizontal tab, 0x09) used in standard TIC data sets. */ - public static final byte SEPARATOR = 0x09; // HT - - /** Date/time field for the data set (optional, may be null). */ - protected BytesArray dateTime = null; - - /** Constructs an empty standard TIC data set. */ - public TICFrameStandardDataSet() { - super(); - this.init(); - } - - /** - * Computes the consistent checksum for this data set according to the standard TIC protocol. - * - *

    The checksum is calculated over the label, optional date/time, and data fields. - * - * @return the computed checksum, or null if label or data is missing - */ - @Override - public Byte getConsistentChecksum() { - Byte crc = 0; - byte[] labelByte; - byte[] dataByte; - byte[] dateByte; - - if ((this.label != null) && (this.data != null)) { - labelByte = this.label.getBytes(); - for (int i = 0; i < labelByte.length; i++) { - crc = this.computeUpdate(crc, labelByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - - if (this.dateTime != null) { - dateByte = this.dateTime.getBytes(); - for (int i = 0; i < dateByte.length; i++) { - crc = this.computeUpdate(crc, dateByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - } - - dataByte = this.data.getBytes(); - - for (int i = 0; i < dataByte.length; i++) { - crc = this.computeUpdate(crc, dataByte[i]); - } - crc = this.computeUpdate(crc, SEPARATOR); - - return this.computeEnd(crc); - } else { - return null; - } - } - - /** - * Updates the checksum value with the given byte. - * - * @param crc the current checksum value - * @param octet the byte to add - * @return the updated checksum value - */ - public Byte computeUpdate(Byte crc, byte octet) { - return (byte) (crc + (octet & 0xff)); - } - - /** - * Finalizes the checksum value according to the standard TIC protocol. - * - * @param crc the current checksum value - * @return the finalized checksum value - */ - public Byte computeEnd(Byte crc) { - return (byte) ((crc & 0x3F) + 0x20); - } - - /** - * Serializes this data set to a byte array according to the standard TIC protocol. - * - * @return the byte array representation of the data set - */ - @Override - public byte[] getBytes() { - BytesArray dataSet = new BytesArray(); - - if ((this.label != null) && (this.data != null)) { - dataSet.add(BEGINNING_PATTERN); - dataSet.addAll(this.label.getBytes()); - - if (null != this.dateTime) { - dataSet.add(SEPARATOR); - dataSet.addAll(this.dateTime.getBytes()); - } - - dataSet.add(SEPARATOR); - dataSet.addAll(this.data.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.add(this.checksum); - - dataSet.add(END_PATTERN); - } - - return dataSet.getBytes(); - } - - /** - * Converts this data set to a JSON object using the default options. - * - * @return the JSON representation of the data set - */ - @Override - public JSONObject toJSON() { - return this.toJSON(TICFrame.TRIMMED); - } - - /** - * Converts this data set to a JSON object with the specified options. - * - * @param option bitmask of options (e.g., {@link TICFrame#NOCHECKSUM}, {@link - * TICFrame#NODATETIME}) - * @return the JSON representation of the data set - */ - @Override - public JSONObject toJSON(int option) { - JSONObject jsonObject = new JSONObject(); - - if ((option & TICFrame.NOCHECKSUM) > 0) { - if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) { - jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); - } else { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); - jsonObject.put(new String(this.label.getBytes()), content); - } - } else { - if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } else { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - } - - return jsonObject; - } - - /** - * Sets up the data set with the given label, data, and date/time strings. - * - * @param label the label string - * @param data the data string - * @param dateTime the date/time string - */ - public void setup(String label, String data, String dateTime) { - this.setup(label.getBytes(), data.getBytes(), dateTime.getBytes()); - } - - /** - * Sets up the data set with the given label, data, and date/time byte arrays. - * - * @param label the label bytes - * @param data the data bytes - * @param dateTime the date/time bytes - */ - public void setup(byte[] label, byte[] data, byte[] dateTime) { - super.setup(label, data); - this.dateTime = new BytesArray(dateTime); - } - - /** - * Returns the date/time string for this data set, or null if not set. - * - * @return the date/time string, or null if not set - */ - public String getDateTime() { - return this.dateTime == null ? null : new String(this.dateTime.getBytes()); - } - - /** - * Checks if the date/time field is set. - * - * @return true if date/time is set, false otherwise - */ - public Boolean checkDateTime() { - return this.dateTime != null; - } - - /** - * Returns the date/time as a {@link LocalDateTime} for this data set, or null if not set or - * invalid. - * - *

    The expected format is "HyyMMddHHmmss" (13 characters, with the first character ignored). - * - * @return the date/time as {@link LocalDateTime}, or null if not set or invalid - */ - public LocalDateTime getDateTimeAsLocalDateTime() { - String strDateTime = this.getDateTime(); - LocalDateTime localDateTime = null; - - if (strDateTime != null && strDateTime.length() == 13) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMddHHmmss"); - try { - localDateTime = LocalDateTime.parse(strDateTime.substring(1), formatter); - } catch (DateTimeParseException e) { - localDateTime = null; - } - } - - return localDateTime; - } - - /** - * Sets the date/time field from a string value. - * - * @param dateTime the date/time string - */ - public void setDateTime(String dateTime) { - this.setDateTime(dateTime.getBytes()); - } - - /** - * Sets the date/time field from a byte array value. - * - * @param dateTime the date/time bytes - */ - public void setDateTime(byte[] dateTime) { - this.setDateTime(new BytesArray(dateTime)); - } - - /** - * Sets the date/time field from a {@link BytesArray} value. - * - * @param dateTime the date/time as a {@link BytesArray} - */ - public void setDateTime(BytesArray dateTime) { - this.dateTime = dateTime; - this.setChecksum(); - } - - /** Initializes the data set, clearing the date/time field. */ - private void init() { - this.dateTime = null; - } -} diff --git a/src/main/java/enedis/lab/types/BytesArray.java b/src/main/java/enedis/lab/types/BytesArray.java deleted file mode 100644 index a993f4b..0000000 --- a/src/main/java/enedis/lab/types/BytesArray.java +++ /dev/null @@ -1,996 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; - -/** - * Represents a mutable array of bytes with utility methods for manipulation, searching, and - * slicing. - * - *

    Implements the {@link List} interface for bytes, and provides additional methods for - * byte-level operations, such as splitting, slicing, and computing checksums. Supports conversion - * from integers and arrays, and offers options for greedy, contiguous, and pattern-based - * operations. - * - * @author Enedis Smarties team - */ -public class BytesArray implements List { - /** Default option for byte array operations. */ - public static final int DEFAULT_OPTIONS = 0x0000; - - /** Greedy option for slice/split operations. */ - public static final int GREEDY = 0x0001; - - /** Contiguous option for slice/split operations. */ - public static final int CONTIGUOUS = 0x0002; - - /** Option to remove pattern bytes from results. */ - public static final int REMOVE_PATTERNS = 0x0004; - - /** - * Convert int to byte[] - * - * @param value - * @param numberOfByte - * @return byte[] - * @throws NumberFormatException - */ - public static byte[] intToArrayOfByte(int value, int numberOfByte) throws NumberFormatException { - if (countBytes(value) > numberOfByte) { - throw new NumberFormatException( - "Value " + value + " can't be converted in a byte[" + numberOfByte + "]"); - } - - byte[] byteArray = new byte[numberOfByte]; - - for (int i = 0; i < byteArray.length; i++) { - if (i < Integer.BYTES) { - byteArray[byteArray.length - i - 1] = (byte) (value >> (i * 8)); - } - } - - return byteArray; - } - - /** - * Conver int to BytesArray - * - * @param value - * @param numberOfByte - * @return BytesArray - * @throws Exception - */ - public static BytesArray valueOf(int value, int numberOfByte) throws Exception { - return new BytesArray(intToArrayOfByte(value, numberOfByte)); - } - - private static int countBytes(int value) { - int tmp = Math.abs(value); - int count = 0; - - for (int i = 0; i < Integer.BYTES; i++) { - tmp = tmp >> 8; - count++; - if (tmp == 0) { - break; - } - } - return count; - } - - protected byte[] buffer; - - /** Constructs an empty BytesArray. */ - public BytesArray() { - this.buffer = new byte[0]; - } - - /** - * Constructs a BytesArray from a given byte array. - * - * @param bytesArray the source array of bytes (copied) - */ - public BytesArray(byte[] bytesArray) { - this.buffer = bytesArray.clone(); - } - - @Override - public int size() { - return this.buffer.length; - } - - @Override - public boolean isEmpty() { - return (0 == this.buffer.length) ? true : false; - } - - @Override - public boolean contains(Object element) { - int index = this.indexOf(element); - return (index >= 0) ? true : false; - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Override - public Iterator iterator() { - throw new UnsupportedOperationException(); - } - - @Override - public Byte[] toArray() { - Byte[] bytesArray = new Byte[this.buffer.length]; - - for (int i = 0; i < this.buffer.length; i++) { - bytesArray[i] = this.buffer[i]; - } - - return bytesArray; - } - - @SuppressWarnings("unchecked") - @Override - public Byte[] toArray(Object[] a) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean add(Byte element) { - byte byte_element = element; - byte[] bytesArray = new byte[this.buffer.length + 1]; - - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); - bytesArray[bytesArray.length - 1] = byte_element; - this.buffer = bytesArray; - - return true; - } - - @Override - public void add(int index, Byte element) { - if ((index < 0) || (index >= this.buffer.length)) { - this.add(element); - } else { - byte[] bytesArray = new byte[this.buffer.length + 1]; - - if (0 == index) { - System.arraycopy(this.buffer, 0, bytesArray, 1, this.buffer.length); - } else if ((this.buffer.length - 1) == index) { - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length - 1); - - bytesArray[bytesArray.length - 1] = this.buffer[this.buffer.length - 1]; - } else { - System.arraycopy(this.buffer, 0, bytesArray, 0, index - 1); - - System.arraycopy( - this.buffer, - index, // src , src_pos - bytesArray, - index + 1, // dest, dest_pos - this.buffer.length - index - 1); // size - } - - bytesArray[index] = element; - this.buffer = bytesArray; - } - } - - @Override - public boolean remove(Object element) { - boolean success = true; - - if ((null != element) && (element instanceof Number)) { - int index = this.indexOf(element); - - if (index >= 0) { - this.removeIndex(index); - } else { - success = false; - } - } else { - success = false; - } - - return success; - } - - @Override - public Byte remove(int index) { - Byte element = null; - - if ((index >= 0) && (index < this.buffer.length)) { - element = new Byte(this.buffer[index]); - this.removeIndex(index); - } - - return element; - } - - /** - * Removes a range of bytes from the buffer, from {@code fromIndex} to {@code toIndex} - * (inclusive). - * - * @param fromIndex the starting index (inclusive) - * @param toIndex the ending index (inclusive) - * @return the number of bytes removed, or 0 if indices are invalid - */ - public int remove(int fromIndex, int toIndex) { - int length = 0; - if ((fromIndex >= 0) && (toIndex < this.buffer.length) && (toIndex > fromIndex)) { - this.removeSubList(fromIndex, toIndex); - length = toIndex - fromIndex + 1; - } - return length; - } - - @Override - public boolean containsAll(Collection c) { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean addAll(Collection collection) { - boolean hasChanged; - - if ((null != collection) && (collection.size() > 0)) { - Object[] collectionArray = collection.toArray(); - byte[] bytesArray = new byte[this.buffer.length + collection.size()]; - - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); - - int j = this.buffer.length; - for (int i = 0; i < collectionArray.length; i++) { - bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); - j++; - } - - this.buffer = bytesArray; - - hasChanged = true; - } else { - hasChanged = false; - } - - return hasChanged; - } - - @Override - public boolean addAll(int index, Collection collection) { - boolean hasChanged; - - if ((null != collection) - && (collection.size() > 0) - && (index >= 0) - && (index <= this.buffer.length)) { - Object[] collectionArray = collection.toArray(); - byte[] bytesArray = new byte[this.buffer.length + collection.size()]; - - if (0 == index) { - for (int i = 0; i < collectionArray.length; i++) { - bytesArray[i] = ((Byte) collectionArray[i]).byteValue(); - } - - System.arraycopy(this.buffer, 0, bytesArray, collectionArray.length, this.buffer.length); - } else if (this.buffer.length == index) { - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); - - int j = index; - for (int i = 0; i < collectionArray.length; i++) { - bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); - j++; - } - } else { - System.arraycopy(this.buffer, 0, bytesArray, 0, index); - - int j = index; - for (int i = 0; i < collectionArray.length; i++) { - bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); - j++; - } - - System.arraycopy( - this.buffer, - index, - bytesArray, - index + collectionArray.length, - this.buffer.length - index); - } - - this.buffer = bytesArray; - - hasChanged = true; - } else { - hasChanged = false; - } - - return hasChanged; - } - - @Override - public boolean removeAll(Collection c) { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean retainAll(Collection c) { - // TODO Auto-generated method stub - return false; - } - - @Override - public void clear() { - this.buffer = new byte[0]; - } - - @Override - public Byte get(int index) { - Byte element = null; - - if ((index >= 0) && (index < this.buffer.length)) { - element = new Byte(this.buffer[index]); - } else { - - } - - return element; - } - - @Override - public Byte set(int index, Byte element) { - Byte previous_element = null; - - if ((index >= 0) && (index < this.buffer.length)) { - previous_element = new Byte(this.buffer[index]); - this.buffer[index] = element; - } else { - previous_element = new Byte(this.buffer[this.buffer.length - 1]); - this.buffer[this.buffer.length - 1] = element; - } - - return new Byte(previous_element); - } - - @Override - public int indexOf(Object element) { - return this.indexOf(element, 0); - } - - /** - * Returns the index of the first occurrence of the specified element, starting from the given - * offset. - * - * @param element the element to search for (must be a Number) - * @param offset the index to start searching from - * @return the index of the element, or -1 if not found - */ - public int indexOf(Object element, int offset) { - int index = -1; - if ((null != element) - && (element instanceof Number) - && offset >= 0 - && offset < this.buffer.length) { - Byte byte_element = ((Number) element).byteValue(); - for (int i = offset; i < this.buffer.length; i++) { - if (this.buffer[i] == byte_element) { - index = i; - break; - } - } - } - return index; - } - - @Override - public int lastIndexOf(Object element) { - int index = -1; - - if ((null != element) && (element instanceof Number)) { - Byte byte_element = ((Number) element).byteValue(); - - for (int i = this.buffer.length - 1; i >= 0; i--) { - if (this.buffer[i] == byte_element) { - index = i; - break; - } - } - } - - return index; - } - - @Override - public ListIterator listIterator() { - // TODO Auto-generated method stub - return null; - } - - @Override - public ListIterator listIterator(int index) { - // TODO Auto-generated method stub - return null; - } - - @Override - public BytesArray subList(int fromIndex, int toIndex) { - BytesArray subBytesArray = new BytesArray(new byte[0]); - - if (fromIndex >= 0) { - if ((toIndex > fromIndex) && (toIndex < this.buffer.length)) { - int size = (toIndex - fromIndex) + 1; - byte[] buffer = new byte[size]; - - System.arraycopy(this.buffer, fromIndex, buffer, 0, size); - - subBytesArray = new BytesArray(buffer); - } else if ((toIndex < 0) || (toIndex >= this.buffer.length)) { - int size = this.buffer.length - fromIndex; - byte[] buffer = new byte[size]; - - System.arraycopy(this.buffer, fromIndex, buffer, 0, size); - - subBytesArray = new BytesArray(buffer); - } else { - - } - } - - return subBytesArray; - } - - /** - * Returns a copy of the underlying byte array. - * - * @return a copy of the bytes in this BytesArray - */ - public byte[] getBytes() { - return this.buffer; - } - - /** - * Returns a deep copy of this BytesArray. - * - * @return a new BytesArray with the same contents - */ - public BytesArray copy() { - return new BytesArray(this.buffer); - } - - /** - * Returns a list of all indices where the specified element occurs in the buffer. - * - * @param element the element to search for (must be a Number) - * @return a list of indices where the element is found - */ - public List indexesOf(Object element) { - List indexesList = new ArrayList(); - if ((null != element) && (element instanceof Number)) { - Byte byte_element = ((Number) element).byteValue(); - for (int i = 0; i < this.buffer.length; i++) { - if (this.buffer[i] == byte_element) { - indexesList.add(i); - } - } - } - return indexesList; - } - - /** - * @param value - * @return true if the BytesArray starts with the given value, else return false - */ - public boolean startsWith(byte value) { - boolean retVal; - - if ((this.buffer.length > 0) && (this.buffer[0] == value)) { - retVal = true; - } else { - retVal = false; - } - - return retVal; - } - - /** - * @param value - * @return true if the BytesArray ends with the given value, else return false - */ - public boolean endsWith(byte value) { - boolean retVal; - - if ((this.buffer.length > 0) && (this.buffer[this.buffer.length - 1] == value)) { - retVal = true; - } else { - retVal = false; - } - - return retVal; - } - - /** - * Get element count - * - * @param element - * @return element count - */ - public int count(Object element) { - return this.indexesOf(element).size(); - } - - /** - * Split - * - * @param pattern - * @return split BytesArray - */ - public List split(Number pattern) { - List patternsIndexesList = this.indexesOf(pattern.byteValue()); - List bytesArraysList = new ArrayList(); - - // If the split pattern exists, copy each segment into the corresponding element - // of the array - if (false == patternsIndexesList.isEmpty()) { - int begin_index = 0; - int end_index; - int segment_length; - int nb_segments = patternsIndexesList.size() + 1; - - for (int i = 0; i < nb_segments; i++) { - end_index = - (i < patternsIndexesList.size()) ? patternsIndexesList.get(i) : this.buffer.length; - - segment_length = end_index - begin_index; - - // If the segment size is not zero - if (segment_length > 0) { - byte[] segment = new byte[segment_length]; - - System.arraycopy(this.buffer, begin_index, segment, 0, segment_length); - - bytesArraysList.add(new BytesArray(segment)); - } else { - bytesArraysList.add(new BytesArray()); - } - - begin_index = end_index + 1; - } - } - - // Otherwise, copy all data into the single element of the array - else { - bytesArraysList.add(new BytesArray(this.buffer)); - } - - return bytesArraysList; - } - - /** - * Extract parts in the ByteArray according specific 'begin' and 'end' patterns.
    - * NB : 'begin' and 'end' patterns shall be different. Use split() method if they don't.
    - *
    - * Example:
    - *
    - * BytesArray myArray(new byte[12] {42,14,20,98,34,47,14,61,17,98,110,25});
    - * List mySlicedArray = myArray.slice(14,98);
    - * mySlicedArray.get(0) contains {14,20,98}
    - * mySlicedArray.get(1) contains {14,61,17,98}
    - * - * @param beginPattern - * @param endPattern - * @return slices list - */ - public List slice(Number beginPattern, Number endPattern) { - return this.slice(beginPattern, endPattern, -1, 0); - } - - /** - * @param beginPattern - * @param endPattern - * @param options - * @return slices list - */ - public List slice(Number beginPattern, Number endPattern, int options) { - return this.slice(beginPattern, endPattern, -1, options); - } - - /** - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return slices list - */ - public List slice( - Number beginPattern, Number endPattern, int occurences, int options) { - List bytesArraysList; - - // ... If the function call is "greedy", there will be at most one element to - // return, - // which will correspond to the largest segment [begin_pattern, end_pattern] of - // the array. - // Note: the 'occurences' parameter is then not significant. - if ((options & GREEDY) != 0) { - bytesArraysList = this.sliceGreedy(beginPattern, endPattern, options); - } - - // ... If the function call is "not greedy", the elements to return will - // correspond - // to the most rational segments [begin_pattern, end_pattern], within the limit - // of the first n occurrences requested. - else { - bytesArraysList = this.sliceNotGreedy(beginPattern, endPattern, occurences, options); - } - - return bytesArraysList; - } - - /** - * Add all byte - * - * @param bytesArray - * @return true if changed has been applied - */ - public boolean addAll(byte[] bytesArray) { - boolean hasChanged = false; - - if (bytesArray != null && bytesArray.length > 0) { - byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; - - // copy existing data - System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); - - // copy new data - System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); - - this.buffer = newBytesArray; - hasChanged = true; - } else { - hasChanged = false; - } - - return hasChanged; - } - - /** - * Add all byte at index - * - * @param index - * @param bytesArray - * @return true if changed has been applied - */ - public boolean addAll(int index, byte[] bytesArray) { - boolean hasChanged = false; - - if ((index >= 0) && (index <= this.buffer.length)) { - byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; - - if (0 == index) { - System.arraycopy(bytesArray, 0, newBytesArray, 0, bytesArray.length); - System.arraycopy(this.buffer, 0, newBytesArray, bytesArray.length, this.buffer.length); - } else if (this.buffer.length == index) { - System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); - System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); - } else { - System.arraycopy(this.buffer, 0, newBytesArray, 0, index); - System.arraycopy(bytesArray, 0, newBytesArray, index, bytesArray.length); - System.arraycopy( - this.buffer, - index, - newBytesArray, - index + bytesArray.length, - this.buffer.length - index); - } - - this.buffer = newBytesArray; - hasChanged = true; - } else { - hasChanged = false; - } - - return hasChanged; - } - - /** - * Compute the BytesArray checksum on 8bits (Byte) - * - * @return the computed checksum or 'null' if the BytesArray is empty - */ - public Byte checksum8() { - Byte checksum = null; - - if (this.buffer.length > 0) { - byte checksum_8 = 0; - - for (int i = 0; i < this.buffer.length; i++) { - checksum_8 += this.buffer[i]; - } - - checksum = new Byte(checksum_8); - } else { - - } - - return checksum; - } - - /** - * Compute the BytesArray checksum on 16 bits (Short) - * - * @return the computed checksum or 'null' if the BytesArray is empty - */ - public Short checksum16() { - Short checksum = null; - - if (this.buffer.length > 0) { - short checksum_16 = 0; - - for (int i = 0; i < this.buffer.length; i++) { - checksum_16 += this.buffer[i]; - } - - checksum = new Short(checksum_16); - } else { - - } - - return checksum; - } - - /** - * Compute the BytesArray checksum on 32 bits (Integer) - * - * @return the computed checksum or 'null' if the BytesArray is empty - */ - public Integer checksum32() { - Integer checksum = null; - - if (this.buffer.length > 0) { - int checksum_32 = 0; - - for (int i = 0; i < this.buffer.length; i++) { - checksum_32 += this.buffer[i]; - } - - checksum = new Integer(checksum_32); - } else { - - } - - return checksum; - } - - /** - * @param beginPattern - * @param endPattern - * @param options - * @return - */ - private List sliceGreedy(Number beginPattern, Number endPattern, int options) { - List bytesArraysList = new ArrayList(); - - int beginIndex = this.indexOf(beginPattern.byteValue()); - - if (beginIndex >= 0) { - int endIndex = this.lastIndexOf(endPattern.byteValue()); - - if (endIndex > 0) { - if ((options & REMOVE_PATTERNS) != 0) { - beginIndex++; - endIndex--; - } - - int size = endIndex - beginIndex + 1; - - if (size > 0) { - byte[] bytesBuffer = new byte[size]; - - System.arraycopy(this.buffer, beginIndex, bytesBuffer, 0, size); - - bytesArraysList.add(new BytesArray(bytesBuffer)); - } else { - bytesArraysList.add(new BytesArray(new byte[0])); - } - } - } else { - - } - - return bytesArraysList; - } - - /** - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return - */ - private List sliceNotGreedy( - Number beginPattern, Number endPattern, int occurences, int options) { - List bytesArraysList = new ArrayList(); - - // If segment contiguity is required, - if ((options & CONTIGUOUS) != 0) { - bytesArraysList = this.sliceContiguous(beginPattern, endPattern, occurences, options); - } else { - bytesArraysList = this.sliceNotContiguous(beginPattern, endPattern, occurences, options); - } - - return bytesArraysList; - } - - /** - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return - */ - private List sliceContiguous( - Number beginPattern, Number endPattern, int occurences, int options) { - List bytesArraysList = new ArrayList(); - - int beginIndex = -1; - int endIndex = -1; - - for (int i = 0; i < this.buffer.length; i++) { - // Handle a BEGIN pattern - if (this.buffer[i] == beginPattern.byteValue()) { - if ((-1 == endIndex) || (this.buffer[i - 1] == endPattern.byteValue())) { - beginIndex = i; - } - } - - // Handle a END pattern - else if (this.buffer[i] == endPattern.byteValue()) { - // If we are not at the last element of the array - if (i < (this.buffer.length - 1)) { - // If the next character is the start pattern - if (this.buffer[i + 1] == beginPattern.byteValue()) { - endIndex = i; - - this.slicePart(bytesArraysList, beginIndex, endIndex, options); - - // If a number of occurrences is specified and the number of segments has - // already been reached, - // stop processing - if ((occurences > 0) && (bytesArraysList.size() >= occurences)) { - break; - } - } - } - - // Otherwise, if we are at the last element of the array - else { - endIndex = i; - - this.slicePart(bytesArraysList, beginIndex, endIndex, options); - - // If a number of occurrences is specified and the number of segments has - // already been reached, - // stop processing - if ((occurences > 0) && (bytesArraysList.size() >= occurences)) { - break; - } - } - } else { - - } - } - - return bytesArraysList; - } - - /** - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return - */ - private List sliceNotContiguous( - Number beginPattern, Number endPattern, int occurences, int options) { - List bytesArraysList = new ArrayList(); - - int beginIndex = -1; - int endIndex = -1; - - for (int i = 0; i < this.buffer.length; i++) { - if (this.buffer[i] == beginPattern.byteValue()) { - beginIndex = i; - } else if (this.buffer[i] == endPattern.byteValue()) { - endIndex = i; - - this.slicePart(bytesArraysList, beginIndex, endIndex, options); - - // If a number of occurrences is specified and the number of segments has - // already been reached, - // stop processing - if ((occurences > 0) && (bytesArraysList.size() >= occurences)) { - break; - } - } else { - - } - } - - return bytesArraysList; - } - - /** - * @param bytesArraysList - * @param beginIndex - * @param endIndex - * @param options - */ - private void slicePart( - List bytesArraysList, int beginIndex, int endIndex, int options) { - if (beginIndex >= 0) { - if ((options & REMOVE_PATTERNS) != 0) { - beginIndex++; - endIndex--; - } - - BytesArray part = this.subList(beginIndex, endIndex); - - if (null != part) { - bytesArraysList.add(part); - } - } - } - - /** - * @param index - */ - private void removeIndex(int index) { - byte[] bytesArray = new byte[this.buffer.length - 1]; - - // If the index is the first in the array - if (0 == index) { - System.arraycopy(this.buffer, 1, bytesArray, 0, bytesArray.length); - } - - // Else, if the index is the last - else if ((this.buffer.length - 1) == index) { - System.arraycopy(this.buffer, 0, bytesArray, 0, bytesArray.length); - } - - // Else, if the index is in the middle of the array - else { - System.arraycopy(this.buffer, 0, bytesArray, 0, index); - - System.arraycopy( - this.buffer, - index + 1, // src , src_pos - bytesArray, - index, // dest, dest_pos - bytesArray.length - index); // size - } - - this.buffer = bytesArray; - } - - private void removeSubList(int fromIndex, int toIndex) { - byte[] bytesArray = new byte[this.buffer.length - (toIndex - fromIndex + 1)]; - - if (fromIndex > 0) { - System.arraycopy(this.buffer, 0, bytesArray, 0, fromIndex); - } - if (toIndex + 1 < this.buffer.length) { - System.arraycopy( - this.buffer, toIndex + 1, bytesArray, fromIndex, bytesArray.length - fromIndex); - } - - this.buffer = bytesArray; - } -} diff --git a/src/main/java/enedis/lab/types/DataArrayList.java b/src/main/java/enedis/lab/types/DataArrayList.java deleted file mode 100644 index 476dc9a..0000000 --- a/src/main/java/enedis/lab/types/DataArrayList.java +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Resizable-array implementation of the {@code DataList} interface. - * - * @param the type of elements in this data list - */ -public class DataArrayList extends ArrayList implements DataList { - private static final long serialVersionUID = 3116080161929203526L; - - private static final int JSON_INDENT_FACTOR = 0; - - /** - * Returns a fixed-size data list backed by the specified array. (Changes to the returned data - * list "write through" to the array.) This method acts as bridge between array-based and - * collection-based APIs, in combination with {@link Collection#toArray}. - * - *

    This method also provides a convenient way to create a fixed-size data list initialized to - * contain several elements. - * - * @param the class of the objects in the array - * @param items the array by which the data list will be backed - * @return a data list view of the specified array - */ - @SafeVarargs - public static DataList asList(E... items) { - DataList dataList = new DataArrayList(); - - for (E item : items) { - dataList.add(item); - } - - return dataList; - } - - /** - * Instantiate a data list from a File - * - * @param the class of the objects in the list - * @param file - * @param clazz - * @return a data list - * @throws JSONException - * @throws IOException - */ - public static DataList fromFile(File file, Class clazz) - throws JSONException, IOException { - InputStream stream = new FileInputStream(file); - - return fromStream(stream, clazz); - } - - /** - * Instantiate a data list from a Stream - * - * @param the class of the objects in the list - * @param stream - * @param clazz - * @return a data list - * @throws JSONException - * @throws IOException - */ - public static DataList fromStream(InputStream stream, Class clazz) - throws JSONException, IOException { - byte[] buffer = new byte[stream.available()]; - stream.read(buffer); - stream.close(); - String text = new String(buffer); - - return fromString(text, clazz); - } - - /** - * Instantiate a data list from a String - * - * @param the class of the objects in the list - * @param text - * @param clazz - * @return a data list - * @throws JSONException - */ - public static DataList fromString(String text, Class clazz) throws JSONException { - if (text == null) { - return null; - } - - JSONArray jsonArray = new JSONArray(text); - - return fromJSON(jsonArray, clazz); - } - - /** - * Instantiate a data list from a JSONObject - * - * @param the class of the objects in the list - * @param jsonArray - * @param clazz - * @return a data list - * @throws JSONException - */ - public static DataList fromJSON(JSONArray jsonArray, Class clazz) throws JSONException { - if (jsonArray == null) { - return null; - } - DataList dataList = new DataArrayList(); - for (int i = 0; i < jsonArray.length(); i++) { - Object jsonItem = jsonArray.get(i); - E dataListItem; - try { - if (JSONObject.NULL.equals(jsonItem)) { - dataListItem = null; - } else if (clazz.isAssignableFrom(jsonItem.getClass())) { - dataListItem = clazz.cast(jsonItem); - } else if (Enum.class.isAssignableFrom(clazz)) { - dataListItem = convertToEnum(jsonItem, clazz); - } else if (LocalDateTime.class.isAssignableFrom(clazz)) { - dataListItem = convertToLocalDateTime(jsonItem, clazz); - } else if (List.class.isAssignableFrom(clazz)) { - dataListItem = convertToDataList(jsonItem, clazz); - } else if (DataDictionary.class.isAssignableFrom(clazz)) { - dataListItem = convertToDataDictionary(jsonItem, clazz); - } else { - throw new IllegalArgumentException( - "Cannot convert " + jsonItem.getClass().getName() + " to " + clazz.getName()); - } - } catch (NoSuchMethodException - | SecurityException - | IllegalAccessException - | IllegalArgumentException e) { - throw new IllegalArgumentException( - "Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getMessage() + ")", - e); - } catch (InvocationTargetException e) { - throw new IllegalArgumentException( - "Cannot convert " - + jsonItem - + " to " - + clazz.getName() - + " (" - + e.getTargetException().getMessage() - + ")", - e); - } - dataList.add(dataListItem); - } - - return dataList; - } - - /** Constructs an empty data list with an initial capacity of ten. */ - public DataArrayList() { - super(); - } - - /** - * Constructs a data list containing the elements of the specified collection, in the order they - * are returned by the collection's iterator. - * - * @param collection the collection whose elements are to be placed into this data list - * @throws NullPointerException if the specified collection is null - */ - public DataArrayList(Collection collection) { - super(collection); - } - - @Override - public JSONArray toJSON() { - JSONArray jsonArray = new JSONArray(); - - for (E item : this) { - Object objectItem; - if (item instanceof DataDictionary) { - DataDictionary dictionaryItem = (DataDictionary) item; - objectItem = dictionaryItem.toJSON(); - } else { - objectItem = item; - } - jsonArray.put(objectItem); - } - - return jsonArray; - } - - @Override - public void toFile(File file, int indentFactor) throws IOException { - OutputStream stream = new FileOutputStream(file); - - this.toStream(stream, indentFactor); - } - - @Override - public void toStream(OutputStream stream, int indentFactor) throws IOException { - String text = this.toString(indentFactor); - - stream.write(text.getBytes()); - } - - @Override - public String toString() { - return this.toString(JSON_INDENT_FACTOR); - } - - @Override - public String toString(int identFactor) { - return this.toJSON().toString(identFactor); - } - - @SuppressWarnings("unchecked") - private static E convertToDataDictionary(Object value, Class clazz) - throws NoSuchMethodException, - SecurityException, - IllegalAccessException, - IllegalArgumentException, - InvocationTargetException { - E result; - - if (value instanceof JSONObject) { - Method method = clazz.getMethod("fromJSON", JSONObject.class, Class.class); - result = (E) method.invoke(null, value, clazz); - } else { - throw new IllegalArgumentException( - "Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToDataList(Object value, Class clazz) { - E result; - - if (value instanceof JSONArray) { - result = (E) DataArrayList.fromJSON((JSONArray) value, Object.class); - } else { - throw new IllegalArgumentException( - "Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToEnum(Object value, Class clazz) - throws NoSuchMethodException, - SecurityException, - IllegalAccessException, - IllegalArgumentException, - InvocationTargetException { - E result; - - if (value instanceof String) { - Method method = clazz.getDeclaredMethod("valueOf", String.class); - result = (E) method.invoke(null, (String) value); - } else { - throw new IllegalArgumentException( - "Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToLocalDateTime(Object value, Class clazz) - throws NoSuchMethodException, - SecurityException, - IllegalAccessException, - IllegalArgumentException, - InvocationTargetException { - E result; - - if (value instanceof String) { - Method method = clazz.getDeclaredMethod("parse", CharSequence.class); - result = (E) method.invoke(null, (String) value); - } else { - throw new IllegalArgumentException( - "Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } -} diff --git a/src/main/java/enedis/lab/types/DataDictionary.java b/src/main/java/enedis/lab/types/DataDictionary.java deleted file mode 100644 index d543ccd..0000000 --- a/src/main/java/enedis/lab/types/DataDictionary.java +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Map; -import org.json.JSONObject; - -/** - * Interface for managing key-value data dictionaries with serialization and utility methods. - * - *

    Provides methods for key management, value access, serialization to JSON and Map, file and - * stream output, and cloning. Implementations may support additional validation and type safety. - * - * @author Enedis Smarties team - */ -public interface DataDictionary extends Cloneable { - - /** - * Checks if the specified key exists in the data dictionary. - * - * @param key the key to check - * @return true if the key exists, false otherwise - */ - public boolean exists(String key); - - /** - * Returns the list of all keys currently present in the data dictionary. - * - * @return an array of key names - */ - public String[] keys(); - - /** - * Checks if the data dictionary contains the specified key in its key set. - * - * @param key the key to check - * @return true if the key is present, false otherwise - */ - public boolean existsInKeys(String key); - - /** - * Adds the specified key to the data dictionary. If the key already exists, no action is taken. - * - * @param key the key to add - */ - public void addKey(String key); - - /** - * Removes the specified key from the data dictionary. - * - * @param key the key to remove - */ - public void removeKey(String key); - - /** Removes all keys and values from the data dictionary. */ - public void clear(); - - /** - * Returns the value associated with the specified key. - * - * @param key the key whose value is to be returned - * @return the value of the given key, or null if not present - */ - public Object get(String key); - - /** - * Sets the value for the specified key in the data dictionary. - * - * @param key the key to set - * @param value the value to associate with the key - * @throws DataDictionaryException if the value is invalid or cannot be set - */ - public void set(String key, Object value) throws DataDictionaryException; - - /** - * Copies all key-value pairs from another data dictionary into this one. - * - * @param other the data dictionary to copy from - * @throws DataDictionaryException if a value cannot be copied - */ - public void copy(DataDictionary other) throws DataDictionaryException; - - /** - * Serializes this data dictionary to a {@link JSONObject}. - * - * @return a JSONObject representing the data dictionary - */ - public JSONObject toJSON(); - - /** - * Serializes this data dictionary to a {@link Map}. - * - * @return a map representing the data dictionary - */ - public Map toMap(); - - /** - * Writes the data dictionary to a file in JSON format. - * - * @param file the file to write to - * @param indentFactor the number of spaces to add to each level of indentation - * @throws IOException if an I/O error occurs - */ - public void toFile(File file, int indentFactor) throws IOException; - - /** - * Writes the data dictionary to an output stream in JSON format. - * - * @param stream the output stream to write to - * @param indentFactor the number of spaces to add to each level of indentation - * @throws IOException if an I/O error occurs - */ - public void toStream(OutputStream stream, int indentFactor) throws IOException; - - @Override - public String toString(); - - /** - * Returns a string representation of the data dictionary in JSON format with the specified - * indentation. - * - * @param indentFactor the number of spaces to add to each level of indentation - * @return a string containing the JSON representation - */ - public String toString(int indentFactor); - - /** - * Creates and returns a deep copy of this data dictionary. - * - * @return a clone of this data dictionary - * @throws CloneNotSupportedException if the object cannot be cloned - */ - public Object clone() throws CloneNotSupportedException; - - /** Prints the contents of this data dictionary to the console. */ - public void print(); -} diff --git a/src/main/java/enedis/lab/types/DataDictionaryException.java b/src/main/java/enedis/lab/types/DataDictionaryException.java deleted file mode 100644 index d78d6f4..0000000 --- a/src/main/java/enedis/lab/types/DataDictionaryException.java +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -/** - * Exception type used to represent errors occurring when manipulating or interpreting data - * dictionary elements. - * - *

    This exception typically indicates issues such as invalid data mappings, corrupted metadata, - * or unexpected dictionary structures encountered during runtime. - * - *

    It extends {@link java.lang.Exception}, allowing both checked exception handling and detailed - * error propagation with message and cause information. - * - * @author Enedis Smarties team - */ -public class DataDictionaryException extends Exception { - - private static final long serialVersionUID = -7967428428453584771L; - - /** Creates a new {@code DataDictionaryException} with no detail message or cause. */ - public DataDictionaryException() { - super(); - } - - /** - * Creates a new {@code DataDictionaryException} with the specified detail message. - * - * @param message the detail message describing the error context - */ - public DataDictionaryException(String message) { - super(message); - } - - /** - * Creates a new {@code DataDictionaryException} with the specified cause. - * - * @param cause the underlying cause of the exception - */ - public DataDictionaryException(Throwable cause) { - super(cause); - } - - /** - * Creates a new {@code DataDictionaryException} with the specified detail message and cause. - * - * @param message the detail message describing the error context - * @param cause the underlying cause of the exception - */ - public DataDictionaryException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Creates a new {@code DataDictionaryException} with full control over suppression and stack - * trace writability. - * - * @param message the detail message describing the error context - * @param cause the underlying cause of the exception - * @param enableSuppression whether suppression is enabled or disabled - * @param writableStackTrace whether the stack trace should be writable - */ - public DataDictionaryException( - String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/src/main/java/enedis/lab/types/DataList.java b/src/main/java/enedis/lab/types/DataList.java deleted file mode 100644 index c73b2d8..0000000 --- a/src/main/java/enedis/lab/types/DataList.java +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.List; -import org.json.JSONArray; - -/** - * Interface extending Java standard List to provide JSON serialization mechanism - * - * @param the type of elements in this list - */ -public interface DataList extends List { - /** - * Write this data list to a File - * - * @param file the output file - * @param indentFactor JSON indentation factor - * @throws IOException if writing file fails - */ - public void toFile(File file, int indentFactor) throws IOException; - - /** - * Write this data list to a Stream - * - * @param stream the output stream - * @param indentFactor JSON indentation factor - * @throws IOException if writing stream fails - */ - public void toStream(OutputStream stream, int indentFactor) throws IOException; - - @Override - public String toString(); - - /** - * Convert this data list to a String - * - * @param indentFactor JSON indentation factor - * @return JSON string representation - */ - public String toString(int indentFactor); - - /** - * Convert this data list to JSON array - * - * @return JSON array - */ - public JSONArray toJSON(); -} diff --git a/src/main/java/enedis/lab/types/ExceptionBase.java b/src/main/java/enedis/lab/types/ExceptionBase.java deleted file mode 100644 index e03d831..0000000 --- a/src/main/java/enedis/lab/types/ExceptionBase.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -/** - * Generic exception base class. - * - *

    This class provides a simple mechanism to associate an error code and an informational message - * with an exception. It can be used as a parent class for more specific exception types that - * require standardized error reporting and structured diagnostic data. - * - *

    Both the code and info fields are intended to help identify and categorize the source of the - * problem more precisely in distributed or modular systems. - * - * @author Enedis Smarties team - */ -@SuppressWarnings("serial") -public class ExceptionBase extends Exception { - - /** Numeric error code identifying the specific error type. */ - protected int code; - - /** Descriptive information about the error context. */ - protected String info; - - /** - * Creates a new {@code ExceptionBase} instance with the specified error code and message. - * - * @param code the numeric error code associated with this exception - * @param info the descriptive message providing details about the error - */ - public ExceptionBase(int code, String info) { - this.setErrorCode(code); - this.setErrorInfo(info); - } - - /** - * Returns a formatted message including both the descriptive info and the error code. - * - * @return the formatted exception message - */ - @Override - public String getMessage() { - return this.info + " (" + this.code + ")"; - } - - /** - * Returns the error code associated with this exception. - * - * @return the numeric error code - */ - public int getErrorCode() { - return this.code; - } - - /** - * Sets the error code for this exception. - * - * @param errorCode the numeric error code to assign - */ - public void setErrorCode(int errorCode) { - this.code = errorCode; - } - - /** - * Returns the descriptive information message associated with this exception. - * - * @return the error information message - */ - public String getErrorInfo() { - return this.info; - } - - /** - * Sets the descriptive information message for this exception. - * - * @param errorInfo the descriptive message to assign - */ - public void setErrorInfo(String errorInfo) { - this.info = errorInfo; - } -} diff --git a/src/main/java/enedis/lab/types/configuration/Configuration.java b/src/main/java/enedis/lab/types/configuration/Configuration.java deleted file mode 100644 index a7ccad2..0000000 --- a/src/main/java/enedis/lab/types/configuration/Configuration.java +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.configuration; - -import enedis.lab.types.DataDictionary; -import java.io.File; - -/** - * Generic interface for handling application configuration objects. - * - *

    This interface defines the basic operations for loading, saving, and identifying a - * configuration, regardless of its type or format. It extends {@link DataDictionary} to allow - * structured manipulation of configuration parameters. - * - *

      - *
    • Robust loading and saving with exception handling - *
    • Unique identification by name and associated file - *
    • Interoperability with data dictionaries - *
    - * - * @author Enedis Smarties team - * @see DataDictionary - * @see ConfigurationException - */ -public interface Configuration extends DataDictionary { - - /** - * Loads the configuration. - * - *

    Must throw a {@link ConfigurationException} if reading fails, the format is invalid, or the - * data is inconsistent. - * - * @throws ConfigurationException if loading fails or the format is invalid - */ - public void load() throws ConfigurationException; - - /** - * Saves the current configuration. - * - *

    Must throw a {@link ConfigurationException} if writing fails or if there is an access - * problem. - * - * @throws ConfigurationException if saving fails - */ - public void save() throws ConfigurationException; - - /** - * Returns the unique name of the configuration. - * - *

    This name is used to identify the configuration within the application or for user display. - * - * @return the configuration name (never null) - */ - public String getConfigName(); - - /** - * Returns the reference to the associated configuration file. - * - *

    May return {@code null} if the configuration is not linked to a physical file. - * - * @return the configuration file, or {@code null} if not applicable - */ - public File getConfigFile(); -} diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java b/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java deleted file mode 100644 index 3912c4c..0000000 --- a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.configuration; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import java.io.File; -import java.util.Map; - -/** - * Base implementation of the {@link Configuration} interface for application configuration - * management. - * - *

    This class extends {@link DataDictionaryBase} and provides default mechanisms for loading, - * saving, and identifying configuration objects. - * - *

    Key features include: - * - *

      - *
    • File-based persistence with structured exception reporting - *
    • Support for cloning and copying configuration data - *
    • Consistent identification by name and file reference - *
    - * - *

    Intended to be subclassed for specific configuration types (e.g., channel, datastream). - * - * @author Enedis Smarties team - * @see Configuration - * @see DataDictionaryBase - * @see ConfigurationException - */ -public class ConfigurationBase extends DataDictionaryBase implements Configuration { - - /** The unique name identifying this configuration. */ - private String name = null; - - /** The file associated with this configuration, or null if not file-based. */ - private File file = null; - - /** - * Protected default constructor for subclassing. - * - *

    Initializes an empty configuration with no name or file. - */ - protected ConfigurationBase() { - super(); - } - - /** - * Constructs a configuration by copying from another {@link DataDictionary}. - * - * @param other the data dictionary to copy from - * @throws DataDictionaryException if copying fails - */ - public ConfigurationBase(DataDictionary other) throws DataDictionaryException { - super(other); - } - - /** - * Constructs a configuration from a map of key-value pairs. - * - * @param map the map containing configuration data - * @throws DataDictionaryException if mapping fails - */ - public ConfigurationBase(Map map) throws DataDictionaryException { - super(map); - } - - /** - * Constructs a configuration with a specific name and file reference. - * - * @param name the configuration name - * @param file the configuration file - */ - public ConfigurationBase(String name, File file) { - super(); - this.init(name, file); - } - - /** - * Loads the configuration from the associated file. - * - *

    Throws a {@link ConfigurationException} if loading or copying fails. - * - * @throws ConfigurationException if the file cannot be read or parsed - */ - @Override - public void load() throws ConfigurationException { - DataDictionary configuration; - try { - configuration = DataDictionaryBase.fromFile(this.file, this.getClass()); - } catch (Exception exception) { - throw new ConfigurationException( - "Cannot load configuration '" - + ((this.name == null) ? "" : this.name) - + "' from file '" - + ((this.file == null) ? "" : this.file) - + "' (" - + exception.getMessage() - + ") " - + exception.getClass().getSimpleName()); - } - try { - this.copy(configuration); - } catch (Exception exception) { - throw new ConfigurationException( - "Cannot copy configuration '" - + ((this.name == null) ? "" : this.name) - + "' from file '" - + ((this.file == null) ? "" : this.file) - + "' (" - + exception.getMessage() - + ")"); - } - } - - /** - * Saves the configuration to the associated file. - * - *

    Throws a {@link ConfigurationException} if writing fails. - * - * @throws ConfigurationException if the file cannot be written - */ - @Override - public void save() throws ConfigurationException { - try { - this.toFile(this.file, 2); - } catch (Exception exception) { - throw new ConfigurationException( - "Cannot save configuration '" - + ((this.name == null) ? "" : this.name) - + "' to file '" - + ((this.file == null) ? "" : this.file) - + "' (" - + exception.getMessage() - + ")"); - } - } - - /** - * Returns the unique name of this configuration. - * - * @return the configuration name, or null if not set - */ - @Override - public String getConfigName() { - return this.name; - } - - /** - * Returns the file associated with this configuration. - * - * @return the configuration file, or null if not file-based - */ - @Override - public File getConfigFile() { - return this.file; - } - - /** - * Sets the configuration name. - * - * @param configName the new configuration name - */ - public void setConfigName(String configName) { - this.name = configName; - } - - /** - * Initializes the configuration with a name and file reference. - * - * @param name the configuration name - * @param file the configuration file - */ - protected void init(String name, File file) { - this.name = name; - this.file = file; - } -} diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java b/src/main/java/enedis/lab/types/configuration/ConfigurationException.java deleted file mode 100644 index 907bac1..0000000 --- a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.configuration; - -import enedis.lab.types.DataDictionaryException; - -/** - * Exception type for configuration-related errors. - * - *

    This exception is thrown to indicate problems encountered during configuration operations, - * such as missing or invalid parameters, file I/O errors, or type mismatches. It extends {@link - * DataDictionaryException} to provide compatibility with the data dictionary error handling - * framework. - * - *

    Static factory methods are provided for common error scenarios, and all standard exception - * constructors are available for flexibility. - * - * @author Enedis Smarties team - * @see DataDictionaryException - */ -public class ConfigurationException extends DataDictionaryException { - - /** Serialization identifier for compatibility. */ - private static final long serialVersionUID = 8090072693974075297L; - - /** - * Creates an exception indicating that a required configuration parameter is missing. - * - * @param parameter the name of the missing parameter - * @return a new {@code ConfigurationException} describing the error - */ - public static ConfigurationException createMissingParameterException(String parameter) { - return new ConfigurationException("Parameter '" + parameter + "' is missing"); - } - - /** - * Creates an exception indicating that an unknown configuration parameter was encountered. - * - * @param parameter the name of the unknown parameter - * @return a new {@code ConfigurationException} describing the error - */ - public static ConfigurationException createUnknownParameterException(String parameter) { - return new ConfigurationException("Parameter '" + parameter + "' is unknown"); - } - - /** - * Creates an exception indicating that a configuration parameter has an invalid value. - * - * @param parameter the name of the parameter - * @param info additional information about the invalid value - * @return a new {@code ConfigurationException} describing the error - */ - public static ConfigurationException createInvalidParameterValueException( - String parameter, String info) { - return new ConfigurationException( - "Parameter '" + parameter + "' has an invalid value : " + info); - } - - /** - * Creates an exception indicating that a configuration parameter has an invalid type. - * - * @param parameter the name of the parameter - * @param type the expected type - * @return a new {@code ConfigurationException} describing the error - */ - public static ConfigurationException createInvalidParameterTypeException( - String parameter, Class type) { - return new ConfigurationException( - "Parameter '" + parameter + "' has an invalid type : " + type.getName()); - } - - /** Constructs a new configuration exception with no detail message. */ - public ConfigurationException() { - super(); - } - - /** - * Constructs a new configuration exception with the specified detail message, cause, suppression - * enabled or disabled, and writable stack trace enabled or disabled. - * - * @param message the detail message - * @param cause the cause - * @param enableSuppression whether suppression is enabled or disabled - * @param writableStackTrace whether the stack trace should be writable - */ - public ConfigurationException( - String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - - /** - * Constructs a new configuration exception with the specified detail message and cause. - * - * @param message the detail message - * @param cause the cause - */ - public ConfigurationException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Constructs a new configuration exception with the specified detail message. - * - * @param message the detail message - */ - public ConfigurationException(String message) { - super(message); - } - - /** - * Constructs a new configuration exception with the specified cause. - * - * @param cause the cause - */ - public ConfigurationException(Throwable cause) { - super(cause); - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java b/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java deleted file mode 100644 index 26721ad..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java +++ /dev/null @@ -1,691 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Basic implementation of the {@link DataDictionary} interface. - * - *

    This class provides a flexible and extensible data dictionary structure for storing key-value - * pairs, supporting serialization to and from JSON, file and stream I/O, and key descriptor - * management. It is designed to be used as a base class for more specific data dictionary - * implementations. - * - *

    Features include: - * - *

      - *
    • Static factory methods for instantiating from files, streams, strings, JSON objects, and - * maps - *
    • Support for nested data dictionaries and lists - *
    • Key descriptor management for type safety and validation - *
    • Serialization and deserialization to/from JSON and Java Map - *
    • Cloning, equality, and string representation - *
    • Mandatory and optional key validation - *
    - * - * @author Enedis Smarties team - */ -public class DataDictionaryBase implements DataDictionary { - private static final int JSON_INDENT_FACTOR = 0; - - /** - * Creates a new data dictionary instance from a file containing JSON data. - * - * @param file the file to read from - * @param clazz the class of the data dictionary to instantiate - * @return a new data dictionary instance - * @throws JSONException if the file content is not valid JSON - * @throws DataDictionaryException if the dictionary cannot be created - * @throws IOException if an I/O error occurs - */ - public static DataDictionary fromFile(File file, Class clazz) - throws JSONException, DataDictionaryException, IOException { - if (file == null) { - throw new DataDictionaryException("Can't load file from null"); - } - - InputStream stream = new FileInputStream(file); - - return DataDictionaryBase.fromStream(stream, clazz); - } - - /** - * Creates a new data dictionary instance from an input stream containing JSON data. - * - * @param stream the input stream to read from - * @param clazz the class of the data dictionary to instantiate - * @return a new data dictionary instance - * @throws JSONException if the stream content is not valid JSON - * @throws DataDictionaryException if the dictionary cannot be created - * @throws IOException if an I/O error occurs - */ - public static DataDictionary fromStream(InputStream stream, Class clazz) - throws JSONException, DataDictionaryException, IOException { - if (stream == null) { - return null; - } - - byte[] buffer = new byte[stream.available()]; - stream.read(buffer); - stream.close(); - String text = new String(buffer); - - return DataDictionaryBase.fromString(text, clazz); - } - - /** - * Creates a new data dictionary instance from a JSON string. - * - * @param text the JSON string - * @param clazz the class of the data dictionary to instantiate - * @return a new data dictionary instance - * @throws JSONException if the string is not valid JSON - * @throws DataDictionaryException if the dictionary cannot be created - */ - public static DataDictionary fromString(String text, Class clazz) - throws JSONException, DataDictionaryException { - if (text == null) { - return null; - } - - JSONObject jsonObject = new JSONObject(text); - - return DataDictionaryBase.fromJSON(jsonObject, clazz); - } - - /** - * Creates a new data dictionary instance from a {@link JSONObject}. - * - * @param jsonObject the JSON object - * @param clazz the class of the data dictionary to instantiate - * @return a new data dictionary instance - * @throws JSONException if the object is not valid JSON - * @throws DataDictionaryException if the dictionary cannot be created - */ - public static DataDictionary fromJSON( - JSONObject jsonObject, Class clazz) - throws JSONException, DataDictionaryException { - if (jsonObject == null) { - return null; - } - - return fromMap(jsonObject.toMap(), clazz); - } - - /** - * Creates a new data dictionary instance from a map of key-value pairs and a dictionary class. - * - * @param map the map containing key-value pairs - * @param clazz the class of the data dictionary to instantiate - * @return a new data dictionary instance - * @throws DataDictionaryException if the dictionary cannot be created - */ - public static DataDictionary fromMap( - Map map, Class clazz) - throws DataDictionaryException { - if (map == null) { - return null; - } - if (clazz == null) { - return fromMap(map); - } - DataDictionary dataDictionnary; - try { - Constructor constructor = clazz.getConstructor(Map.class); - dataDictionnary = constructor.newInstance(map); - } catch (InvocationTargetException exception) { - throw new DataDictionaryException( - "Cannot create instance of " - + clazz.getName() - + " : " - + exception.getTargetException().getMessage()); - } catch (Exception exception) { - throw new DataDictionaryException( - "Cannot create instance of " + clazz.getName() + " : " + exception.getMessage()); - } - - return dataDictionnary; - } - - /** - * Creates a new data dictionary instance from a map of key-value pairs. - * - * @param map the map containing key-value pairs - * @return a new data dictionary instance - * @throws DataDictionaryException if the dictionary cannot be created - */ - public static DataDictionary fromMap(Map map) throws DataDictionaryException { - if (map == null) { - return null; - } - DataDictionaryBase dataDictionnary = new DataDictionaryBase(); - for (String key : map.keySet()) { - Object value = map.get(key); - if (value instanceof JSONObject) { - JSONObject jsonObjectValue = (JSONObject) value; - DataDictionary dataDictionaryValue = fromMap(jsonObjectValue.toMap()); - dataDictionnary.set(key, dataDictionaryValue); - } else if (value instanceof JSONArray) { - JSONArray jsonArrayValue = (JSONArray) value; - DataList listValue = new DataArrayList(jsonArrayValue.toList()); - dataDictionnary.set(key, listValue); - } else { - dataDictionnary.set(key, value); - } - } - - return dataDictionnary; - } - - /** List of key descriptors defining the structure and validation rules for this dictionary. */ - private List> keyDescriptors; - - /** Internal map storing the key-value pairs of the dictionary. */ - protected HashMap data; - - /** Constructs an empty data dictionary with no key descriptors or data. */ - public DataDictionaryBase() { - this.init(); - } - - /** - * Constructs a data dictionary from a map of key-value pairs. - * - * @param map the map containing initial key-value pairs - * @throws DataDictionaryException if an error occurs during initialization - */ - public DataDictionaryBase(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a data dictionary by copying another data dictionary. - * - * @param other the data dictionary to copy - * @throws DataDictionaryException if an error occurs during initialization - */ - public DataDictionaryBase(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - @Override - /** - * Checks if a key exists in the data dictionary. - * - * @param key the key to check - * @return true if the key exists, false otherwise - */ - public final boolean exists(String key) { - return this.data.containsKey(key); - } - - @Override - /** - * Checks if a key exists in the key descriptors. - * - * @param key the key to check - * @return true if the key exists in the key descriptors, false otherwise - */ - public final boolean existsInKeys(String key) { - return this.keyDescriptors.stream().filter(k -> k.getName().equals(key)).findAny().isPresent(); - } - - @Override - /** - * Returns all keys present in the data dictionary. - * - * @return an array of all keys - */ - public final String[] keys() { - Set setKeys = this.data.keySet(); - String[] keys = new String[setKeys.size()]; - - setKeys.toArray(keys); - - return keys; - } - - @Override - /** - * Adds a key to the data dictionary if it does not already exist. - * - * @param key the key to add - */ - public final void addKey(String key) { - if (key != null) { - this.data.putIfAbsent(key, null); - } - } - - @Override - /** - * Removes a key from the data dictionary. - * - * @param key the key to remove - */ - public final void removeKey(String key) { - if (key != null) { - this.data.remove(key); - } - } - - @Override - /** - * Retrieves the value associated with a key in the data dictionary. - * - * @param key the key whose value is to be returned - * @return the value associated with the key, or null if not present - */ - public final Object get(String key) { - return this.data.get(key); - } - - @Override - /** - * Sets the value for a key in the data dictionary. - * - * @param key the key to set - * @param value the value to associate with the key - * @throws DataDictionaryException if the key is not allowed or an error occurs - */ - public final void set(String key, Object value) throws DataDictionaryException { - if (key == null) { - throw new DataDictionaryException("Key null not allowed"); - } - - if (this.keyDescriptors.isEmpty()) { - this.data.put(key, value); - } else { - Optional> keyDescriptor = this.getKeyDescriptor(key); - if (keyDescriptor.isPresent()) { - try { - String setterName = this.getSetterName(key); - List methodNameList = - Arrays.asList(this.getClass().getDeclaredMethods()).stream() - .map(m -> m.getName()) - .collect(Collectors.toList()); - if (methodNameList.contains(setterName)) { - Method method = this.getClass().getDeclaredMethod(setterName, Object.class); - method.setAccessible(true); - method.invoke(this, value); - } else { - this.data.put(key, keyDescriptor.get().convert(value)); - } - } catch (NoSuchMethodException - | SecurityException - | IllegalAccessException - | IllegalArgumentException e) { - throw new DataDictionaryException( - "Set key " - + key - + " failed : " - + e.getClass().getSimpleName() - + " -> " - + e.getMessage()); - } catch (InvocationTargetException e) { - throw new DataDictionaryException( - "Set key " - + key - + " failed : " - + e.getTargetException().getClass().getSimpleName() - + " -> " - + e.getTargetException().getMessage()); - } - - } else { - throw new DataDictionaryException("Key " + key + " not allowed"); - } - } - } - - @Override - /** - * Copies all key-value pairs from another data dictionary into this one. - * - * @param other the data dictionary to copy from - * @throws DataDictionaryException if an error occurs during copying - */ - public void copy(DataDictionary other) throws DataDictionaryException { - String[] otherKeys = other.keys(); - - this.clear(); - for (int i = 0; i < otherKeys.length; i++) { - this.set(otherKeys[i], other.get(otherKeys[i])); - } - - this.checkAndUpdate(); - } - - @Override - /** Removes all key-value pairs from the data dictionary. */ - public final void clear() { - this.data.clear(); - } - - @Override - /** - * Returns the hash code value for this data dictionary. - * - * @return the hash code value - */ - public final int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((this.data == null) ? 0 : this.data.hashCode()); - return result; - } - - @Override - /** - * Compares this data dictionary to another object for equality. - * - * @param object the object to compare - * @return true if the objects are equal, false otherwise - */ - public final boolean equals(Object object) { - if (object == null) { - return false; - } - if (this == object) { - return true; - } - if (!(object instanceof DataDictionary)) { - return false; - } - DataDictionaryBase other = (DataDictionaryBase) object; - - if (this.data == null) { - if (other.data != null) { - return false; - } - } else { - if (!this.data.keySet().equals(other.data.keySet())) { - return false; - } - for (String key : this.data.keySet()) { - Object thisValue = this.data.get(key); - Object otherValue = other.data.get(key); - - if (thisValue == null) { - if (otherValue != null) { - return false; - } else { - continue; - } - } else if (otherValue == null) { - return false; - } - if (!thisValue.getClass().equals(otherValue.getClass())) { - if ((thisValue instanceof Number) && (otherValue instanceof Number)) { - double thisDoubleValue = ((Number) thisValue).doubleValue(); - double otherDoubleValue = ((Number) otherValue).doubleValue(); - return thisDoubleValue == otherDoubleValue; - } else { - return false; - } - } - if (thisValue.getClass().isArray()) { - if (thisValue instanceof boolean[]) { - if (!Arrays.equals((boolean[]) thisValue, (boolean[]) otherValue)) { - return false; - } - } else if (thisValue instanceof byte[]) { - if (!Arrays.equals((byte[]) thisValue, (byte[]) otherValue)) { - return false; - } - } else if (thisValue instanceof char[]) { - if (!Arrays.equals((char[]) thisValue, (char[]) otherValue)) { - return false; - } - } else if (thisValue instanceof short[]) { - if (!Arrays.equals((short[]) thisValue, (short[]) otherValue)) { - return false; - } - } else if (thisValue instanceof int[]) { - if (!Arrays.equals((int[]) thisValue, (int[]) otherValue)) { - return false; - } - } else if (thisValue instanceof long[]) { - if (!Arrays.equals((long[]) thisValue, (long[]) otherValue)) { - return false; - } - } else if (thisValue instanceof float[]) { - if (!Arrays.equals((float[]) thisValue, (float[]) otherValue)) { - return false; - } - } else if (thisValue instanceof double[]) { - if (!Arrays.equals((double[]) thisValue, (double[]) otherValue)) { - return false; - } - } else if (thisValue instanceof Object[]) { - if (!Arrays.equals((Object[]) thisValue, (Object[]) otherValue)) { - return false; - } - } - } else { - if (!thisValue.equals(otherValue)) { - return false; - } - } - } - } - - return true; - } - - @Override - /** - * Writes the data dictionary to a file as JSON. - * - * @param file the file to write to - * @param indentFactor the number of spaces to add to each level of indentation - * @throws IOException if an I/O error occurs - */ - public final void toFile(File file, int indentFactor) throws IOException { - OutputStream stream = new FileOutputStream(file); - - this.toStream(stream, indentFactor); - } - - @Override - /** - * Writes the data dictionary to an output stream as JSON. - * - * @param stream the output stream to write to - * @param indentFactor the number of spaces to add to each level of indentation - * @throws IOException if an I/O error occurs - */ - public final void toStream(OutputStream stream, int indentFactor) throws IOException { - String text = this.toString(indentFactor); - - stream.write(text.getBytes()); - } - - @Override - /** - * Serializes the data dictionary to a {@link JSONObject}. - * - * @return the JSON representation of the data dictionary - */ - public JSONObject toJSON() { - JSONObject jsonObject = new JSONObject(); - String[] keys = this.keys(); - - for (int i = 0; i < keys.length; i++) { - Object value = this.get(keys[i]); - if (value instanceof DataDictionaryBase) { - DataDictionaryBase dataDictionary = (DataDictionaryBase) value; - jsonObject.put(keys[i], dataDictionary.toJSON()); - } else if (value instanceof LocalDateTime) { - String textValue = - ((LocalDateTime) value).format(KeyDescriptorLocalDateTime.DEFAULT_FORMATTER); - jsonObject.put(keys[i], textValue); - } else { - jsonObject.put(keys[i], value); - } - } - - return jsonObject; - } - - @SuppressWarnings("unchecked") - @Override - /** - * Returns a shallow copy of the internal map representing the data dictionary. - * - * @return a map containing all key-value pairs - */ - public final Map toMap() { - return (Map) this.data.clone(); - } - - @Override - /** - * Returns a string representation of the data dictionary in JSON format. - * - * @return the string representation of the data dictionary - */ - public String toString() { - return this.toString(DataDictionaryBase.JSON_INDENT_FACTOR); - } - - @Override - /** - * Returns a string representation of the data dictionary in JSON format with indentation. - * - * @param identFactor the number of spaces to add to each level of indentation - * @return the string representation of the data dictionary - */ - public String toString(int identFactor) { - return this.toJSON().toString(identFactor); - } - - @Override - /** - * Creates and returns a deep copy of this data dictionary. - * - * @return a clone of this data dictionary - */ - public DataDictionaryBase clone() { - try { - Constructor constructor = this.getClass().getConstructor(DataDictionary.class); - return this.getClass().cast(constructor.newInstance(this)); - } catch (Exception e) { - throw new RuntimeException(e.getMessage(), e); - } - } - - @Override - /** Prints the data dictionary to the standard output in pretty JSON format. */ - public void print() { - System.out.println(this.toString(2)); - } - - /** - * Updates optional parameters and checks that all mandatory parameters are present. - * - * @throws DataDictionaryException if a mandatory parameter is missing or invalid - */ - protected final void checkAndUpdate() throws DataDictionaryException { - this.updateOptionalParameters(); - this.checkMandatoryParameters(); - } - - /** - * Checks that all mandatory parameters defined by key descriptors are present in the dictionary. - * - * @throws DataDictionaryException if a mandatory key is missing - */ - protected void checkMandatoryParameters() throws DataDictionaryException { - for (KeyDescriptor key : this.keyDescriptors) { - if (key.isMandatory() && !this.exists(key.getName())) { - throw new DataDictionaryException("Mandatory key " + key.getName() + " not defined"); - } - } - } - - /** - * Updates optional parameters in the data dictionary. Subclasses may override to provide custom - * logic. - * - * @throws DataDictionaryException if an error occurs during update - */ - protected void updateOptionalParameters() throws DataDictionaryException {} - - /** - * Adds a key descriptor to the list of key descriptors for this dictionary. - * - * @param keyDescriptor the key descriptor to add - * @throws DataDictionaryException if the key already exists - */ - protected void addKeyDescriptor(KeyDescriptor keyDescriptor) throws DataDictionaryException { - if (this.existsInKeys(keyDescriptor.getName())) { - throw new DataDictionaryException("Key " + keyDescriptor.getName() + "already exists"); - } - this.keyDescriptors.add(keyDescriptor); - } - - /** - * Adds all key descriptors from the provided list to this dictionary. - * - * @param keyDescriptor the list of key descriptors to add - * @throws DataDictionaryException if a key already exists - */ - protected void addAllKeyDescriptor(List> keyDescriptor) - throws DataDictionaryException { - for (KeyDescriptor key : keyDescriptor) { - this.addKeyDescriptor(key); - } - } - - /** - * Retrieves the key descriptor for the specified key name, if present. - * - * @param name the name of the key - * @return an {@link Optional} containing the key descriptor if found, or empty otherwise - */ - protected Optional> getKeyDescriptor(String name) { - return this.keyDescriptors.stream().filter(k -> k.getName().equals(name)).findFirst(); - } - - private String getSetterName(String key) { - return "set" + key.substring(0, 1).toUpperCase() + key.substring(1); - } - - private void init() { - this.keyDescriptors = new ArrayList>(); - this.data = new HashMap(); - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java deleted file mode 100644 index 40c9106..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * Interface for describing a key in a data dictionary structure. - * - *

    A key descriptor defines the name, type, and validation rules for a key in a {@link - * enedis.lab.types.DataDictionary}. Implementations may specify accepted values, conversion logic, - * and whether the key is mandatory. - * - * @param the type of value associated with the key - * @author Enedis Smarties team - */ -public interface KeyDescriptor { - - /** - * Returns the name of the key described by this descriptor. - * - * @return the key name (never null) - */ - String getName(); - - /** - * Indicates whether the key is mandatory in the data dictionary. - * - * @return true if the key is mandatory, false otherwise - */ - boolean isMandatory(); - - /** - * Sets whether the key is mandatory in the data dictionary. - * - * @param mandatory true to mark the key as mandatory, false otherwise - */ - void setMandatory(boolean mandatory); - - /** - * Sets the list of accepted values for this key. If not set, any value of type T may be accepted - * depending on implementation. - * - * @param acceptedValues the accepted values for this key - */ - @SuppressWarnings("unchecked") - void setAcceptedValues(T... acceptedValues); - - /** - * Converts an object to the value type T for this key. Implementations should validate and - * convert the input as needed. - * - * @param value the object to convert - * @return the converted value of type T - * @throws DataDictionaryException if the value cannot be converted or is invalid - */ - T convert(Object value) throws DataDictionaryException; - - /** - * Converts a value of type T to its string representation. - * - * @param value the value to convert to String - * @return the string representation of the value - */ - String toString(T value); -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java deleted file mode 100644 index 67d3df3..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; -import java.util.Arrays; -import java.util.List; - -/** - * Abstract base class for key descriptors in a data dictionary. - * - *

    Provides common logic for key name, mandatory flag, accepted values, and value - * conversion/validation. Subclasses must implement the type-specific conversion logic. - * - * @param the type of value associated with the key - * @author Enedis Smarties team - */ -public abstract class KeyDescriptorBase implements KeyDescriptor { - /** The name of the key described by this descriptor. */ - private String name; - - /** Whether the key is mandatory in the data dictionary. */ - private boolean mandatory; - - /** List of accepted values for this key, or null if any value is accepted. */ - protected List acceptedValues; - - /** - * Constructs a key descriptor with the given name and mandatory flag. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - */ - public KeyDescriptorBase(String name, boolean mandatory) { - super(); - this.name = name; - this.mandatory = mandatory; - } - - /** - * Converts an object to the value type T for this key, checking accepted values if set. - * - * @param value the object to convert - * @return the converted value of type T - * @throws DataDictionaryException if the value is null and mandatory, or not accepted, or cannot - * be converted - */ - @Override - public final T convert(Object value) throws DataDictionaryException { - T convertedValue = null; - - if (value == null) { - this.handleNullValue(); - return null; - } - - convertedValue = this.convertValue(value); - - this.checkAcceptedValues(convertedValue); - - return convertedValue; - } - - /** - * Returns the string representation of a value of type T. - * - * @param value the value to convert to String - * @return the string representation, or null if value is null - */ - @Override - public String toString(T value) { - if (value == null) { - return null; - } - - return value.toString(); - } - - /** - * Returns the name of the key described by this descriptor. - * - * @return the key name - */ - @Override - public String getName() { - return this.name; - } - - /** - * Sets the name of the key described by this descriptor. - * - * @param name the key name - */ - public void setName(String name) { - this.name = name; - } - - /** - * Indicates whether the key is mandatory in the data dictionary. - * - * @return true if the key is mandatory, false otherwise - */ - @Override - public boolean isMandatory() { - return this.mandatory; - } - - /** - * Sets whether the key is mandatory in the data dictionary. - * - * @param mandatory true to mark the key as mandatory, false otherwise - */ - @Override - public void setMandatory(boolean mandatory) { - this.mandatory = mandatory; - } - - /** - * Sets the list of accepted values for this key. - * - * @param acceptedValues the accepted values for this key - */ - @SuppressWarnings("unchecked") - @Override - public void setAcceptedValues(T... acceptedValues) { - this.acceptedValues = Arrays.asList(acceptedValues); - } - - /** - * Converts an object to the value type T for this key (type-specific logic). Subclasses must - * implement this method. - * - * @param value the object to convert - * @return the converted value of type T - * @throws DataDictionaryException if the value cannot be converted - */ - protected abstract T convertValue(Object value) throws DataDictionaryException; - - /** - * Handles the case where a null value is set for this key. Throws an exception if the key is - * mandatory. - * - * @throws DataDictionaryException if the key is mandatory - */ - protected final void handleNullValue() throws DataDictionaryException { - if (this.isMandatory()) { - throw new DataDictionaryException("Cannot set null " + this.getName()); - } - } - - /** - * Checks if the given value is among the accepted values for this key, if any are set. Throws an - * exception if the value is not accepted. - * - * @param value the value to check - * @throws DataDictionaryException if the value is not accepted - */ - protected void checkAcceptedValues(T value) throws DataDictionaryException { - if (this.acceptedValues != null) { - boolean accepted = false; - - for (T v : this.acceptedValues) { - if (v.equals(value)) { - accepted = true; - break; - } - } - - if (!accepted) { - throw new DataDictionaryException( - "Key " + this.getName() + " doesn't respect mandatory value"); - } - } - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java deleted file mode 100644 index 80de482..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.Map; - -/** - * Key descriptor for values that are themselves data dictionaries. - * - *

    This descriptor allows a key to be associated with a nested {@link DataDictionaryBase} value. - * It supports conversion from strings, maps, and other data dictionary instances to the target - * type. - * - * @param the type of data dictionary accepted as value - * @author Enedis Smarties team - */ -public class KeyDescriptorDataDictionary - extends KeyDescriptorBase { - /** The class of the data dictionary accepted as value for this key. */ - private Class dataDictionaryClass; - - /** - * Constructs a key descriptor for a data dictionary value. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - * @param dataDictionaryClass the class of the data dictionary accepted as value - */ - public KeyDescriptorDataDictionary(String name, boolean mandatory, Class dataDictionaryClass) { - super(name, mandatory); - this.dataDictionaryClass = dataDictionaryClass; - } - - /** - * Converts an object to the data dictionary type T for this key. Accepts instances of T, strings - * (parsed as JSON), other DataDictionary, or Map. - * - * @param value the object to convert - * @return the converted data dictionary value - * @throws DataDictionaryException if the value cannot be converted - */ - @SuppressWarnings("unchecked") - @Override - public T convertValue(Object value) throws DataDictionaryException { - T convertedValue = null; - - if (this.dataDictionaryClass.isAssignableFrom(value.getClass())) { - convertedValue = this.dataDictionaryClass.cast(value); - } else if (value instanceof String) { - convertedValue = (T) DataDictionaryBase.fromString((String) value, this.dataDictionaryClass); - } else if (value instanceof DataDictionary) { - try { - Constructor constructor = this.dataDictionaryClass.getConstructor(DataDictionary.class); - convertedValue = constructor.newInstance((DataDictionary) value); - } catch (InvocationTargetException e) { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": Cannot convert type " - + value.getClass().getSimpleName() - + " to " - + this.dataDictionaryClass.getSimpleName() - + " : " - + e.getTargetException().getMessage()); - } catch (NoSuchMethodException - | SecurityException - | InstantiationException - | IllegalAccessException - | IllegalArgumentException e) { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": Cannot convert type " - + value.getClass().getSimpleName() - + " to " - + this.dataDictionaryClass.getSimpleName() - + " : " - + e.getMessage()); - } - } else if (value instanceof Map) { - try { - Constructor constructor = this.dataDictionaryClass.getConstructor(Map.class); - convertedValue = constructor.newInstance((Map) value); - } catch (InvocationTargetException e) { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": Cannot convert type " - + value.getClass().getSimpleName() - + " to " - + this.dataDictionaryClass.getSimpleName() - + " : " - + e.getTargetException().getMessage()); - } catch (NoSuchMethodException - | SecurityException - | InstantiationException - | IllegalAccessException - | IllegalArgumentException e) { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": Cannot convert type " - + value.getClass().getSimpleName() - + " to " - + this.dataDictionaryClass.getSimpleName() - + " : " - + e.getMessage()); - } - } else { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": Cannot convert type " - + value.getClass().getSimpleName() - + " to " - + this.dataDictionaryClass.getSimpleName()); - } - - return convertedValue; - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java deleted file mode 100644 index 1e1717b..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * Key descriptor for values that are Java enums. - * - *

    This descriptor allows a key to be associated with an enum value, supporting conversion from - * strings and type-safe validation. Optionally, a prefix can be added to the string value before - * conversion. - * - * @param the enum type accepted as value - * @author Enedis Smarties team - */ -@SuppressWarnings("rawtypes") -public class KeyDescriptorEnum extends KeyDescriptorBase { - /** The enum class accepted as value for this key. */ - private Class enumClass; - - /** Optional prefix to prepend to the string value before conversion. */ - private String prefix; - - /** - * Constructs a key descriptor for an enum value with no prefix. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - * @param enumClass the enum class accepted as value - */ - public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass) { - this(name, mandatory, enumClass, ""); - } - - /** - * Constructs a key descriptor for an enum value with a prefix. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - * @param enumClass the enum class accepted as value - * @param prefix the prefix to prepend to the string value before conversion - */ - public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass, String prefix) { - super(name, mandatory); - this.enumClass = enumClass; - this.prefix = prefix; - } - - /** - * Converts an object to the enum type T for this key. Accepts instances of T or strings - * (converted to enum constant). - * - * @param value the object to convert - * @return the converted enum value - * @throws DataDictionaryException if the value cannot be converted - */ - @Override - public T convertValue(Object value) throws DataDictionaryException { - T convertedValue = null; - - if (this.enumClass.isAssignableFrom(value.getClass())) { - convertedValue = this.enumClass.cast(value); - } else if (value instanceof String) { - convertedValue = this.toEnum((String) value); - } else { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": Cannot convert type " - + value.getClass().getSimpleName() - + " to " - + this.enumClass.getSimpleName()); - } - - return convertedValue; - } - - /** - * Converts a string to the enum type T, applying the prefix if set. The string is uppercased and - * dots/hyphens are replaced with underscores. - * - * @param value the string value to convert - * @return the corresponding enum constant - * @throws DataDictionaryException if the value does not match any enum constant - */ - @SuppressWarnings("unchecked") - protected T toEnum(String value) throws DataDictionaryException { - T convertedValue = null; - try { - String preparedValue = this.prefix + value.toUpperCase().replace(".", "_").replace("-", "_"); - convertedValue = (T) Enum.valueOf(this.enumClass, preparedValue); - } catch (IllegalArgumentException e) { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": " - + this.enumClass.getSimpleName() - + " doesn't contain " - + value); - } - return convertedValue; - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java deleted file mode 100644 index baf6ab3..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.json.JSONObject; - -/** - * Key descriptor for values that are lists of a specific type. - * - *

    This descriptor allows a key to be associated with a list of items, supporting conversion from - * arrays, lists, maps, and JSON objects. It handles type conversion for each item, including - * support for nested data dictionaries and enums. - * - * @param the type of item in the list - * @author Enedis Smarties team - */ -public class KeyDescriptorList extends KeyDescriptorBase> { - /** The class of the item accepted in the list for this key. */ - private Class itemClass; - - /** - * Constructs a key descriptor for a list value. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - * @param itemClass the class of the item accepted in the list - */ - public KeyDescriptorList(String name, boolean mandatory, Class itemClass) { - super(name, mandatory); - this.itemClass = itemClass; - } - - /** - * Converts an object to a list of items of type T for this key. Accepts single items, lists, - * arrays, or maps, and converts each item as needed. - * - * @param value the object to convert - * @return the converted list of items - * @throws DataDictionaryException if the value or any item cannot be converted - */ - @Override - public List convertValue(Object value) throws DataDictionaryException { - List convertedValue = new ArrayList(); - - if (this.itemClass.isAssignableFrom(value.getClass())) { - T convertedItem = this.convertItem(value); - convertedValue.add(convertedItem); - } else if (value instanceof HashMap) { - T convertedItem = this.convertItem(value); - convertedValue.add(convertedItem); - } else if (value instanceof List) { - for (Object item : (List) value) { - T convertedItem = this.convertItem(item); - convertedValue.add(convertedItem); - } - } else if (value instanceof Object[]) { - for (Object item : (Object[]) value) { - T convertedItem = this.convertItem(item); - convertedValue.add(convertedItem); - } - } else { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": Cannot convert type " - + value.getClass().getSimpleName() - + " to List<" - + this.itemClass.getSimpleName() - + ">"); - } - return convertedValue; - } - - /** - * Converts a single item to the type T, handling data dictionaries and enums. - * - * @param item the item to convert - * @return the converted item - * @throws DataDictionaryException if the item cannot be converted - */ - private T convertItem(Object item) throws DataDictionaryException { - T convertedItem = null; - if (this.itemClass.isAssignableFrom(item.getClass())) { - convertedItem = this.itemClass.cast(item); - } else if (DataDictionary.class.isAssignableFrom(this.itemClass)) { - convertedItem = this.convertDataDictionaryItem(item); - } else if (Enum.class.isAssignableFrom(this.itemClass)) { - convertedItem = this.convertEnumItem(item); - } else { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": at least on item isn't a " - + this.itemClass.getSimpleName() - + ", type received :" - + item.getClass().getSimpleName()); - } - return convertedItem; - } - - /** - * Converts an item to a data dictionary of type T, using a Map or JSONObject. - * - * @param item the item to convert - * @return the converted data dictionary item - * @throws DataDictionaryException if the item cannot be converted - */ - private T convertDataDictionaryItem(Object item) throws DataDictionaryException { - T convertedItem = null; - if (item instanceof JSONObject) { - JSONObject itemJsonObject = (JSONObject) item; - try { - Constructor constructor = this.itemClass.getConstructor(Map.class); - convertedItem = (T) constructor.newInstance(itemJsonObject.toMap()); - } catch (SecurityException - | InstantiationException - | IllegalAccessException - | IllegalArgumentException - | NoSuchMethodException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - } else if (item instanceof Map) { - try { - Constructor constructor = this.itemClass.getConstructor(Map.class); - convertedItem = (T) constructor.newInstance((Map) item); - } catch (SecurityException - | InstantiationException - | IllegalAccessException - | IllegalArgumentException - | NoSuchMethodException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - } else { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": at least on item isn't a JSONObject, type received :" - + item.getClass().getSimpleName()); - } - return convertedItem; - } - - /** - * Converts an item to an enum of type T, using the valueOf method. - * - * @param item the item to convert (must be a String) - * @return the converted enum item - * @throws DataDictionaryException if the item cannot be converted - */ - @SuppressWarnings("unchecked") - private T convertEnumItem(Object item) throws DataDictionaryException { - T convertedItem = null; - if (item instanceof String) { - String itemString = (String) item; - try { - Method valueOf = this.itemClass.getMethod("valueOf", String.class); - convertedItem = (T) valueOf.invoke(null, itemString.toUpperCase()); - } catch (SecurityException - | IllegalAccessException - | IllegalArgumentException - | NoSuchMethodException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - } else { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": at least on item isn't a JSONObject, type received :" - + item.getClass().getSimpleName()); - } - return convertedItem; - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java deleted file mode 100644 index 84c6210..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.MinMaxChecker; -import java.util.List; - -/** - * Key descriptor for list values with minimum and maximum size constraints. - * - *

    This descriptor extends {@link KeyDescriptorList} to enforce that the list size is within - * specified bounds. It uses a {@link MinMaxChecker} to validate the size after conversion. - * - * @param the type of item in the list - * @author Enedis Smarties team - */ -public class KeyDescriptorListMinMaxSize extends KeyDescriptorList { - /** Utility for checking minimum and maximum list size constraints. */ - private MinMaxChecker minMaxChecker; - - /** - * Constructs a key descriptor for a list value with no size constraints. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - * @param itemClass the class of the item accepted in the list - */ - public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass) { - this(name, mandatory, itemClass, null, null); - } - - /** - * Constructs a key descriptor for a list value with minimum and maximum size constraints. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - * @param itemClass the class of the item accepted in the list - * @param min the minimum allowed list size (nullable) - * @param max the maximum allowed list size (nullable) - */ - public KeyDescriptorListMinMaxSize( - String name, boolean mandatory, Class itemClass, Integer min, Integer max) { - super(name, mandatory, itemClass); - this.minMaxChecker = new MinMaxChecker(min, max); - } - - /** - * Converts an object to a list of items of type T and checks the list size constraints. - * - * @param value the object to convert - * @return the converted list of items - * @throws DataDictionaryException if the value or any item cannot be converted, or if the list - * size is out of bounds - */ - @Override - public List convertValue(Object value) throws DataDictionaryException { - List convertedValue = super.convertValue(value); - this.check(convertedValue); - return convertedValue; - } - - /** - * Returns the minimum allowed list size, or null if not set. - * - * @return the minimum allowed list size - */ - public Integer getMin() { - return this.minMaxChecker.getMin().intValue(); - } - - /** - * Sets the minimum allowed list size. - * - * @param min the minimum allowed list size - * @throws IllegalArgumentException if min is greater than max - */ - public void setMin(Integer min) { - this.minMaxChecker.setMin(min); - } - - /** - * Returns the maximum allowed list size, or null if not set. - * - * @return the maximum allowed list size - */ - public Integer getMax() { - return this.minMaxChecker.getMax().intValue(); - } - - /** - * Sets the maximum allowed list size. - * - * @param max the maximum allowed list size - * @throws IllegalArgumentException if max is smaller than min - */ - public void setMax(Integer max) { - this.minMaxChecker.setMax(max); - } - - /** - * Checks that the list size is within the allowed bounds. - * - * @param list the list to check - * @throws DataDictionaryException if the list size is out of bounds - */ - private void check(List list) throws DataDictionaryException { - if (list != null) { - if (!this.minMaxChecker.check(list.size())) { - throw new DataDictionaryException( - "Key " + this.getName() + ": value size (" + list.size() + ") out of bound"); - } - } - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java deleted file mode 100644 index 4af8812..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; - -/** - * Key descriptor for values that are {@link LocalDateTime} instances. - * - *

    This descriptor allows a key to be associated with a date-time value, supporting conversion - * from strings using a configurable pattern. It provides default formatting and parsing logic for - * date-time values. - * - * @author Enedis Smarties team - */ -public class KeyDescriptorLocalDateTime extends KeyDescriptorBase { - /** Default date-time pattern used for formatting and parsing. */ - public static final String DEFAULT_PATTERN = "dd/MM/yyyy HH:mm:ss"; - - /** Default formatter used for date-time values. */ - public static final DateTimeFormatter DEFAULT_FORMATTER = - DateTimeFormatter.ofPattern(DEFAULT_PATTERN); - - /** Formatter used for this key descriptor. */ - private DateTimeFormatter formatter; - - /** - * Constructs a key descriptor for a date-time value using the default pattern. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - */ - public KeyDescriptorLocalDateTime(String name, boolean mandatory) { - this(name, mandatory, DEFAULT_PATTERN); - } - - /** - * Constructs a key descriptor for a date-time value using a custom pattern. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - * @param formatterPattern the date-time pattern to use for formatting and parsing - */ - public KeyDescriptorLocalDateTime(String name, boolean mandatory, String formatterPattern) { - super(name, mandatory); - this.formatter = DateTimeFormatter.ofPattern(formatterPattern); - } - - /** - * Converts a {@link LocalDateTime} value to its string representation using the configured - * formatter. - * - * @param value the date-time value to convert - * @return the formatted string, or null if value is null - */ - @Override - public String toString(LocalDateTime value) { - if (value == null) { - return null; - } - return this.formatter.format(value); - } - - /** - * Converts an object to a {@link LocalDateTime} value for this key. Accepts {@link LocalDateTime} - * instances or strings (parsed using the configured formatter). - * - * @param value the object to convert - * @return the converted date-time value - * @throws DataDictionaryException if the value cannot be converted - */ - @Override - public LocalDateTime convertValue(Object value) throws DataDictionaryException { - LocalDateTime convertedValue = null; - if (value instanceof LocalDateTime) { - convertedValue = (LocalDateTime) value; - } else if (value instanceof String) { - convertedValue = this.toLocalDateTime((String) value); - } else { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": Cannot convert type " - + value.getClass().getSimpleName() - + " to LocalDateTime"); - } - return convertedValue; - } - - /** - * Parses a string to a {@link LocalDateTime} using the configured formatter. - * - * @param value the string to parse - * @return the parsed date-time value - * @throws DataDictionaryException if the string cannot be parsed - */ - private LocalDateTime toLocalDateTime(String value) throws DataDictionaryException { - LocalDateTime convertedValue = null; - try { - convertedValue = LocalDateTime.parse(value, this.formatter); - } catch (DateTimeParseException e) { - throw new DataDictionaryException( - "Key " + this.getName() + ": string " + value + " cannot be converted to LocalDateTime"); - } - return convertedValue; - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java deleted file mode 100644 index cda1166..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * Key descriptor for values that are {@link Number} instances. - * - *

    This descriptor allows a key to be associated with a numeric value, supporting conversion from - * strings and type-safe validation. It provides logic for accepted values and conversion from - * string representations. - * - * @author Enedis Smarties team - */ -public class KeyDescriptorNumber extends KeyDescriptorBase { - /** - * Constructs a key descriptor for a numeric value. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - */ - public KeyDescriptorNumber(String name, boolean mandatory) { - super(name, mandatory); - } - - /** - * Converts an object to a {@link Number} value for this key. Accepts {@link Number} instances or - * strings (parsed as double). - * - * @param value the object to convert - * @return the converted number value - * @throws DataDictionaryException if the value cannot be converted - */ - @Override - public Number convertValue(Object value) throws DataDictionaryException { - Number convertedValue = null; - - if (value instanceof Number) { - convertedValue = (Number) value; - } else if (value instanceof String) { - convertedValue = this.toNumber((String) value); - } else { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": Cannot convert type " - + value.getClass().getSimpleName() - + " to Number"); - } - - return convertedValue; - } - - /** - * Checks if the given value is among the accepted values for this key, if any are set. Uses - * double value comparison for numeric equality. - * - * @param value the value to check - * @throws DataDictionaryException if the value is not accepted - */ - @Override - protected void checkAcceptedValues(Number value) throws DataDictionaryException { - if (this.acceptedValues != null) { - boolean accepted = false; - - for (Number v : this.acceptedValues) { - if (v.doubleValue() == value.doubleValue()) { - accepted = true; - break; - } - } - - if (!accepted) { - throw new DataDictionaryException( - "Key " + this.getName() + " doesn't respect mandatory value"); - } - } - } - - /** - * Converts a string to a {@link Number} (as double). - * - * @param value the string to convert - * @return the parsed number - * @throws DataDictionaryException if the string cannot be parsed as a number - */ - private Number toNumber(String value) throws DataDictionaryException { - Number out = null; - try { - out = Double.valueOf((String) value); - } catch (NumberFormatException e) { - throw new DataDictionaryException( - "Key " - + this.getName() - + ": Cannot convert type " - + value.getClass().getSimpleName() - + " to Number"); - } - return out; - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java deleted file mode 100644 index c5fc481..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.MinMaxChecker; - -/** - * Key descriptor for numeric values with minimum and maximum constraints. - * - *

    This descriptor extends {@link KeyDescriptorNumber} to enforce that the value is within - * specified bounds. It uses a {@link MinMaxChecker} to validate the value after conversion. - * - * @author Enedis Smarties team - */ -public class KeyDescriptorNumberMinMax extends KeyDescriptorNumber { - /** Utility for checking minimum and maximum value constraints. */ - private MinMaxChecker minMaxChecker; - - /** - * Constructs a key descriptor for a numeric value with no min/max constraints. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - */ - public KeyDescriptorNumberMinMax(String name, boolean mandatory) { - this(name, mandatory, null, null); - } - - /** - * Constructs a key descriptor for a numeric value with minimum and maximum constraints. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - * @param min the minimum allowed value (nullable) - * @param max the maximum allowed value (nullable) - */ - public KeyDescriptorNumberMinMax(String name, boolean mandatory, Number min, Number max) { - super(name, mandatory); - this.minMaxChecker = new MinMaxChecker(min, max); - } - - /** - * Converts an object to a {@link Number} value and checks the min/max constraints. - * - * @param value the object to convert - * @return the converted number value - * @throws DataDictionaryException if the value cannot be converted or is out of bounds - */ - @Override - public Number convertValue(Object value) throws DataDictionaryException { - Number convertedValue = super.convertValue(value); - this.check(convertedValue); - return convertedValue; - } - - /** - * Returns the minimum allowed value, or null if not set. - * - * @return the minimum allowed value - */ - public Number getMin() { - return this.minMaxChecker.getMin(); - } - - /** - * Sets the minimum allowed value. - * - * @param min the minimum allowed value - * @throws IllegalArgumentException if min is greater than max - */ - public void setMin(Number min) { - this.minMaxChecker.setMin(min); - } - - /** - * Returns the maximum allowed value, or null if not set. - * - * @return the maximum allowed value - */ - public Number getMax() { - return this.minMaxChecker.getMax(); - } - - /** - * Sets the maximum allowed value. - * - * @param max the maximum allowed value - * @throws IllegalArgumentException if max is smaller than min - */ - public void setMax(Number max) { - this.minMaxChecker.setMax(max); - } - - /** - * Checks that the value is within the allowed bounds. - * - * @param convertedValue the value to check - * @throws DataDictionaryException if the value is out of bounds - */ - private void check(Number convertedValue) throws DataDictionaryException { - if (convertedValue != null) { - if (!this.minMaxChecker.check(convertedValue)) { - throw new DataDictionaryException( - "Key " + this.getName() + ": value (" + convertedValue + ") out of bound"); - } - } - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java deleted file mode 100644 index d58da9b..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * Key descriptor for values that are strings. - * - *

    This descriptor allows a key in a data dictionary to be associated with a string value, with - * optional control over whether empty strings are allowed. Provides conversion and validation logic - * for string values. - * - * @author Enedis Smarties team - */ -public class KeyDescriptorString extends KeyDescriptorBase { - /** Default flag indicating whether empty strings are allowed. */ - private static final boolean DEFAULT_EMPTY_ALLOW_FLAG = true; - - /** Indicates whether empty strings are allowed for this key. */ - private boolean emptyAllow; - - /** - * Constructs a string key descriptor with the given name and mandatory flag. - * - *

    By default, empty strings are allowed. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - */ - public KeyDescriptorString(String name, boolean mandatory) { - this(name, mandatory, DEFAULT_EMPTY_ALLOW_FLAG); - } - - /** - * Constructs a string key descriptor with the given name, mandatory flag, and empty string - * allowance. - * - * @param name the key name (must not be null) - * @param mandatory true if the key is mandatory, false otherwise - * @param emptyAllow true if empty strings are allowed, false otherwise - */ - public KeyDescriptorString(String name, boolean mandatory, boolean emptyAllow) { - super(name, mandatory); - this.emptyAllow = emptyAllow; - } - - /** - * Converts the given object to a string value for this key, applying validation rules. - * - *

    The value is converted using {@code toString()} and checked for emptiness if not allowed. - * - * @param value the object to convert - * @return the converted string value - * @throws DataDictionaryException if the value is empty and empty strings are not allowed - */ - @Override - public String convertValue(Object value) throws DataDictionaryException { - String convertedValue = value.toString(); - this.check(convertedValue); - return convertedValue; - } - - /** - * Returns whether empty strings are allowed for this key. - * - * @return true if empty strings are allowed, false otherwise - */ - public boolean isEmptyAllow() { - return this.emptyAllow; - } - - /** - * Sets whether empty strings are allowed for this key. - * - * @param emptyAllow true to allow empty strings, false to disallow - */ - public void setEmptyAllow(boolean emptyAllow) { - this.emptyAllow = emptyAllow; - } - - /** - * Checks if the given string value is valid according to the empty string allowance. - * - * @param value the string value to check - * @throws DataDictionaryException if the value is empty and empty strings are not allowed - */ - private void check(String value) throws DataDictionaryException { - if (!this.emptyAllow && value.trim().isEmpty()) { - throw new DataDictionaryException("Key " + this.getName() + ": value can't be empty"); - } - } -} diff --git a/src/main/java/enedis/lab/util/MinMaxChecker.java b/src/main/java/enedis/lab/util/MinMaxChecker.java deleted file mode 100644 index f77745b..0000000 --- a/src/main/java/enedis/lab/util/MinMaxChecker.java +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util; - -/** - * Utility class for checking if values are within a specified numeric range. - * - *

    This class provides methods to set minimum and maximum bounds and to check whether a given - * value falls within the defined range. It is designed for general-purpose range validation. - * - *

    Common use cases include: - * - *

      - *
    • Validating input values against numeric constraints - *
    • Ensuring values remain within allowed limits - *
    • Supporting configuration or parameter validation - *
    - * - * @author Enedis Smarties team - */ -public class MinMaxChecker { - private Number min; - private Number max; - - /** Default constructor */ - public MinMaxChecker() { - super(); - } - - /** - * Constructor setting parameters - * - * @param min - * @param max - */ - public MinMaxChecker(Number min, Number max) { - this(); - this.setMin(min); - this.setMax(max); - } - - /** - * Get min - * - * @return min - */ - public Number getMin() { - return this.min; - } - - /** - * Set min - * - * @param min - * @throws IllegalArgumentException if min is greater than max - */ - public void setMin(Number min) { - if (min != null) { - if (this.max != null && min.doubleValue() > this.max.doubleValue()) { - throw new IllegalArgumentException("min can't be greater than max"); - } - } - this.min = min; - } - - /** - * Get max - * - * @return max - */ - public Number getMax() { - return this.max; - } - - /** - * Set max - * - * @param max - * @throws IllegalArgumentException if max is smaller than min - */ - public void setMax(Number max) { - if (max != null) { - if (this.min != null && max.doubleValue() < this.min.doubleValue()) { - throw new IllegalArgumentException("max can't be smaller than min"); - } - } - this.max = max; - } - - /** - * Check the given value - * - * @param value - * @return true if the value is in [min, max] - */ - public boolean check(Number value) { - if (value != null) { - if (this.min != null) { - if (value.doubleValue() < this.min.doubleValue()) { - return false; - } - } - if (this.max != null) { - if (value.doubleValue() > this.max.doubleValue()) { - return false; - } - } - return true; - } else { - return false; - } - } -} diff --git a/src/main/java/enedis/lab/util/SystemError.java b/src/main/java/enedis/lab/util/SystemError.java deleted file mode 100644 index a61612e..0000000 --- a/src/main/java/enedis/lab/util/SystemError.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util; - -import com.sun.jna.Native; -import com.sun.jna.platform.win32.Kernel32Util; -import org.apache.commons.lang3.SystemUtils; - -/** - * Utility class for retrieving system error codes and messages. - * - *

    This class provides static methods to obtain the last system error code and its associated - * message, supporting multiple operating systems. It is designed for general-purpose error handling - * and diagnostics. - * - *

    Common use cases include: - * - *

      - *
    • Retrieving the last system error code - *
    • Obtaining human-readable error messages - *
    • Supporting cross-platform error diagnostics - *
    - * - * @author Enedis Smarties team - */ -public class SystemError { - /** - * Get system last error code - * - * @return System last error code - */ - public static int getCode() { - return Native.getLastError(); - } - - /** - * Get system last error message - * - * @return System last error message - */ - public static String getMessage() { - return getMessage(getCode()); - } - - /** - * Get system error message associated with code - * - * @param code the system error code - * @return System error message - */ - public static String getMessage(int code) { - String message = null; - - if (SystemUtils.IS_OS_WINDOWS) { - message = Kernel32Util.formatMessage(code); - } else if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_UNIX || SystemUtils.IS_OS_MAC_OSX) { - message = SystemLibC.INSTANCE.strerror(code); - } - - return message; - } -} diff --git a/src/main/java/enedis/lab/util/SystemLibC.java b/src/main/java/enedis/lab/util/SystemLibC.java deleted file mode 100644 index db83761..0000000 --- a/src/main/java/enedis/lab/util/SystemLibC.java +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util; - -import com.sun.jna.Library; -import com.sun.jna.Native; - -/** - * Interface for accessing native C library functions via JNA. - * - *

    This interface provides access to system-level C library functions, such as retrieving error - * messages associated with error codes. It is designed for general-purpose native integration and - * diagnostics. - * - *

    Common use cases include: - * - *

      - *
    • Obtaining human-readable error messages from system error codes - *
    • Integrating with native system libraries - *
    • Supporting cross-platform diagnostics - *
    - * - * @author Enedis Smarties team - */ -public interface SystemLibC extends Library { - /** Instance */ - SystemLibC INSTANCE = Native.load("c", SystemLibC.class); - - /** - * Get string error from code - * - * @param code - * @return string error - */ - public String strerror(int code); -} diff --git a/src/main/java/enedis/lab/util/message/Event.java b/src/main/java/enedis/lab/util/message/Event.java deleted file mode 100644 index 1fb7b02..0000000 --- a/src/main/java/enedis/lab/util/message/Event.java +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Abstract base class for event messages. - * - *

    This class represents event messages. It provides support for event-specific fields such as - * date/time and enforces the accepted message type. Subclasses can define additional event data and - * behavior. - * - *

    Common use cases include: - * - *

      - *
    • Representing system or application events - *
    • Handling event notifications in the message pipeline - *
    • Extending for custom event types - *
    - * - * @author Enedis Smarties team - * @see Message - */ -public abstract class Event extends Message { - protected static final String KEY_DATE_TIME = "dateTime"; - - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.EVENT; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorLocalDateTime kDateTime; - - protected Event() { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Event(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Event(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param dateTime - * @throws DataDictionaryException - */ - public Event(String name, LocalDateTime dateTime) throws DataDictionaryException { - this(); - - this.setName(name); - this.setDateTime(dateTime); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_TYPE)) { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - /** - * Get date time - * - * @return the date time - */ - public LocalDateTime getDateTime() { - return (LocalDateTime) this.data.get(KEY_DATE_TIME); - } - - /** - * Set date time - * - * @param dateTime - * @throws DataDictionaryException - */ - public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException { - this.setDateTime((Object) dateTime); - } - - protected void setDateTime(Object dateTime) throws DataDictionaryException { - this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); - } - - private void loadKeyDescriptors() { - try { - this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); - this.keys.add(this.kDateTime); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/Message.java b/src/main/java/enedis/lab/util/message/Message.java deleted file mode 100644 index bdaf81f..0000000 --- a/src/main/java/enedis/lab/util/message/Message.java +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorString; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Base class for all messages. - * - *

    This class provides the core structure for messages, including type and name fields. It - * supports construction from maps and data dictionaries, and can be extended for specific message - * types such as requests, responses, and events. - * - *

    Common use cases include: - * - *

      - *
    • Representing generic messages in the communication pipeline - *
    • Providing a base for request, response, and event messages - *
    • Supporting extensible message structures - *
    - * - * @author Enedis Smarties team - */ -public class Message extends DataDictionaryBase { - /** Key type */ - public static final String KEY_TYPE = "type"; - - /** Key name */ - public static final String KEY_NAME = "name"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kType; - protected KeyDescriptorString kName; - - protected Message() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Message(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Message(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @throws DataDictionaryException - */ - public Message(MessageType type, String name) throws DataDictionaryException { - this(); - - this.setType(type); - this.setName(name); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - super.updateOptionalParameters(); - } - - /** - * Get type - * - * @return the type - */ - public MessageType getType() { - return (MessageType) this.data.get(KEY_TYPE); - } - - /** - * Get name - * - * @return the name - */ - public String getName() { - return (String) this.data.get(KEY_NAME); - } - - /** - * Set type - * - * @param type - * @throws DataDictionaryException - */ - public void setType(MessageType type) throws DataDictionaryException { - this.setType((Object) type); - } - - /** - * Set name - * - * @param name - * @throws DataDictionaryException - */ - public void setName(String name) throws DataDictionaryException { - this.setName((Object) name); - } - - protected void setType(Object type) throws DataDictionaryException { - this.data.put(KEY_TYPE, this.kType.convert(type)); - } - - protected void setName(Object name) throws DataDictionaryException { - this.data.put(KEY_NAME, this.kName.convert(name)); - } - - private void loadKeyDescriptors() { - try { - this.kType = new KeyDescriptorEnum(KEY_TYPE, true, MessageType.class); - this.keys.add(this.kType); - - this.kName = new KeyDescriptorString(KEY_NAME, true, false); - this.keys.add(this.kName); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/Request.java b/src/main/java/enedis/lab/util/message/Request.java deleted file mode 100644 index 483d322..0000000 --- a/src/main/java/enedis/lab/util/message/Request.java +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Abstract base class for request messages. - * - *

    This class represents request messages. It enforces the accepted message type and provides - * support for request-specific fields. Subclasses can define additional request data and behavior. - * - *

    Common use cases include: - * - *

      - *
    • Representing client requests for data or actions - *
    • Handling request validation and processing - *
    • Extending for custom request types - *
    - * - * @author Enedis Smarties team - * @see Message - */ -public abstract class Request extends Message { - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.REQUEST; - - private List> keys = new ArrayList>(); - - protected Request() { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Request(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Request(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @throws DataDictionaryException - */ - public Request(MessageType type, String name) throws DataDictionaryException { - this(); - - this.setType(type); - this.setName(name); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_TYPE)) { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - private void loadKeyDescriptors() { - try { - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/Response.java b/src/main/java/enedis/lab/util/message/Response.java deleted file mode 100644 index 1f5274d..0000000 --- a/src/main/java/enedis/lab/util/message/Response.java +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Abstract base class for response messages. - * - *

    This class represents response messages. It provides support for response-specific fields such - * as date/time, error code, and error message, and enforces the accepted message type. Subclasses - * can define additional response data and behavior. - * - *

    Common use cases include: - * - *

      - *
    • Representing server responses to client requests - *
    • Handling error reporting and status information - *
    • Extending for custom response types - *
    - * - * @author Enedis Smarties team - * @see Message - */ -public abstract class Response extends Message { - protected static final String KEY_DATE_TIME = "dateTime"; - protected static final String KEY_ERROR_CODE = "errorCode"; - protected static final String KEY_ERROR_MESSAGE = "errorMessage"; - - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.RESPONSE; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorLocalDateTime kDateTime; - protected KeyDescriptorNumber kErrorCode; - protected KeyDescriptorString kErrorMessage; - - protected Response() { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Response(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Response(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public Response( - MessageType type, String name, LocalDateTime dateTime, Number errorCode, String errorMessage) - throws DataDictionaryException { - this(); - - this.setType(type); - this.setName(name); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_TYPE)) { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - /** - * Get date time - * - * @return the date time - */ - public LocalDateTime getDateTime() { - return (LocalDateTime) this.data.get(KEY_DATE_TIME); - } - - /** - * Get error code - * - * @return the error code - */ - public Number getErrorCode() { - return (Number) this.data.get(KEY_ERROR_CODE); - } - - /** - * Get error message - * - * @return the error message - */ - public String getErrorMessage() { - return (String) this.data.get(KEY_ERROR_MESSAGE); - } - - /** - * Set date time - * - * @param dateTime - * @throws DataDictionaryException - */ - public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException { - this.setDateTime((Object) dateTime); - } - - /** - * Set error code - * - * @param errorCode - * @throws DataDictionaryException - */ - public void setErrorCode(Number errorCode) throws DataDictionaryException { - this.setErrorCode((Object) errorCode); - } - - /** - * Set error message - * - * @param errorMessage - * @throws DataDictionaryException - */ - public void setErrorMessage(String errorMessage) throws DataDictionaryException { - this.setErrorMessage((Object) errorMessage); - } - - protected void setDateTime(Object dateTime) throws DataDictionaryException { - this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); - } - - protected void setErrorCode(Object errorCode) throws DataDictionaryException { - this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); - } - - protected void setErrorMessage(Object errorMessage) throws DataDictionaryException { - this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); - } - - private void loadKeyDescriptors() { - try { - this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); - this.keys.add(this.kDateTime); - - this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); - this.keys.add(this.kErrorCode); - - this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, false, true); - this.keys.add(this.kErrorMessage); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/ResponseBase.java b/src/main/java/enedis/lab/util/message/ResponseBase.java deleted file mode 100644 index 3abf06b..0000000 --- a/src/main/java/enedis/lab/util/message/ResponseBase.java +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Concrete base class for response messages with data payload. - * - *

    This class extends {@link Response} to provide support for a data payload field, enabling - * responses to carry additional structured data. It is used for responses that require more complex - * content beyond basic status and error reporting. - * - *

    Common use cases include: - * - *

      - *
    • Returning structured data in response to client requests - *
    • Supporting extensible response formats - *
    • Providing a base for custom response types with data - *
    - * - * @author Enedis Smarties team - * @see Response - */ -public class ResponseBase extends Response { - protected static final String KEY_DATA = "data"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - protected ResponseBase() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseBase(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseBase(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseBase( - String name, - LocalDateTime dateTime, - Number errorCode, - String errorMessage, - DataDictionaryBase data) - throws DataDictionaryException { - this(); - - this.setName(name); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - super.updateOptionalParameters(); - } - - /** - * Get data - * - * @return the data - */ - public DataDictionaryBase getData() { - return (DataDictionaryBase) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(DataDictionaryBase data) throws DataDictionaryException { - this.setData((Object) data); - } - - protected void setData(Object data) throws DataDictionaryException { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() { - try { - this.kData = - new KeyDescriptorDataDictionary( - KEY_DATA, false, DataDictionaryBase.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java deleted file mode 100644 index f39606c..0000000 --- a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.util.message.Message; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import java.util.HashMap; -import java.util.Map; -import org.json.JSONException; - -/** - * Generic factory for creating and decoding message objects. - * - *

    This class provides methods for registering message types and decoding messages from text - * representations, typically JSON. It supports extensible message handling by allowing new message - * classes to be registered and decoded dynamically based on their names. - * - *

    Common use cases include: - * - *

      - *
    • Decoding incoming JSON messages into typed objects - *
    • Registering custom message types for extensibility - *
    • Handling errors in message format or content - *
    • Supporting generic message processing pipelines - *
    - * - * @param the type of message handled by this factory - * @author Enedis Smarties team - */ -public class AbstractMessageFactory { - private Class clazz; - private Map> messageClasses; - - /** - * Creates a new AbstractMessageFactory for the specified message type. - * - *

    This constructor initializes the factory for a given message class and prepares the internal - * registry for message type mappings. - * - * @param clazz the class of message objects handled by this factory - */ - public AbstractMessageFactory(Class clazz) { - this.clazz = clazz; - this.messageClasses = new HashMap>(); - } - - /** - * Decodes a message from its text representation and type name. - * - *

    This method parses the provided text (typically JSON) and instantiates the corresponding - * message object based on the registered type name. It throws specific exceptions for unsupported - * message types, invalid formats, or content errors. - * - * @param text the text representation of the message (usually JSON) - * @param name the type name of the message to decode - * @return the decoded message object - * @throws UnsupportedMessageException if the message type is not registered or supported - * @throws MessageInvalidFormatException if the message format is invalid (e.g., malformed JSON) - * @throws MessageInvalidContentException if the message content fails validation - */ - public final T getMessage(String text, String name) - throws UnsupportedMessageException, - MessageInvalidFormatException, - MessageInvalidContentException { - T message = null; - - try { - Class messageClazz = this.messageClasses.get(name); - - if (messageClazz != null) { - message = messageClazz.cast(DataDictionaryBase.fromString(text, messageClazz)); - } else { - throw new UnsupportedMessageException( - "Unsupported " + this.clazz.getSimpleName() + " : " + name); - } - } catch (JSONException e) { - throw new MessageInvalidFormatException( - "Invalid " - + this.clazz.getSimpleName() - + " " - + name - + " format, it should be JSON : " - + e.getMessage(), - e); - } catch (DataDictionaryException e) { - throw new MessageInvalidContentException( - "Invalid " + this.clazz.getSimpleName() + " " + name + " content : " + e.getMessage(), e); - } - - return message; - } - - /** - * Registers a message class for decoding messages with the specified type name. - * - *

    This method allows the factory to support new message types by associating a name with a - * message class. Registered types can then be decoded from text representations. - * - * @param name the type name of the message - * @param messageClazz the class to use for decoding messages of this type - */ - public final void addMessageClass(String name, Class messageClazz) { - if (name == null || messageClazz == null) { - throw new IllegalArgumentException( - "Name and " + this.clazz.getSimpleName() + " class can't be null"); - } - - this.messageClasses.put(name, messageClazz); - } -} diff --git a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java deleted file mode 100644 index 13e7205..0000000 --- a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Message; -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Static factory for decoding basic message objects. - * - *

    This class provides static methods for parsing and validating messages from their text - * representations, typically JSON. It handles extraction of message type and name, and throws - * specific exceptions for format, type, or key errors. - * - *

    Common use cases include: - * - *

      - *
    • Decoding incoming JSON messages into basic message objects - *
    • Validating message format and required keys - *
    • Handling errors in message type or name extraction - *
    • Supporting generic message processing pipelines - *
    - * - * @author Enedis Smarties team - */ -public abstract class BasicMessageFactory { - - /** - * Decodes a basic message from its text representation. - * - *

    This method parses the provided text (typically JSON), extracts the message type and name, - * and instantiates a basic message object. It throws specific exceptions for unsupported message - * types, invalid formats, or missing keys. - * - * @param text the text representation of the message (usually JSON) - * @return the decoded basic message object - * @throws MessageInvalidFormatException if the message format is invalid (e.g., malformed JSON) - * @throws MessageKeyTypeDoesntExistException if the message type key is missing - * @throws MessageKeyNameDoesntExistException if the message name key is missing - * @throws MessageInvalidTypeException if the message type is invalid or unsupported - */ - public static Message getMessage(String text) - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException { - JSONObject messageJson = convertTextToJson(text); - - MessageType type = extractType(messageJson); - String name = extractName(messageJson); - - return createMessage(type, name); - } - - private static JSONObject convertTextToJson(String text) throws MessageInvalidFormatException { - try { - return new JSONObject(text); - } catch (JSONException e) { - throw new MessageInvalidFormatException( - "Invalid format, it should be JSON : " + e.getMessage()); - } - } - - private static MessageType extractType(JSONObject jsonObj) - throws MessageKeyTypeDoesntExistException, - MessageInvalidFormatException, - MessageInvalidTypeException { - try { - checkKeyTypeExists(jsonObj); - String typeStr = jsonObj.getString(Message.KEY_TYPE); - MessageType type = MessageType.valueOf(typeStr); - return type; - } catch (JSONException e) { - throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); - } catch (IllegalArgumentException e) { - throw new MessageInvalidTypeException("Invalid format : " + e.getMessage()); - } - } - - private static void checkKeyTypeExists(JSONObject jsonObj) - throws MessageKeyTypeDoesntExistException { - if (!jsonObj.has(Message.KEY_TYPE)) { - throw new MessageKeyTypeDoesntExistException("Key type missing"); - } - } - - private static String extractName(JSONObject jsonObj) - throws MessageInvalidFormatException, MessageKeyNameDoesntExistException { - try { - checkKeyNameExists(jsonObj); - return jsonObj.getString(Message.KEY_NAME); - } catch (JSONException e) { - throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); - } - } - - private static void checkKeyNameExists(JSONObject jsonObj) - throws MessageKeyNameDoesntExistException { - if (!jsonObj.has(Message.KEY_NAME)) { - throw new MessageKeyNameDoesntExistException("Key name missing"); - } - } - - private static Message createMessage(MessageType type, String name) { - try { - return new Message(type, name); - } catch (DataDictionaryException e) { - return null; - } - } -} diff --git a/src/main/java/enedis/lab/util/message/factory/EventFactory.java b/src/main/java/enedis/lab/util/message/factory/EventFactory.java deleted file mode 100644 index 8aa53b6..0000000 --- a/src/main/java/enedis/lab/util/message/factory/EventFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Event; - -/** - * Factory for creating and decoding event message objects. - * - *

    This class extends {@link AbstractMessageFactory} to provide specialized support for event - * messages. It enables registration and decoding of event types from their text representations, - * typically JSON, and supports extensible event handling in the message processing pipeline. - * - *

    Common use cases include: - * - *

      - *
    • Decoding incoming event messages into typed objects - *
    • Registering custom event types for extensibility - *
    • Supporting generic event processing pipelines - *
    - * - * @author Enedis Smarties team - * @see AbstractMessageFactory - * @see Event - */ -public class EventFactory extends AbstractMessageFactory { - /** - * Creates a new EventFactory for event message objects. - * - *

    This constructor initializes the factory for the {@link Event} class and prepares the - * internal registry for event type mappings. - */ - public EventFactory() { - super(Event.class); - } -} diff --git a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java deleted file mode 100644 index 4804409..0000000 --- a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Message; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; - -/** - * Central factory for decoding and dispatching messages in the TIC2WebSocket framework. - * - *

    This class coordinates the decoding of messages by delegating to specialized sub-factories for - * requests, responses, and events. It parses the message type and name, then dispatches to the - * appropriate factory for further decoding and validation. - * - *

    Common use cases include: - * - *

      - *
    • Decoding incoming JSON messages into typed objects - *
    • Coordinating request, response, and event message handling - *
    • Supporting extensible message processing pipelines - *
    • Managing sub-factory references for modularity - *
    - * - * @author Enedis Smarties team - * @see BasicMessageFactory - * @see RequestFactory - * @see ResponseFactory - * @see EventFactory - */ -public class MessageFactory { - private RequestFactory requestFactory; - private ResponseFactory responseFactory; - private EventFactory eventFactory; - - /** - * Creates a new MessageFactory with no sub-factories set. - * - *

    Sub-factories must be set before decoding messages. - */ - public MessageFactory() { - super(); - } - - /** - * Creates a new MessageFactory with the specified sub-factories. - * - *

    This constructor initializes the factory with request, response, and event sub-factories for - * coordinated message decoding. - * - * @param requestFactory the factory for decoding request messages - * @param responseFactory the factory for decoding response messages - * @param eventFactory the factory for decoding event messages - */ - public MessageFactory( - RequestFactory requestFactory, ResponseFactory responseFactory, EventFactory eventFactory) { - super(); - this.requestFactory = requestFactory; - this.responseFactory = responseFactory; - this.eventFactory = eventFactory; - } - - /** - * Decodes and dispatches a message from its text representation. - * - *

    This method parses the provided text (typically JSON), determines the message type, and - * delegates decoding to the appropriate sub-factory. It throws specific exceptions for - * unsupported message types, invalid formats, or missing keys. - * - * @param text the text representation of the message (usually JSON) - * @return the decoded message object - * @throws MessageInvalidTypeException if the message type is invalid or unsupported - * @throws MessageKeyNameDoesntExistException if the message name key is missing - * @throws MessageKeyTypeDoesntExistException if the message type key is missing - * @throws MessageInvalidFormatException if the message format is invalid (e.g., malformed JSON) - * @throws MessageInvalidContentException if the message content fails validation - * @throws UnsupportedMessageException if the message type is not registered or supported - */ - public Message getMessage(String text) - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - this.checkSubFactoryReferences(); - - Message genericMessage = BasicMessageFactory.getMessage(text); - Message message = null; - - // @formatter:off - switch (genericMessage.getType()) { - case REQUEST: - message = this.requestFactory.getMessage(text, genericMessage.getName()); - break; - case RESPONSE: - message = this.responseFactory.getMessage(text, genericMessage.getName()); - break; - case EVENT: - message = this.eventFactory.getMessage(text, genericMessage.getName()); - break; - default: - break; - } - // @formatter:on - - return message; - } - - /** - * Sets the request sub-factory for decoding request messages. - * - * @param requestFactory the factory for decoding request messages - */ - public void setRequestFactory(RequestFactory requestFactory) { - this.requestFactory = requestFactory; - } - - /** - * Sets the response sub-factory for decoding response messages. - * - * @param responseFactory the factory for decoding response messages - */ - public void setResponseFactory(ResponseFactory responseFactory) { - this.responseFactory = responseFactory; - } - - /** - * Sets the event sub-factory for decoding event messages. - * - * @param eventFactory the factory for decoding event messages - */ - public void setEventFactory(EventFactory eventFactory) { - this.eventFactory = eventFactory; - } - - private void checkSubFactoryReferences() { - if (this.requestFactory == null || this.responseFactory == null || this.eventFactory == null) { - throw new IllegalStateException( - "requestFactory, responseFactory and eventFactory have to be set"); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java deleted file mode 100644 index ed80d48..0000000 --- a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Request; - -/** - * Factory for decoding and instantiating request messages. - * - *

    This factory is responsible for creating {@link Request} objects from their serialized - * representations. It supports extensible registration of request message types, enabling dynamic - * decoding and validation of incoming request messages. - * - *

    Common use cases include: - * - *

      - *
    • Decoding incoming JSON request messages into typed {@link Request} objects - *
    • Registering custom request message classes for extensibility - *
    • Supporting modular request processing pipelines - *
    • Managing request message type mappings for robust decoding - *
    - * - *

    The factory pattern allows for decoupled message instantiation, enabling flexible handling of - * different request types and supporting future protocol extensions. - * - * @author Enedis Smarties team - * @see Request - * @see AbstractMessageFactory - */ -public class RequestFactory extends AbstractMessageFactory { - /** Default constructor */ - public RequestFactory() { - super(Request.class); - } -} diff --git a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java deleted file mode 100644 index 3923f81..0000000 --- a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Response; - -/** - * Factory for creating and decoding response message objects. - * - *

    This class extends {@link AbstractMessageFactory} to provide specialized support for response - * messages. It enables registration and decoding of response types from their text representations, - * typically JSON, and supports extensible response handling in the message processing pipeline. - * - *

    Common use cases include: - * - *

      - *
    • Decoding incoming response messages into typed objects - *
    • Registering custom response types for extensibility - *
    • Supporting generic response processing pipelines - *
    - * - * @author Enedis Smarties team - * @see AbstractMessageFactory - * @see Response - */ -public class ResponseFactory extends AbstractMessageFactory { - /** - * Creates a new ResponseFactory for response message objects. - * - *

    This constructor initializes the factory for the {@link Response} class and prepares the - * internal registry for response type mappings. - */ - public ResponseFactory() { - super(Response.class); - } -} diff --git a/src/main/java/enedis/tic/core/TICCoreError.java b/src/main/java/enedis/tic/core/TICCoreError.java deleted file mode 100644 index 18c40f2..0000000 --- a/src/main/java/enedis/tic/core/TICCoreError.java +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Class representing a core error with identifier, code, message, and optional data. - * - *

    This class provides mechanisms for constructing, accessing, and managing error information - * including error codes, messages, and associated data. It is designed for general-purpose error - * handling. - * - *

    Common use cases include: - * - *

      - *
    • Representing errors with structured data - *
    • Managing error codes and messages - *
    • Associating additional data with errors - *
    - * - * @author Enedis Smarties team - */ -public class TICCoreError extends DataDictionaryBase { - protected static final String KEY_IDENTIFIER = "identifier"; - protected static final String KEY_ERROR_CODE = "errorCode"; - protected static final String KEY_ERROR_MESSAGE = "errorMessage"; - protected static final String KEY_DATA = "data"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kIdentifier; - protected KeyDescriptorNumber kErrorCode; - protected KeyDescriptorString kErrorMessage; - protected KeyDescriptorDataDictionary kData; - - protected TICCoreError() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICCoreError(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICCoreError(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param identifier - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public TICCoreError(TICIdentifier identifier, Number errorCode, String errorMessage) - throws DataDictionaryException { - this(identifier, errorCode, errorMessage, null); - } - - /** - * Constructor setting parameters to specific values - * - * @param identifier - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public TICCoreError( - TICIdentifier identifier, Number errorCode, String errorMessage, DataDictionary data) - throws DataDictionaryException { - this(); - - this.setIdentifier(identifier); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /** - * Get identifier - * - * @return the identifier - */ - public TICIdentifier getIdentifier() { - return (TICIdentifier) this.data.get(KEY_IDENTIFIER); - } - - /** - * Get error code - * - * @return the error code - */ - public Number getErrorCode() { - return (Number) this.data.get(KEY_ERROR_CODE); - } - - /** - * Get error message - * - * @return the error message - */ - public String getErrorMessage() { - return (String) this.data.get(KEY_ERROR_MESSAGE); - } - - /** - * Get data - * - * @return the data - */ - public DataDictionaryBase getData() { - return (DataDictionaryBase) this.data.get(KEY_DATA); - } - - /** - * Set identifier - * - * @param identifier - * @throws DataDictionaryException - */ - public void setIdentifier(TICIdentifier identifier) throws DataDictionaryException { - this.setIdentifier((Object) identifier); - } - - /** - * Set error code - * - * @param errorCode - * @throws DataDictionaryException - */ - public void setErrorCode(Number errorCode) throws DataDictionaryException { - this.setErrorCode((Object) errorCode); - } - - /** - * Set error message - * - * @param errorMessage - * @throws DataDictionaryException - */ - public void setErrorMessage(String errorMessage) throws DataDictionaryException { - this.setErrorMessage((Object) errorMessage); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(DataDictionary data) throws DataDictionaryException { - this.setData((Object) data); - } - - protected void setIdentifier(Object identifier) throws DataDictionaryException { - this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); - } - - protected void setErrorCode(Object errorCode) throws DataDictionaryException { - this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); - } - - protected void setErrorMessage(Object errorMessage) throws DataDictionaryException { - this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); - } - - protected void setData(Object data) throws DataDictionaryException { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - private void loadKeyDescriptors() { - try { - this.kIdentifier = - new KeyDescriptorDataDictionary(KEY_IDENTIFIER, true, TICIdentifier.class); - this.keys.add(this.kIdentifier); - - this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); - this.keys.add(this.kErrorCode); - - this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, true, false); - this.keys.add(this.kErrorMessage); - - this.kData = - new KeyDescriptorDataDictionary( - KEY_DATA, false, DataDictionaryBase.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/core/TICCoreFrame.java b/src/main/java/enedis/tic/core/TICCoreFrame.java deleted file mode 100644 index 137d0d5..0000000 --- a/src/main/java/enedis/tic/core/TICCoreFrame.java +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Class representing a core frame with identifier, mode, capture time, and content. - * - *

    This class provides mechanisms for constructing, accessing, and managing frame information - * including identifiers, modes, timestamps, and associated content. It is designed for - * general-purpose frame handling. - * - *

    Common use cases include: - * - *

      - *
    • Representing frames with structured data - *
    • Managing frame identifiers and modes - *
    • Associating content and timestamps with frames - *
    - * - * @author Enedis Smarties team - */ -public class TICCoreFrame extends DataDictionaryBase { - protected static final String KEY_IDENTIFIER = "identifier"; - protected static final String KEY_MODE = "mode"; - protected static final String KEY_CAPTURE_DATE_TIME = "captureDateTime"; - protected static final String KEY_CONTENT = "content"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kIdentifier; - protected KeyDescriptorEnum kMode; - protected KeyDescriptorLocalDateTime kCaptureDateTime; - protected KeyDescriptorDataDictionary kContent; - - protected TICCoreFrame() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICCoreFrame(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICCoreFrame(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param identifier - * @param mode - * @param captureDateTime - * @param content - * @throws DataDictionaryException - */ - public TICCoreFrame( - TICIdentifier identifier, - TICMode mode, - LocalDateTime captureDateTime, - DataDictionaryBase content) - throws DataDictionaryException { - this(); - - this.setIdentifier(identifier); - this.setMode(mode); - this.setCaptureDateTime(captureDateTime); - this.setContent(content); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - super.updateOptionalParameters(); - } - - /** - * Get identifier - * - * @return the identifier - */ - public TICIdentifier getIdentifier() { - return (TICIdentifier) this.data.get(KEY_IDENTIFIER); - } - - /** - * Get mode - * - * @return the mode - */ - public TICMode getMode() { - return (TICMode) this.data.get(KEY_MODE); - } - - /** - * Get capture date time - * - * @return the capture date time - */ - public LocalDateTime getCaptureDateTime() { - return (LocalDateTime) this.data.get(KEY_CAPTURE_DATE_TIME); - } - - /** - * Get content - * - * @return the content - */ - public DataDictionaryBase getContent() { - return (DataDictionaryBase) this.data.get(KEY_CONTENT); - } - - /** - * Set identifier - * - * @param identifier - * @throws DataDictionaryException - */ - public void setIdentifier(TICIdentifier identifier) throws DataDictionaryException { - this.setIdentifier((Object) identifier); - } - - /** - * Set mode - * - * @param mode - * @throws DataDictionaryException - */ - public void setMode(TICMode mode) throws DataDictionaryException { - this.setMode((Object) mode); - } - - /** - * Set capture date time - * - * @param captureDateTime - * @throws DataDictionaryException - */ - public void setCaptureDateTime(LocalDateTime captureDateTime) throws DataDictionaryException { - this.setCaptureDateTime((Object) captureDateTime); - } - - /** - * Set content - * - * @param content - * @throws DataDictionaryException - */ - public void setContent(DataDictionaryBase content) throws DataDictionaryException { - this.setContent((Object) content); - } - - protected void setIdentifier(Object identifier) throws DataDictionaryException { - this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); - } - - protected void setMode(Object mode) throws DataDictionaryException { - this.data.put(KEY_MODE, this.kMode.convert(mode)); - } - - protected void setCaptureDateTime(Object captureDateTime) throws DataDictionaryException { - this.data.put(KEY_CAPTURE_DATE_TIME, this.kCaptureDateTime.convert(captureDateTime)); - } - - protected void setContent(Object content) throws DataDictionaryException { - this.data.put(KEY_CONTENT, this.kContent.convert(content)); - } - - private void loadKeyDescriptors() { - try { - this.kIdentifier = - new KeyDescriptorDataDictionary(KEY_IDENTIFIER, true, TICIdentifier.class); - this.keys.add(this.kIdentifier); - - this.kMode = new KeyDescriptorEnum(KEY_MODE, true, TICMode.class); - this.keys.add(this.kMode); - - this.kCaptureDateTime = new KeyDescriptorLocalDateTime(KEY_CAPTURE_DATE_TIME, true); - this.keys.add(this.kCaptureDateTime); - - this.kContent = - new KeyDescriptorDataDictionary( - KEY_CONTENT, true, DataDictionaryBase.class); - this.keys.add(this.kContent); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/core/TICCoreStreamBase.java b/src/main/java/enedis/tic/core/TICCoreStreamBase.java deleted file mode 100644 index 43036db..0000000 --- a/src/main/java/enedis/tic/core/TICCoreStreamBase.java +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.io.channels.Channel; -import enedis.lab.io.channels.ChannelException; -import enedis.lab.io.datastreams.DataStreamBase; -import enedis.lab.io.datastreams.DataStreamDirection; -import enedis.lab.io.datastreams.DataStreamException; -import enedis.lab.io.datastreams.DataStreamListener; -import enedis.lab.io.datastreams.DataStreamStatus; -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.io.tic.TICPortFinderBase; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.channels.ChannelTICSerialPort; -import enedis.lab.protocol.tic.channels.ChannelTICSerialPortConfiguration; -import enedis.lab.protocol.tic.datastreams.TICInputStream; -import enedis.lab.protocol.tic.datastreams.TICStreamConfiguration; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.util.task.Notifier; -import enedis.lab.util.task.NotifierBase; -import enedis.lab.util.task.Task; -import java.time.LocalDateTime; -import java.util.Collection; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Core stream implementation for frame acquisition and subscriber notifications. - * - *

    This class provides mechanisms for managing streams, acquiring frames, handling errors, and - * notifying subscribers. It implements the contract for event-driven stream operations. - * - *

    Common use cases include: - * - *

      - *
    • Acquiring frames from streams - *
    • Managing and notifying subscribers - *
    • Handling errors and stream lifecycle - *
    - * - * @author Enedis Smarties team - */ -public class TICCoreStreamBase implements TICCoreStream, Task, DataStreamListener { - - private TICIdentifier identifier; - private DataStreamBase stream; - private Channel channel; - private Notifier notifier; - private static Logger logger = LogManager.getLogger(); - - public TICCoreStreamBase(String portId, String portName, TICMode ticMode) - throws TICCoreException { - TICPortDescriptor descriptor = null; - - if (portId != null) { - descriptor = TICPortFinderBase.getInstance().findByPortId(portId); - if (descriptor == null) { - TICCoreException exception = - new TICCoreException( - TICCoreErrorCode.STREAM_PORT_ID_NOT_FOUND.getCode(), - "TICCore stream port id " + portId + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - } else if (portName != null) { - descriptor = TICPortFinderBase.getInstance().findByPortName(portName); - if (descriptor == null) { - descriptor = TICPortFinderBase.getInstance().findNative(portName); - if (descriptor == null) { - TICCoreException exception = - new TICCoreException( - TICCoreErrorCode.STREAM_PORT_NAME_NOT_FOUND.getCode(), - "TICCore stream port name " + portName + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - } - } else { - TICCoreException exception = - new TICCoreException( - TICCoreErrorCode.STREAM_PORT_DESCRIPTOR_EMPTY.getCode(), - "TICCore stream port descriptor empty!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - if (ticMode == null) { - TICCoreException exception = - new TICCoreException( - TICCoreErrorCode.STREAM_MODE_NOT_DEFINED.getCode(), - "TICCore stream mode not defined!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - try { - this.identifier = new TICIdentifier(portId, portName, null); - this.channel = - new ChannelTICSerialPort( - new ChannelTICSerialPortConfiguration( - "Channel@" + descriptor.getPortName(), portName, ticMode)); - this.stream = - new TICInputStream( - new TICStreamConfiguration( - "Stream@" + descriptor.getPortName(), - DataStreamDirection.INPUT, - "Channel@" + descriptor.getPortName(), - ticMode)); - this.stream.setChannel(this.channel); - this.notifier = new NotifierBase(); - logger = LogManager.getLogger(); - } catch (DataDictionaryException | ChannelException | DataStreamException e) { - TICCoreException exception = - new TICCoreException( - TICCoreErrorCode.OTHER_REASON.getCode(), - "TICCore stream instanciation failed : " + e.getMessage()); - logger.error(exception.getMessage(), exception); - throw exception; - } - } - - @Override - public TICIdentifier getIdentifier() { - TICIdentifier identifier; - - synchronized (this.identifier) { - identifier = (TICIdentifier) this.identifier.clone(); - } - - return identifier; - } - - @Override - public Collection getSubscribers() { - return this.notifier.getSubscribers(); - } - - @Override - public boolean hasSubscriber(TICCoreSubscriber subscriber) { - return this.notifier.hasSubscriber(subscriber); - } - - @Override - public void subscribe(TICCoreSubscriber subscriber) { - this.notifier.subscribe(subscriber); - } - - @Override - public void unsubscribe(TICCoreSubscriber subscriber) { - this.notifier.unsubscribe(subscriber); - } - - @Override - public void onDataReceived(String dataStreamName, DataDictionary data) { - TICCoreFrame frame = this.createFrame(data); - - if (frame != null) { - this.notifyOnData(frame); - } else { - logger.debug("Frame creation skipped due to null TICFrame"); - } - } - - @Override - public void onDataSent(String dataStreamName, DataDictionary data) {} - - @Override - public void onStatusChanged(String dataStreamName, DataStreamStatus newStatus) {} - - @Override - public void onErrorDetected( - String dataStreamName, int errorCode, String errorMessage, DataDictionary data) { - TICCoreError error = this.createError(errorCode, errorMessage, data); - this.notifyOnError(error); - } - - @Override - public void start() { - this.channel.start(); - try { - this.stream.open(); - this.stream.subscribe(this); - } catch (DataStreamException e) { - logger.error(e.getMessage()); - } - } - - @Override - public void stop() { - this.stream.unsubscribe(this); - try { - this.stream.close(); - } catch (DataStreamException e) { - logger.error(e.getMessage()); - } - this.channel.stop(); - } - - @Override - public boolean isRunning() { - return this.channel.isRunning(); - } - - private TICCoreFrame createFrame(DataDictionary data) { - TICCoreFrame frame = new TICCoreFrame(); - try { - frame.setCaptureDateTime(LocalDateTime.now()); - TICFrame ticFrame = (TICFrame) data.get(TICInputStream.KEY_DATA); - - if (ticFrame == null) { - logger.warn("TICFrame is null. Skipping frame creation."); - return null; - } - - DataDictionaryBase content = new DataDictionaryBase(); - for (TICFrameDataSet frameDataSet : ticFrame.getDataSetList()) { - String label = frameDataSet.getLabel(); - content.set(label, ticFrame.getData(label)); - } - frame.setContent(content); - String serialNumber = null; - if (ticFrame instanceof TICFrameStandard) { - frame.setMode(TICMode.STANDARD); - serialNumber = (String) content.get("ADSC"); - } else if (ticFrame instanceof TICFrameHistoric) { - frame.setMode(TICMode.HISTORIC); - serialNumber = (String) content.get("ADCO"); - } - synchronized (this.identifier) { - this.identifier.setSerialNumber(serialNumber); - frame.setIdentifier(this.identifier.clone()); - } - } catch (DataDictionaryException e) { - logger.error(e.getMessage()); - frame = null; - } - - return frame; - } - - private TICCoreError createError(int errorCode, String errorMessage, DataDictionary data) { - TICCoreError error = null; - try { - error = new TICCoreError(this.getIdentifier(), errorCode, errorMessage, data); - } catch (DataDictionaryException e) { - logger.error(e.getMessage()); - } - - return error; - } - - private void notifyOnData(TICCoreFrame frame) { - if (frame == null) { - return; - } - for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) { - subscriber.onData(frame); - } - } - - private void notifyOnError(TICCoreError error) { - if (error == null) { - return; - } - for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) { - subscriber.onError(error); - } - } -} diff --git a/src/main/java/enedis/tic/core/TICIdentifier.java b/src/main/java/enedis/tic/core/TICIdentifier.java deleted file mode 100644 index 8136e17..0000000 --- a/src/main/java/enedis/tic/core/TICIdentifier.java +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorString; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Class representing a core identifier with port ID, port name, and serial number. - * - *

    This class provides mechanisms for constructing, accessing, and managing identifier - * information including port IDs, port names, and serial numbers. It is designed for - * general-purpose identifier handling. - * - *

    Common use cases include: - * - *

      - *
    • Representing identifiers with structured data - *
    • Matching identifiers for streams and devices - *
    • Managing port and serial information - *
    - * - * @author Enedis Smarties team - */ -public class TICIdentifier extends DataDictionaryBase { - protected static final String KEY_PORT_ID = "portId"; - protected static final String KEY_PORT_NAME = "portName"; - protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kPortId; - protected KeyDescriptorString kPortName; - protected KeyDescriptorString kSerialNumber; - - protected TICIdentifier() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICIdentifier(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICIdentifier(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param portId - * @param portName - * @param serialNumber - * @throws DataDictionaryException - */ - public TICIdentifier(String portId, String portName, String serialNumber) - throws DataDictionaryException { - this(); - - this.setPortId((Object) portId); - this.setPortName((Object) portName); - this.setSerialNumber((Object) serialNumber); - - this.checkAndUpdate(); - } - - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (this.getPortId() == null && this.getPortName() == null && this.getSerialNumber() == null) { - throw new DataDictionaryException("Empty TICIdentifier not allowed!"); - } - } - - public boolean matches(TICIdentifier identifier) { - if (identifier != null) { - if (this.getSerialNumber() != null && identifier.getSerialNumber() != null) { - return this.getSerialNumber().equals(identifier.getSerialNumber()); - } - if (this.getPortId() != null && identifier.getPortId() != null) { - return this.getPortId().equals(identifier.getPortId()); - } - if (this.getPortName() != null && identifier.getPortName() != null) { - return this.getPortName().equals(identifier.getPortName()); - } - } - - return false; - } - - /** - * Get port id - * - * @return the port id - */ - public String getPortId() { - return (String) this.data.get(KEY_PORT_ID); - } - - /** - * Get port name - * - * @return the port name - */ - public String getPortName() { - return (String) this.data.get(KEY_PORT_NAME); - } - - /** - * Get serial number - * - * @return the serial number - */ - public String getSerialNumber() { - return (String) this.data.get(KEY_SERIAL_NUMBER); - } - - /** - * Set port id - * - * @param portId - * @throws DataDictionaryException - */ - public void setPortId(String portId) throws DataDictionaryException { - if (portId == null && this.getPortName() == null && this.getSerialNumber() == null) { - throw new DataDictionaryException( - "Cannot set null portId because empty TICIdentifier not allowed!"); - } - this.setPortId((Object) portId); - } - - /** - * Set port name - * - * @param portName - * @throws DataDictionaryException - */ - public void setPortName(String portName) throws DataDictionaryException { - if (this.getPortId() == null && portName == null && this.getSerialNumber() == null) { - throw new DataDictionaryException( - "Cannot set null portName because empty TICIdentifier not allowed!"); - } - this.setPortName((Object) portName); - } - - /** - * Set serial number - * - * @param serialNumber - * @throws DataDictionaryException - */ - public void setSerialNumber(String serialNumber) throws DataDictionaryException { - if (this.getPortId() == null && this.getPortName() == null && serialNumber == null) { - throw new DataDictionaryException( - "Cannot set null serialNumber because empty TICIdentifier not allowed!"); - } - this.setSerialNumber((Object) serialNumber); - } - - protected void setPortId(Object portId) throws DataDictionaryException { - this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); - } - - protected void setPortName(Object portName) throws DataDictionaryException { - this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); - } - - protected void setSerialNumber(Object serialNumber) throws DataDictionaryException { - this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); - } - - private void loadKeyDescriptors() { - try { - this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); - this.keys.add(this.kPortId); - - this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); - this.keys.add(this.kPortName); - - this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, false); - this.keys.add(this.kSerialNumber); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java deleted file mode 100644 index 05ca25d..0000000 --- a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.config; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.configuration.ConfigurationBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorListMinMaxSize; -import enedis.lab.types.datadictionary.KeyDescriptorNumberMinMax; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Configuration class for TIC2WebSocket service. - * - *

    This class manages configuration parameters for the TIC2WebSocket server, including server - * port, TIC mode, and the list of TIC port names. It provides validation and conversion logic for - * each parameter, ensuring correct types and values. - * - *

    Key features include: - * - *

      - *
    • Validation of server port range and TIC port names - *
    • Support for multiple construction methods (map, DataDictionary, explicit values) - *
    • Integration with the configuration base and key descriptor system - *
    - * - * @author Enedis Smarties team - * @see ConfigurationBase - * @see TICMode - */ -public class TIC2WebSocketConfiguration extends ConfigurationBase { - /** Key for server port configuration. */ - protected static final String KEY_SERVER_PORT = "serverPort"; - - /** Key for TIC mode configuration. */ - protected static final String KEY_TIC_MODE = "ticMode"; - - /** Key for TIC port names configuration. */ - protected static final String KEY_TIC_PORT_NAMES = "ticPortNames"; - - /** Minimum allowed server port value. */ - private static final Number SERVER_PORT_MIN = 1; - - /** Maximum allowed server port value. */ - private static final Number SERVER_PORT_MAX = 65535; - - /** Minimum number of TIC port names required. */ - private static final int TIC_PORT_NAMES_MIN_SIZE = 1; - - /** List of key descriptors for configuration parameters. */ - private final List> keys = new ArrayList<>(); - - /** Key descriptor for server port. */ - protected KeyDescriptorNumberMinMax kServerPort; - - /** Key descriptor for TIC mode. */ - protected KeyDescriptorEnum kTicMode; - - /** Key descriptor for TIC port names. */ - protected KeyDescriptorListMinMaxSize kTicPortNames; - - /** Constructs a configuration with default values and loads key descriptors. */ - protected TIC2WebSocketConfiguration() { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructs a configuration from a map of values. - * - *

    Initializes the configuration using a map of key-value pairs. - * - * @param map the map containing configuration parameters - * @throws DataDictionaryException if validation fails - */ - public TIC2WebSocketConfiguration(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a configuration from another DataDictionary instance. - * - *

    Copies configuration values from the provided DataDictionary. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public TIC2WebSocketConfiguration(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a configuration with a specified name and file, using default parameter values. - * - * @param name the configuration name - * @param file the configuration file - */ - public TIC2WebSocketConfiguration(String name, File file) { - this(); - this.init(name, file); - } - - /** - * Constructs a configuration with explicit parameter values. - * - *

    Initializes the configuration with the specified server port, TIC mode, and TIC port names. - * - * @param serverPort the server port number - * @param ticMode the TIC mode - * @param ticPortNames the list of TIC port names - * @throws DataDictionaryException if validation fails - */ - public TIC2WebSocketConfiguration(Number serverPort, TICMode ticMode, List ticPortNames) - throws DataDictionaryException { - this(); - - this.setServerPort(serverPort); - this.setTicMode(ticMode); - this.setTicPortNames(ticPortNames); - - this.checkAndUpdate(); - } - - /** - * Returns the configured server port number. - * - * @return the server port - */ - public Number getServerPort() { - return (Number) this.data.get(KEY_SERVER_PORT); - } - - /** - * Returns the configured TIC mode. - * - * @return the TIC mode - */ - public TICMode getTicMode() { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Returns the list of configured TIC port names. - * - * @return the list of TIC port names - */ - @SuppressWarnings("unchecked") - public List getTicPortNames() { - return (List) this.data.get(KEY_TIC_PORT_NAMES); - } - - /** - * Sets the server port value. - * - * @param serverPort the server port number - * @throws DataDictionaryException if validation fails - */ - public void setServerPort(Number serverPort) throws DataDictionaryException { - this.setServerPort((Object) serverPort); - } - - /** - * Sets the TIC mode value. - * - * @param ticMode the TIC mode - * @throws DataDictionaryException if validation fails - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException { - this.setTicMode((Object) ticMode); - } - - /** - * Sets the list of TIC port names. - * - * @param ticPortNames the list of TIC port names - * @throws DataDictionaryException if validation fails - */ - public void setTicPortNames(List ticPortNames) throws DataDictionaryException { - this.setTicPortNames((Object) ticPortNames); - } - - /** - * Internal setter for server port, with conversion and validation. - * - * @param serverPort the server port value (Object or Number) - * @throws DataDictionaryException if validation fails - */ - protected void setServerPort(Object serverPort) throws DataDictionaryException { - this.data.put(KEY_SERVER_PORT, this.kServerPort.convert(serverPort)); - } - - /** - * Internal setter for TIC mode, with conversion and validation. - * - * @param ticMode the TIC mode value (Object or TICMode) - * @throws DataDictionaryException if validation fails - */ - protected void setTicMode(Object ticMode) throws DataDictionaryException { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - } - - /** - * Internal setter for TIC port names, with conversion and validation. - * - *

    Checks for null, empty, and duplicate port names. - * - * @param ticPortNames the list or object representing TIC port names - * @throws DataDictionaryException if validation fails - */ - protected void setTicPortNames(Object ticPortNames) throws DataDictionaryException { - List portNames = this.kTicPortNames.convert(ticPortNames); - for (int i = 0; i < portNames.size(); i++) { - String portName = portNames.get(i); - if (portName == null) { - throw new DataDictionaryException( - "Key " - + KEY_TIC_PORT_NAMES - + ": value at index " - + i - + "(" - + portName - + ") cannot be null"); - } else if (portName.isEmpty()) { - throw new DataDictionaryException( - "Key " - + KEY_TIC_PORT_NAMES - + ": value at index " - + i - + "(" - + portName - + ") cannot be empty"); - } else if (i != portNames.lastIndexOf(portName)) { - throw new DataDictionaryException( - "Key " - + KEY_TIC_PORT_NAMES - + ": value at index " - + i - + " and " - + portNames.lastIndexOf(portName) - + "(" - + portName - + ") are identical"); - } - } - this.data.put(KEY_TIC_PORT_NAMES, portNames); - } - - /** - * Loads key descriptors for configuration parameters. - * - *

    Initializes descriptors for server port, TIC mode, and TIC port names, and adds them to the - * configuration. - */ - private void loadKeyDescriptors() { - try { - this.kServerPort = - new KeyDescriptorNumberMinMax(KEY_SERVER_PORT, true, SERVER_PORT_MIN, SERVER_PORT_MAX); - this.keys.add(this.kServerPort); - - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, false, TICMode.class); - this.keys.add(this.kTicMode); - - this.kTicPortNames = - new KeyDescriptorListMinMaxSize<>( - KEY_TIC_PORT_NAMES, false, String.class, TIC_PORT_NAMES_MIN_SIZE, null); - this.keys.add(this.kTicPortNames); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/EventOnError.java b/src/main/java/enedis/tic/service/message/EventOnError.java deleted file mode 100644 index b0364ba..0000000 --- a/src/main/java/enedis/tic/service/message/EventOnError.java +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Event; -import enedis.tic.core.TICCoreError; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Event message representing an error in the TIC2WebSocket protocol. - * - *

    This class encapsulates error information using a {@link TICCoreError} object and provides - * constructors for various initialization scenarios. It integrates with the event messaging system - * for error notification and handling. - * - *

    Key features include: - * - *

      - *
    • Encapsulates error data for protocol events - *
    • Supports construction from map, DataDictionary, or explicit values - *
    • Validates and manages error data using key descriptors - *
    - * - * @author Enedis Smarties team - * @see Event - * @see TICCoreError - */ -public class EventOnError extends Event { - /** Key for error data in the event. */ - protected static final String KEY_DATA = "data"; - - /** Message name for error events. */ - public static final String NAME = "OnError"; - - private List> keys = new ArrayList>(); - - /** Key descriptor for error data. */ - protected KeyDescriptorDataDictionary kData; - - /** Constructs an error event with default values and loads key descriptors. */ - protected EventOnError() { - super(); - this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructs an error event from a map of values. - * - * @param map the map containing event parameters - * @throws DataDictionaryException if validation fails - */ - public EventOnError(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs an error event from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public EventOnError(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs an error event with explicit date/time and error data. - * - * @param dateTime the event timestamp - * @param data the error data - * @throws DataDictionaryException if validation fails - */ - public EventOnError(LocalDateTime dateTime, TICCoreError data) throws DataDictionaryException { - this(); - this.setDateTime(dateTime); - this.setData(data); - this.checkAndUpdate(); - } - - /** - * Updates optional parameters for the event, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Returns the error data associated with this event. - * - * @return the error data - */ - public TICCoreError getData() { - return (TICCoreError) this.data.get(KEY_DATA); - } - - /** - * Sets the error data for this event. - * - * @param data the error data - * @throws DataDictionaryException if validation fails - */ - public void setData(TICCoreError data) throws DataDictionaryException { - this.setData((Object) data); - } - - /** - * Internal setter for error data, with conversion and validation. - * - * @param data the error data (Object or TICCoreError) - * @throws DataDictionaryException if validation fails - */ - protected void setData(Object data) throws DataDictionaryException { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /** - * Loads key descriptors for event parameters. - * - *

    Initializes the descriptor for error data and adds it to the event. - */ - private void loadKeyDescriptors() { - try { - this.kData = - new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreError.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/EventOnTICData.java b/src/main/java/enedis/tic/service/message/EventOnTICData.java deleted file mode 100644 index efe7f20..0000000 --- a/src/main/java/enedis/tic/service/message/EventOnTICData.java +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Event; -import enedis.tic.core.TICCoreFrame; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Event message representing TIC data in the TIC2WebSocket protocol. - * - *

    This class encapsulates TIC data using a {@link TICCoreFrame} object and provides constructors - * for various initialization scenarios. It integrates with the event messaging system for data - * notification and handling. - * - *

    Key features include: - * - *

      - *
    • Encapsulates TIC data for protocol events - *
    • Supports construction from map, DataDictionary, or explicit values - *
    • Validates and manages TIC data using key descriptors - *
    - * - * @author Enedis Smarties team - * @see Event - * @see TICCoreFrame - */ -public class EventOnTICData extends Event { - /** Key for TIC data in the event. */ - protected static final String KEY_DATA = "data"; - - /** Message name for TIC data events. */ - public static final String NAME = "OnTICData"; - - private List> keys = new ArrayList>(); - - /** Key descriptor for TIC data. */ - protected KeyDescriptorDataDictionary kData; - - /** Constructs a TIC data event with default values and loads key descriptors. */ - protected EventOnTICData() { - super(); - this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructs a TIC data event from a map of values. - * - * @param map the map containing event parameters - * @throws DataDictionaryException if validation fails - */ - public EventOnTICData(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a TIC data event from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public EventOnTICData(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a TIC data event with explicit date/time and TIC data. - * - * @param dateTime the event timestamp - * @param data the TIC data - * @throws DataDictionaryException if validation fails - */ - public EventOnTICData(LocalDateTime dateTime, TICCoreFrame data) throws DataDictionaryException { - this(); - this.setDateTime(dateTime); - this.setData(data); - this.checkAndUpdate(); - } - - /** - * Updates optional parameters for the event, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Returns the TIC data associated with this event. - * - * @return the TIC data - */ - public TICCoreFrame getData() { - return (TICCoreFrame) this.data.get(KEY_DATA); - } - - /** - * Sets the TIC data for this event. - * - * @param data the TIC data - * @throws DataDictionaryException if validation fails - */ - public void setData(TICCoreFrame data) throws DataDictionaryException { - this.setData((Object) data); - } - - /** - * Internal setter for TIC data, with conversion and validation. - * - * @param data the TIC data (Object or TICCoreFrame) - * @throws DataDictionaryException if validation fails - */ - protected void setData(Object data) throws DataDictionaryException { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /** - * Loads key descriptors for event parameters. - * - *

    Initializes the descriptor for TIC data and adds it to the event. - */ - private void loadKeyDescriptors() { - try { - this.kData = - new KeyDescriptorDataDictionary(KEY_DATA, true, TICCoreFrame.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java deleted file mode 100644 index 4ddb217..0000000 --- a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Request; -import java.util.Map; - -/** - * Request message for retrieving available TICs. - * - *

    This class represents a request to obtain the list of available TICs. It provides constructors - * for various initialization scenarios and integrates with the request messaging system. - * - *

    Key features include: - * - *

      - *
    • Encapsulates request for available TICs - *
    • Supports construction from map, DataDictionary, or default values - *
    • Validates and manages request parameters - *
    - * - * @author Enedis Smarties team - * @see Request - */ -public class RequestGetAvailableTICs extends Request { - /** Message name for this request. */ - public static final String NAME = "GetAvailableTICs"; - - /** - * Constructs a request for available TICs with default values. - * - * @throws DataDictionaryException if validation fails - */ - public RequestGetAvailableTICs() throws DataDictionaryException { - super(); - this.kName.setAcceptedValues(NAME); - this.checkAndUpdate(); - } - - /** - * Constructs a request for available TICs from a map of values. - * - * @param map the map containing request parameters - * @throws DataDictionaryException if validation fails - */ - public RequestGetAvailableTICs(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a request for available TICs from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public RequestGetAvailableTICs(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Updates optional parameters for the request, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } -} diff --git a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java b/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java deleted file mode 100644 index 0d435f3..0000000 --- a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Request; -import java.util.Map; - -/** - * Request message for retrieving modem information. - * - *

    This class represents a request to obtain information about available modems. It provides - * constructors for various initialization scenarios and integrates with the request messaging - * system. - * - *

    Key features include: - * - *

      - *
    • Encapsulates request for modem information - *
    • Supports construction from map, DataDictionary, or default values - *
    • Validates and manages request parameters - *
    - * - * @author Enedis Smarties team - * @see Request - */ -public class RequestGetModemsInfo extends Request { - /** Message name for this request. */ - public static final String NAME = "GetModemsInfo"; - - /** - * Constructs a request for modem information with default values. - * - * @throws DataDictionaryException if validation fails - */ - public RequestGetModemsInfo() throws DataDictionaryException { - super(); - this.kName.setAcceptedValues(NAME); - this.checkAndUpdate(); - } - - /** - * Constructs a request for modem information from a map of values. - * - * @param map the map containing request parameters - * @throws DataDictionaryException if validation fails - */ - public RequestGetModemsInfo(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a request for modem information from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public RequestGetModemsInfo(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Updates optional parameters for the request, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } -} diff --git a/src/main/java/enedis/tic/service/message/RequestReadTIC.java b/src/main/java/enedis/tic/service/message/RequestReadTIC.java deleted file mode 100644 index 7857888..0000000 --- a/src/main/java/enedis/tic/service/message/RequestReadTIC.java +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Request; -import enedis.tic.core.TICIdentifier; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Request message for reading TIC data in the TIC2WebSocket protocol. - * - *

    This class represents a request to read TIC data for a specific identifier. It provides - * constructors for various initialization scenarios and integrates with the request messaging - * system. - * - *

    Key features include: - * - *

      - *
    • Encapsulates request for reading TIC data - *
    • Supports construction from map, DataDictionary, or explicit identifier - *
    • Validates and manages request parameters using key descriptors - *
    - * - * @author Enedis Smarties team - * @see Request - * @see TICIdentifier - */ -public class RequestReadTIC extends Request { - /** Key for TIC identifier data in the request. */ - protected static final String KEY_DATA = "data"; - - /** Message name for this request. */ - public static final String NAME = "ReadTIC"; - - private List> keys = new ArrayList>(); - - /** Key descriptor for TIC identifier data. */ - protected KeyDescriptorDataDictionary kData; - - /** Constructs a request for reading TIC data with default values. */ - protected RequestReadTIC() { - super(); - this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructs a request for reading TIC data from a map of values. - * - * @param map the map containing request parameters - * @throws DataDictionaryException if validation fails - */ - public RequestReadTIC(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a request for reading TIC data from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public RequestReadTIC(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a request for reading TIC data with a specific identifier. - * - * @param data the TIC identifier - * @throws DataDictionaryException if validation fails - */ - public RequestReadTIC(TICIdentifier data) throws DataDictionaryException { - this(); - this.setData(data); - this.checkAndUpdate(); - } - - /** - * Updates optional parameters for the request, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Returns the TIC identifier data associated with this request. - * - * @return the TIC identifier - */ - public TICIdentifier getData() { - return (TICIdentifier) this.data.get(KEY_DATA); - } - - /** - * Sets the TIC identifier data for this request. - * - * @param data the TIC identifier - * @throws DataDictionaryException if validation fails - */ - public void setData(TICIdentifier data) throws DataDictionaryException { - this.setData((Object) data); - } - - /** - * Internal setter for TIC identifier data, with conversion and validation. - * - * @param data the TIC identifier (Object or TICIdentifier) - * @throws DataDictionaryException if validation fails - */ - protected void setData(Object data) throws DataDictionaryException { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /** - * Loads key descriptors for request parameters. - * - *

    Initializes the descriptor for TIC identifier data and adds it to the request. - */ - private void loadKeyDescriptors() { - try { - this.kData = - new KeyDescriptorDataDictionary(KEY_DATA, true, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java deleted file mode 100644 index d617edf..0000000 --- a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Request; -import enedis.tic.core.TICIdentifier; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Request message for subscribing to TIC data. - * - *

    This class represents a request to subscribe to TIC data for one or more identifiers. It - * provides constructors for various initialization scenarios and integrates with the request - * messaging system. - * - *

    Key features include: - * - *

      - *
    • Encapsulates request for TIC data subscription - *
    • Supports construction from map, DataDictionary, or explicit identifier list - *
    • Validates and manages request parameters using key descriptors - *
    - * - * @author Enedis Smarties team - * @see Request - * @see TICIdentifier - */ -public class RequestSubscribeTIC extends Request { - /** Key for TIC identifier data in the request. */ - protected static final String KEY_DATA = "data"; - - /** Message name for this request. */ - public static final String NAME = "SubscribeTIC"; - - private List> keys = new ArrayList>(); - - /** Key descriptor for TIC identifier data. */ - protected KeyDescriptorList kData; - - /** Constructs a request for subscribing to TIC data with default values. */ - protected RequestSubscribeTIC() { - super(); - this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructs a request for subscribing to TIC data from a map of values. - * - * @param map the map containing request parameters - * @throws DataDictionaryException if validation fails - */ - public RequestSubscribeTIC(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a request for subscribing to TIC data from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public RequestSubscribeTIC(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a request for subscribing to TIC data with a specific list of identifiers. - * - * @param data the list of TIC identifiers - * @throws DataDictionaryException if validation fails - */ - public RequestSubscribeTIC(List data) throws DataDictionaryException { - this(); - this.setData(data); - this.checkAndUpdate(); - } - - /** - * Updates optional parameters for the request, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Returns the list of TIC identifier data associated with this request. - * - * @return the list of TIC identifiers - */ - @SuppressWarnings("unchecked") - public List getData() { - return (List) this.data.get(KEY_DATA); - } - - /** - * Sets the list of TIC identifier data for this request. - * - * @param data the list of TIC identifiers - * @throws DataDictionaryException if validation fails - */ - public void setData(List data) throws DataDictionaryException { - this.setData((Object) data); - } - - /** - * Internal setter for TIC identifier data, with conversion and validation. - * - * @param data the TIC identifier list (Object or List) - * @throws DataDictionaryException if validation fails - */ - protected void setData(Object data) throws DataDictionaryException { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /** - * Loads key descriptors for request parameters. - * - *

    Initializes the descriptor for TIC identifier data and adds it to the request. - */ - private void loadKeyDescriptors() { - try { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java deleted file mode 100644 index b40665b..0000000 --- a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Request; -import enedis.tic.core.TICIdentifier; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Request message for unsubscribing from TIC data in the TIC2WebSocket protocol. - * - *

    This class represents a request to unsubscribe from TIC data for one or more identifiers. It - * provides constructors for various initialization scenarios and integrates with the request - * messaging system. - * - *

    Key features include: - * - *

      - *
    • Encapsulates request for TIC data unsubscription - *
    • Supports construction from map, DataDictionary, or explicit identifier list - *
    • Validates and manages request parameters using key descriptors - *
    - * - * @author Enedis Smarties team - * @see Request - * @see TICIdentifier - */ -public class RequestUnsubscribeTIC extends Request { - /** Key for TIC identifier data in the request. */ - protected static final String KEY_DATA = "data"; - - /** Message name for this request. */ - public static final String NAME = "UnsubscribeTIC"; - - private List> keys = new ArrayList>(); - - /** Key descriptor for TIC identifier data. */ - protected KeyDescriptorList kData; - - /** Constructs a request for unsubscribing from TIC data with default values. */ - protected RequestUnsubscribeTIC() { - super(); - this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructs a request for unsubscribing from TIC data from a map of values. - * - * @param map the map containing request parameters - * @throws DataDictionaryException if validation fails - */ - public RequestUnsubscribeTIC(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a request for unsubscribing from TIC data from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public RequestUnsubscribeTIC(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a request for unsubscribing from TIC data with a specific list of identifiers. - * - * @param data the list of TIC identifiers - * @throws DataDictionaryException if validation fails - */ - public RequestUnsubscribeTIC(List data) throws DataDictionaryException { - this(); - this.setData(data); - this.checkAndUpdate(); - } - - /** - * Updates optional parameters for the request, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Returns the list of TIC identifier data associated with this request. - * - * @return the list of TIC identifiers - */ - @SuppressWarnings("unchecked") - public List getData() { - return (List) this.data.get(KEY_DATA); - } - - /** - * Sets the list of TIC identifier data for this request. - * - * @param data the list of TIC identifiers - * @throws DataDictionaryException if validation fails - */ - public void setData(List data) throws DataDictionaryException { - this.setData((Object) data); - } - - /** - * Internal setter for TIC identifier data, with conversion and validation. - * - * @param data the TIC identifier list (Object or List) - * @throws DataDictionaryException if validation fails - */ - protected void setData(Object data) throws DataDictionaryException { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /** - * Loads key descriptors for request parameters. - * - *

    Initializes the descriptor for TIC identifier data and adds it to the request. - */ - private void loadKeyDescriptors() { - try { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java deleted file mode 100644 index 18a1480..0000000 --- a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Response; -import enedis.tic.core.TICIdentifier; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Response message for available TIC identifiers. - * - *

    This class represents a response containing the list of available TIC identifiers. It provides - * constructors for various initialization scenarios and integrates with the response messaging - * system. - * - *

    Key features include: - * - *

      - *
    • Encapsulates response for available TIC identifiers - *
    • Supports construction from map, DataDictionary, or explicit parameter list - *
    • Validates and manages response parameters using key descriptors - *
    - * - * @author Enedis Smarties team - * @see Response - * @see TICIdentifier - */ -public class ResponseGetAvailableTICs extends Response { - /** Key for TIC identifier data in the response. */ - protected static final String KEY_DATA = "data"; - - /** Message name for this response. */ - public static final String NAME = "GetAvailableTICs"; - - private List> keys = new ArrayList>(); - - /** Key descriptor for TIC identifier data. */ - protected KeyDescriptorList kData; - - /** Constructs a response for available TIC identifiers with default values. */ - protected ResponseGetAvailableTICs() { - super(); - this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructs a response for available TIC identifiers from a map of values. - * - * @param map the map containing response parameters - * @throws DataDictionaryException if validation fails - */ - public ResponseGetAvailableTICs(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a response for available TIC identifiers from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public ResponseGetAvailableTICs(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a response for available TIC identifiers with explicit parameters. - * - * @param dateTime the response date and time - * @param errorCode the error code, if any - * @param errorMessage the error message, if any - * @param data the list of available TIC identifiers - * @throws DataDictionaryException if validation fails - */ - public ResponseGetAvailableTICs( - LocalDateTime dateTime, Number errorCode, String errorMessage, List data) - throws DataDictionaryException { - this(); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - this.checkAndUpdate(); - } - - /** - * Updates optional parameters for the response, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Returns the list of TIC identifier data associated with this response. - * - * @return the list of TIC identifiers - */ - @SuppressWarnings("unchecked") - public List getData() { - return (List) this.data.get(KEY_DATA); - } - - /** - * Sets the list of TIC identifier data for this response. - * - * @param data the list of TIC identifiers - * @throws DataDictionaryException if validation fails - */ - public void setData(List data) throws DataDictionaryException { - this.setData((Object) data); - } - - /** - * Internal setter for TIC identifier data, with conversion and validation. - * - * @param data the TIC identifier list (Object or List) - * @throws DataDictionaryException if validation fails - */ - protected void setData(Object data) throws DataDictionaryException { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /** - * Loads key descriptors for response parameters. - * - *

    Initializes the descriptor for TIC identifier data and adds it to the response. - */ - private void loadKeyDescriptors() { - try { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java b/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java deleted file mode 100644 index b04258b..0000000 --- a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Response; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Response message for modem information. - * - *

    This class represents a response containing the list of modem port descriptors. It provides - * constructors for various initialization scenarios and integrates with the response messaging - * system. - * - *

    Key features include: - * - *

      - *
    • Encapsulates response for modem information - *
    • Supports construction from map, DataDictionary, or explicit parameter list - *
    • Validates and manages response parameters using key descriptors - *
    - * - * @author Enedis Smarties team - * @see Response - * @see TICPortDescriptor - */ -public class ResponseGetModemsInfo extends Response { - /** Key for modem port descriptor data in the response. */ - protected static final String KEY_DATA = "data"; - - /** Message name for this response. */ - public static final String NAME = "GetModemsInfo"; - - private List> keys = new ArrayList>(); - - /** Key descriptor for modem port descriptor data. */ - protected KeyDescriptorList kData; - - /** Constructs a response for modem information with default values. */ - protected ResponseGetModemsInfo() { - super(); - this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructs a response for modem information from a map of values. - * - * @param map the map containing response parameters - * @throws DataDictionaryException if validation fails - */ - public ResponseGetModemsInfo(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a response for modem information from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public ResponseGetModemsInfo(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a response for modem information with explicit parameters. - * - * @param dateTime the response date and time - * @param errorCode the error code, if any - * @param errorMessage the error message, if any - * @param data the list of modem port descriptors - * @throws DataDictionaryException if validation fails - */ - public ResponseGetModemsInfo( - LocalDateTime dateTime, Number errorCode, String errorMessage, List data) - throws DataDictionaryException { - this(); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - this.checkAndUpdate(); - } - - /** - * Updates optional parameters for the response, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Returns the list of modem port descriptor data associated with this response. - * - * @return the list of modem port descriptors - */ - @SuppressWarnings("unchecked") - public List getData() { - return (List) this.data.get(KEY_DATA); - } - - /** - * Sets the list of modem port descriptor data for this response. - * - * @param data the list of modem port descriptors - * @throws DataDictionaryException if validation fails - */ - public void setData(List data) throws DataDictionaryException { - this.setData((Object) data); - } - - /** - * Internal setter for modem port descriptor data, with conversion and validation. - * - * @param data the modem port descriptor list (Object or List) - * @throws DataDictionaryException if validation fails - */ - protected void setData(Object data) throws DataDictionaryException { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /** - * Loads key descriptors for response parameters. - * - *

    Initializes the descriptor for modem port descriptor data and adds it to the response. - */ - private void loadKeyDescriptors() { - try { - this.kData = - new KeyDescriptorList(KEY_DATA, false, TICPortDescriptor.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java b/src/main/java/enedis/tic/service/message/ResponseReadTIC.java deleted file mode 100644 index 4f2a5c2..0000000 --- a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Response; -import enedis.tic.core.TICCoreFrame; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Response message for TIC frame data. - * - *

    This class represents a response containing a TIC frame. It provides constructors for various - * initialization scenarios and integrates with the response messaging system. - * - *

    Key features include: - * - *

      - *
    • Encapsulates response for TIC frame data - *
    • Supports construction from map, DataDictionary, or explicit parameter list - *
    • Validates and manages response parameters using key descriptors - *
    - * - * @author Enedis Smarties team - * @see Response - * @see TICCoreFrame - */ -public class ResponseReadTIC extends Response { - /** Key for TIC frame data in the response. */ - protected static final String KEY_DATA = "data"; - - /** Message name for this response. */ - public static final String NAME = "ReadTIC"; - - private List> keys = new ArrayList>(); - - /** Key descriptor for TIC frame data. */ - protected KeyDescriptorDataDictionary kData; - - /** Constructs a response for TIC frame data with default values. */ - protected ResponseReadTIC() { - super(); - this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructs a response for TIC frame data from a map of values. - * - * @param map the map containing response parameters - * @throws DataDictionaryException if validation fails - */ - public ResponseReadTIC(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a response for TIC frame data from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public ResponseReadTIC(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a response for TIC frame data with explicit parameters. - * - * @param dateTime the response date and time - * @param errorCode the error code, if any - * @param errorMessage the error message, if any - * @param data the TIC frame data - * @throws DataDictionaryException if validation fails - */ - public ResponseReadTIC( - LocalDateTime dateTime, Number errorCode, String errorMessage, TICCoreFrame data) - throws DataDictionaryException { - this(); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - this.checkAndUpdate(); - } - - /** - * Updates optional parameters for the response, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Returns the TIC frame data associated with this response. - * - * @return the TIC frame data - */ - public TICCoreFrame getData() { - return (TICCoreFrame) this.data.get(KEY_DATA); - } - - /** - * Sets the TIC frame data for this response. - * - * @param data the TIC frame data - * @throws DataDictionaryException if validation fails - */ - public void setData(TICCoreFrame data) throws DataDictionaryException { - this.setData((Object) data); - } - - /** - * Internal setter for TIC frame data, with conversion and validation. - * - * @param data the TIC frame data (Object or TICCoreFrame) - * @throws DataDictionaryException if validation fails - */ - protected void setData(Object data) throws DataDictionaryException { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /** - * Loads key descriptors for response parameters. - * - *

    Initializes the descriptor for TIC frame data and adds it to the response. - */ - private void loadKeyDescriptors() { - try { - this.kData = - new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreFrame.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java deleted file mode 100644 index 391e617..0000000 --- a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.util.message.Response; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Response message for TIC subscription. - * - *

    This class represents a response to a TIC subscription request. It provides constructors for - * various initialization scenarios and integrates with the response messaging system. - * - *

    Key features include: - * - *

      - *
    • Encapsulates response for TIC subscription - *
    • Supports construction from map, DataDictionary, or explicit parameter list - *
    • Validates and manages response parameters using key descriptors - *
    - * - * @author Enedis Smarties team - * @see Response - */ -public class ResponseSubscribeTIC extends Response { - /** Message name for this response. */ - public static final String NAME = "SubscribeTIC"; - - private List> keys = new ArrayList>(); - - /** Constructs a response for TIC subscription with default values. */ - protected ResponseSubscribeTIC() { - super(); - this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructs a response for TIC subscription from a map of values. - * - * @param map the map containing response parameters - * @throws DataDictionaryException if validation fails - */ - public ResponseSubscribeTIC(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a response for TIC subscription from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public ResponseSubscribeTIC(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a response for TIC subscription with explicit parameters. - * - * @param dateTime the response date and time - * @param errorCode the error code, if any - * @param errorMessage the error message, if any - * @throws DataDictionaryException if validation fails - */ - public ResponseSubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) - throws DataDictionaryException { - this(); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.checkAndUpdate(); - } - - /** - * Updates optional parameters for the response, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Loads key descriptors for response parameters. - * - *

    Initializes the descriptors and adds them to the response. - */ - private void loadKeyDescriptors() { - try { - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java deleted file mode 100644 index be29ca3..0000000 --- a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.util.message.Response; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Response message for TIC unsubscription. - * - *

    This class represents a response to a TIC unsubscription request. It provides constructors for - * various initialization scenarios and integrates with the response messaging system. - * - *

    Key features include: - * - *

      - *
    • Encapsulates response for TIC unsubscription - *
    • Supports construction from map, DataDictionary, or explicit parameter list - *
    • Validates and manages response parameters using key descriptors - *
    - * - * @author Enedis Smarties team - * @see Response - */ -public class ResponseUnsubscribeTIC extends Response { - /** Message name for this response. */ - public static final String NAME = "UnsubscribeTIC"; - - private List> keys = new ArrayList>(); - - /** Constructs a response for TIC unsubscription with default values. */ - protected ResponseUnsubscribeTIC() { - super(); - this.loadKeyDescriptors(); - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructs a response for TIC unsubscription from a map of values. - * - * @param map the map containing response parameters - * @throws DataDictionaryException if validation fails - */ - public ResponseUnsubscribeTIC(Map map) throws DataDictionaryException { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructs a response for TIC unsubscription from another DataDictionary instance. - * - * @param other the DataDictionary to copy from - * @throws DataDictionaryException if validation fails - */ - public ResponseUnsubscribeTIC(DataDictionary other) throws DataDictionaryException { - this(); - this.copy(other); - } - - /** - * Constructs a response for TIC unsubscription with explicit parameters. - * - * @param dateTime the response date and time - * @param errorCode the error code, if any - * @param errorMessage the error message, if any - * @throws DataDictionaryException if validation fails - */ - public ResponseUnsubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) - throws DataDictionaryException { - this(); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.checkAndUpdate(); - } - - /** - * Updates optional parameters for the response, ensuring the name is set. - * - * @throws DataDictionaryException if validation fails - */ - @Override - protected void updateOptionalParameters() throws DataDictionaryException { - if (!this.exists(KEY_NAME)) { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /** - * Loads key descriptors for response parameters. - * - *

    Initializes the descriptors and adds them to the response. - */ - private void loadKeyDescriptors() { - try { - this.addAllKeyDescriptor(this.keys); - } catch (DataDictionaryException e) { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java deleted file mode 100644 index 5b618bc..0000000 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message.factory; - -import enedis.lab.util.message.factory.EventFactory; -import enedis.tic.service.message.EventOnError; -import enedis.tic.service.message.EventOnTICData; - -/** - * Factory for creating TIC2WebSocket event messages. - * - *

    This class extends {@link EventFactory} to register and instantiate event message types - * specific to the TIC2WebSocket protocol, including TIC data and error events. - * - *

    Key features include: - * - *

      - *
    • Registers {@link EventOnTICData} and {@link EventOnError} message classes - *
    • Supports dynamic event creation by message name - *
    - * - * @author Enedis Smarties team - * @see EventFactory - * @see EventOnTICData - * @see EventOnError - */ -public class TIC2WebSocketEventFactory extends EventFactory { - /** Constructs a new event factory and registers TIC2WebSocket event message types. */ - public TIC2WebSocketEventFactory() { - super(); - this.addMessageClass(EventOnTICData.NAME, EventOnTICData.class); - this.addMessageClass(EventOnError.NAME, EventOnError.class); - } -} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java deleted file mode 100644 index 5d583af..0000000 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message.factory; - -import enedis.lab.util.message.factory.MessageFactory; - -/** - * Factory for creating TIC2WebSocket protocol messages. - * - *

    This class extends {@link MessageFactory} to register and instantiate request, response, and - * event message types specific to the TIC2WebSocket protocol. It aggregates the protocol's request, - * response, and event factories for unified message creation. - * - *

    Key features include: - * - *

      - *
    • Centralized registration of request, response, and event factories - *
    • Supports dynamic message creation for all protocol message types - *
    - * - * @author Enedis Smarties team - * @see MessageFactory - * @see TIC2WebSocketRequestFactory - * @see TIC2WebSocketResponseFactory - * @see TIC2WebSocketEventFactory - */ -public class TIC2WebSocketMessageFactory extends MessageFactory { - /** - * Constructs a new message factory and registers TIC2WebSocket request, response, and event - * factories. - */ - public TIC2WebSocketMessageFactory() { - super( - new TIC2WebSocketRequestFactory(), - new TIC2WebSocketResponseFactory(), - new TIC2WebSocketEventFactory()); - } -} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java deleted file mode 100644 index 059cf26..0000000 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message.factory; - -import enedis.lab.util.message.factory.RequestFactory; -import enedis.tic.service.message.RequestGetAvailableTICs; -import enedis.tic.service.message.RequestGetModemsInfo; -import enedis.tic.service.message.RequestReadTIC; -import enedis.tic.service.message.RequestSubscribeTIC; -import enedis.tic.service.message.RequestUnsubscribeTIC; - -/** - * Factory for creating TIC2WebSocket request messages. - * - *

    This class extends {@link RequestFactory} to register and instantiate request message types - * specific to the TIC2WebSocket protocol, including requests for TICs, modems, subscriptions, and - * reads. - * - *

    Key features include: - * - *

      - *
    • Registers all supported TIC2WebSocket request message classes - *
    • Supports dynamic request creation by message name - *
    - * - * @author Enedis Smarties team - * @see RequestFactory - * @see RequestGetAvailableTICs - * @see RequestGetModemsInfo - * @see RequestReadTIC - * @see RequestSubscribeTIC - * @see RequestUnsubscribeTIC - */ -public class TIC2WebSocketRequestFactory extends RequestFactory { - /** Constructs a new request factory and registers TIC2WebSocket request message types. */ - public TIC2WebSocketRequestFactory() { - super(); - this.addMessageClass(RequestGetAvailableTICs.NAME, RequestGetAvailableTICs.class); - this.addMessageClass(RequestGetModemsInfo.NAME, RequestGetModemsInfo.class); - this.addMessageClass(RequestReadTIC.NAME, RequestReadTIC.class); - this.addMessageClass(RequestSubscribeTIC.NAME, RequestSubscribeTIC.class); - this.addMessageClass(RequestUnsubscribeTIC.NAME, RequestUnsubscribeTIC.class); - } -} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java deleted file mode 100644 index ea26094..0000000 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message.factory; - -import enedis.lab.util.message.factory.ResponseFactory; -import enedis.tic.service.message.ResponseGetAvailableTICs; -import enedis.tic.service.message.ResponseGetModemsInfo; -import enedis.tic.service.message.ResponseReadTIC; -import enedis.tic.service.message.ResponseSubscribeTIC; -import enedis.tic.service.message.ResponseUnsubscribeTIC; - -/** - * Factory for creating TIC2WebSocket response messages. - * - *

    This class extends {@link ResponseFactory} to register and instantiate response message types - * specific to the TIC2WebSocket protocol, including responses for TICs, modems, subscriptions, and - * reads. - * - *

    Key features include: - * - *

      - *
    • Registers all supported TIC2WebSocket response message classes - *
    • Supports dynamic response creation by message name - *
    - * - * @author Enedis Smarties team - * @see ResponseFactory - * @see ResponseGetAvailableTICs - * @see ResponseGetModemsInfo - * @see ResponseReadTIC - * @see ResponseSubscribeTIC - * @see ResponseUnsubscribeTIC - */ -public class TIC2WebSocketResponseFactory extends ResponseFactory { - /** Constructs a new response factory and registers TIC2WebSocket response message types. */ - public TIC2WebSocketResponseFactory() { - super(); - this.addMessageClass(ResponseGetAvailableTICs.NAME, ResponseGetAvailableTICs.class); - this.addMessageClass(ResponseGetModemsInfo.NAME, ResponseGetModemsInfo.class); - this.addMessageClass(ResponseReadTIC.NAME, ResponseReadTIC.class); - this.addMessageClass(ResponseSubscribeTIC.NAME, ResponseSubscribeTIC.class); - this.addMessageClass(ResponseUnsubscribeTIC.NAME, ResponseUnsubscribeTIC.class); - } -} diff --git a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java b/src/main/java/tic/core/ReadNextFrameSubscriber.java similarity index 98% rename from src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java rename to src/main/java/tic/core/ReadNextFrameSubscriber.java index ef41414..7ee9ae3 100644 --- a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java +++ b/src/main/java/tic/core/ReadNextFrameSubscriber.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; +package tic.core; /** * Subscriber implementation for receiving the next frame and error notifications. diff --git a/src/main/java/enedis/tic/core/TICCore.java b/src/main/java/tic/core/TICCore.java similarity index 94% rename from src/main/java/enedis/tic/core/TICCore.java rename to src/main/java/tic/core/TICCore.java index c74fc1b..38a96b7 100644 --- a/src/main/java/enedis/tic/core/TICCore.java +++ b/src/main/java/tic/core/TICCore.java @@ -5,11 +5,11 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; +package tic.core; -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.util.task.Task; import java.util.List; +import tic.io.modem.ModemDescriptor; +import tic.util.task.Task; /** * Core interface for managing frame reading and subscriber notifications. @@ -40,7 +40,7 @@ public interface TICCore extends Task { * * @return The collection of TIC port descriptors */ - public List getModemsInfo(); + public List getModemsInfo(); /** * Read next frame diff --git a/src/main/java/enedis/tic/core/TICCoreBase.java b/src/main/java/tic/core/TICCoreBase.java similarity index 66% rename from src/main/java/enedis/tic/core/TICCoreBase.java rename to src/main/java/tic/core/TICCoreBase.java index 1ecd8f0..4a2cc9b 100644 --- a/src/main/java/enedis/tic/core/TICCoreBase.java +++ b/src/main/java/tic/core/TICCoreBase.java @@ -5,22 +5,8 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; - -import enedis.lab.io.PlugSubscriber; -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.io.tic.TICPortFinder; -import enedis.lab.io.tic.TICPortFinderBase; -import enedis.lab.io.tic.TICPortPlugNotifier; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.task.FilteredNotifier; -import enedis.lab.util.task.FilteredNotifierBase; -import enedis.lab.util.task.Task; -import enedis.lab.util.task.TaskBase; -import enedis.lab.util.time.Time; -import java.lang.reflect.Constructor; +package tic.core; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -30,6 +16,22 @@ import java.util.function.Predicate; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import tic.core.codec.TICCoreErrorCodec; +import tic.core.codec.TICCoreFrameCodec; +import tic.frame.TICMode; +import tic.io.PlugSubscriber; +import tic.io.modem.ModemDescriptor; +import tic.io.modem.ModemFinder; +import tic.io.modem.ModemFinderBase; +import tic.io.modem.ModemJsonCodec; +import tic.io.modem.ModemPlugNotifier; +import tic.io.serialport.SerialPortFinderBase; +import tic.io.usb.UsbPortFinderBase; +import tic.util.task.FilteredNotifier; +import tic.util.task.FilteredNotifierBase; +import tic.util.task.Task; +import tic.util.task.TaskBase; +import tic.util.time.Time; /** * Core implementation for managing frame reading and subscriber notifications. @@ -47,16 +49,14 @@ * * @author Enedis Smarties team */ -public class TICCoreBase - implements TICCore, Task, TICCoreSubscriber, PlugSubscriber { +public class TICCoreBase implements TICCore, TICCoreSubscriber, PlugSubscriber { private static final int PLUG_NOTIFIER_POLLING_PERIOD = 100; private static final int READ_NEXT_FRAME_TIMEOUT = 30000; private static final int READ_NEXT_FRAME_POLLING_PERIOD = 100; - private TICPortFinder portFinder; - private TICPortPlugNotifier plugNotifier; + private ModemFinder modemFinder; + private ModemPlugNotifier plugNotifier; private long plugNotifierPeriod; - private Constructor streamConstructor; private TICMode streamMode; private List nativePortNamesOnStart; private Collection streamList; @@ -69,29 +69,20 @@ public TICCoreBase() { public TICCoreBase(TICMode streamMode, List nativePortNamesStart) { this( - TICPortFinderBase.getInstance(), + ModemFinderBase.create(SerialPortFinderBase.getInstance(), UsbPortFinderBase.getInstance()), PLUG_NOTIFIER_POLLING_PERIOD, - TICCoreStreamBase.class, streamMode, nativePortNamesStart); } public TICCoreBase( - TICPortFinder ticPortFinder, + ModemFinder modemFinder, long plugNotifierPeriod, - Class streamClass, TICMode streamMode, List nativePortNamesOnStart) { super(); - this.portFinder = ticPortFinder; + this.modemFinder = modemFinder; this.plugNotifierPeriod = plugNotifierPeriod; - try { - this.streamConstructor = - streamClass.getConstructor(String.class, String.class, TICMode.class); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException( - "Class " + streamClass.getName() + " has no valid constructor : " + e.getMessage()); - } if (streamMode == null) { throw new IllegalArgumentException("TICMode should be defined"); } @@ -103,23 +94,39 @@ public TICCoreBase( @Override public List getAvailableTICs() { - List identifiers = new DataArrayList(); + List identifiers = new ArrayList<>(); for (TICCoreStream stream : this.streamList) { - identifiers.add(stream.getIdentifier()); + if (stream == null) { + continue; + } + + TICIdentifier identifier = stream.getIdentifier(); + if (!identifiers.contains(identifier)) { + identifiers.add(identifier); + } } return identifiers; } @Override - public List getModemsInfo() { - List descriptors = new DataArrayList(); + public List getModemsInfo() { + List descriptors = new ArrayList<>(); for (TICCoreStream stream : this.streamList) { - TICPortDescriptor descriptor = - this.portFinder.findByPortName(stream.getIdentifier().getPortName()); - descriptors.add(descriptor); + if (stream == null) { + continue; + } + + TICIdentifier identifier = stream.getIdentifier(); + ModemDescriptor descriptor = null; + if (identifier != null && identifier.getPortName() != null) { + descriptor = this.modemFinder.findByPortName(identifier.getPortName()); + } + if (!descriptors.contains(descriptor)) { + descriptors.add(descriptor); + } } return descriptors; @@ -133,17 +140,17 @@ public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreExcept @Override public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException { TICCoreFrame frame = null; - TICPortDescriptor descriptor = null; + ModemDescriptor descriptor = null; TICCoreStream stream = this.findStream(identifier); if (stream == null) { - descriptor = this.portFinder.findNative(identifier.getPortName()); + descriptor = this.modemFinder.findNative(identifier.getPortName()); if (descriptor == null) { TICCoreException exception = new TICCoreException( TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); - logger.error(exception.getMessage(), exception); + logger.error(exception.getMessage()); throw exception; } stream = this.startNewStream(descriptor); @@ -159,7 +166,7 @@ public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws new TICCoreException( subscriber.getError().getErrorCode().intValue(), subscriber.getError().getErrorMessage()); - logger.error(exception.getMessage(), exception); + logger.error(exception.getMessage()); stream.unsubscribe(subscriber); this.closeNativeStream(identifier, stream, subscriber); throw exception; @@ -178,7 +185,7 @@ public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws new TICCoreException( TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), "Stream " + identifier + " data read timeout !"); - logger.error(exception.getMessage(), exception); + logger.error(exception.getMessage()); throw exception; } return frame; @@ -190,7 +197,7 @@ public void subscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) TICCoreStream stream = this.findStream(identifier); if (stream == null) { - TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); + ModemDescriptor descriptor = this.modemFinder.findNative(identifier.getPortName()); if (descriptor == null) { TICCoreException exception = new TICCoreException( @@ -211,7 +218,7 @@ public void subscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) @Override public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException { - TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); + ModemDescriptor descriptor = this.modemFinder.findNative(identifier.getPortName()); if (descriptor != null) { if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) { @@ -253,7 +260,11 @@ public List getIndentifiers(TICCoreSubscriber listener) { @Override public void onData(TICCoreFrame frame) { - logger.trace("TICCore frame:\n" + frame.toString(2)); + try { + logger.trace("TICCore frame:\n" + TICCoreFrameCodec.getInstance().encodeToJsonString(frame)); + } catch (Exception e) { + logger.error("Error encoding TICCore frame to JSON string", e); + } Collection subscriberList = this.findSubscribers(frame.getIdentifier(), true); Task task = @@ -268,12 +279,15 @@ public void process() { @Override public void onError(TICCoreError error) { - logger.error("TICCore error:\n" + error.toString(2)); + try { + logger.trace("TICCore error:\n" + TICCoreErrorCodec.getInstance().encodeToJsonString(error)); + } catch (Exception e) { + logger.error("Error encoding TICCore error to JSON string", e); + } Collection subscriberList = this.findSubscribers(error.getIdentifier(), true); Task task = new TaskBase() { - @Override public void process() { TICCoreBase.this.notifyOnError(error, subscriberList); } @@ -282,14 +296,22 @@ public void process() { } @Override - public void onPlugged(TICPortDescriptor descriptor) { - logger.info("TICCore modem plugged:\n" + descriptor.toString(2)); + public void onPlugged(ModemDescriptor descriptor) { + try { + logger.info("TICCore modem plugged:\n" + ModemJsonCodec.getInstance().encodeToJsonString(descriptor)); + } catch (Exception e) { + logger.error("Error encoding ModemDescriptor to JSON string", e); + } this.startNewStream(descriptor); } @Override - public void onUnplugged(TICPortDescriptor descriptor) { - logger.info("TICCore modem unplugged:\n" + descriptor.toString(2)); + public void onUnplugged(ModemDescriptor descriptor) { + try { + logger.info("TICCore modem unplugged:\n" + ModemJsonCodec.getInstance().encodeToJsonString(descriptor)); + } catch (Exception e) { + logger.error("Error encoding ModemDescriptor to JSON string", e); + } TICIdentifier identifier = this.stopStream(descriptor); Task task = new TaskBase() { @@ -306,7 +328,7 @@ public void start() { if (!this.isRunning()) { logger.info("Starting TICCore"); logger.debug("Starting TIC port plug notifier"); - this.plugNotifier = new TICPortPlugNotifier(this.plugNotifierPeriod, this.portFinder); + this.plugNotifier = new ModemPlugNotifier(this.plugNotifierPeriod, this.modemFinder); this.plugNotifier.subscribe(this); this.plugNotifier.start(); logger.debug("TIC port plug notifier started"); @@ -314,8 +336,13 @@ public void start() { logger.debug( "Starting natives TIC port: " + Arrays.toString(this.nativePortNamesOnStart.toArray())); for (String portName : this.nativePortNamesOnStart) { - TICPortDescriptor descriptor = this.portFinder.findNative(portName); - logger.debug("Starting native TIC port " + portName + " : " + descriptor); + ModemDescriptor descriptor = this.modemFinder.findNative(portName); + try { + logger.debug( + "Starting native TIC port " + portName + " : " + ModemJsonCodec.getInstance().encodeToJsonString(descriptor)); + } catch (Exception e) { + logger.error("Error encoding ModemDescriptor to JSON string", e); + } if (descriptor != null && this.findStream(descriptor) == null) { this.startNewStream(descriptor); } @@ -355,16 +382,16 @@ private TICCoreStream findStream(TICIdentifier identifier) { return null; } - private TICCoreStream findStream(TICPortDescriptor descriptor) { + private TICCoreStream findStream(ModemDescriptor descriptor) { for (TICCoreStream stream : this.streamList) { TICIdentifier identifier = stream.getIdentifier(); - if (descriptor.getPortId() != null && identifier.getPortId() != null) { - if (descriptor.getPortId().equals(identifier.getPortId())) { + if (descriptor.portId() != null && identifier.getPortId() != null) { + if (descriptor.portId().equals(identifier.getPortId())) { return stream; } } - if (descriptor.getPortName() != null && identifier.getPortName() != null) { - if (descriptor.getPortName().equals(identifier.getPortName())) { + if (descriptor.portName() != null && identifier.getPortName() != null) { + if (descriptor.portName().equals(identifier.getPortName())) { return stream; } } @@ -373,35 +400,62 @@ private TICCoreStream findStream(TICPortDescriptor descriptor) { return null; } - private TICCoreStream startNewStream(TICPortDescriptor descriptor) { - TICCoreStream stream = null; - logger.debug("TICCore starting new stream : " + descriptor.toString()); + private TICCoreStream startNewStream(ModemDescriptor descriptor) { + if (descriptor == null) { + return null; + } + + TICCoreStream existing = this.findStream(descriptor); + if (existing != null) { + if (existing.isRunning()) { + return existing; + } + this.streamList.remove(existing); + } + + try { + logger.debug("TICCore starting new stream : " + ModemJsonCodec.getInstance().encodeToJsonString(descriptor)); + } catch (Exception e) { + logger.error("Error encoding ModemDescriptor to JSON string", e); + } try { - stream = - this.streamConstructor.newInstance( - descriptor.getPortId(), descriptor.getPortName(), this.streamMode); + TICCoreStream stream = + TICCoreStreamBase.create( + descriptor.portId(), descriptor.portName(), this.streamMode, this.modemFinder); stream.subscribe(this); - stream.start(); this.streamList.add(stream); - logger.debug("TICCore started new stream : " + descriptor.toString()); + try { + logger.debug("TICCore started new stream : " + ModemJsonCodec.getInstance().encodeToJsonString(descriptor)); + } catch (Exception e) { + logger.error("Error encoding ModemDescriptor to JSON string", e); + } + return stream; } catch (Exception e) { logger.error(e.getMessage(), e); + return null; } - return stream; } - private TICIdentifier stopStream(TICPortDescriptor descriptor) { + private TICIdentifier stopStream(ModemDescriptor descriptor) { TICIdentifier identifier = null; TICCoreStream stream = this.findStream(descriptor); - logger.debug("TICCore stopping new stream : " + descriptor.toString()); + try { + logger.debug("TICCore stopping stream : " + ModemJsonCodec.getInstance().encodeToJsonString(descriptor)); + } catch (Exception e) { + logger.error("Error encoding ModemDescriptor to JSON string", e); + } if (stream != null) { identifier = stream.getIdentifier(); stream.unsubscribe(this); stream.stop(); this.streamList.remove(stream); - logger.debug("TICCore stopped new stream : " + descriptor.toString()); + try { + logger.debug("TICCore stopped stream : " + ModemJsonCodec.getInstance().encodeToJsonString(descriptor)); + } catch (Exception e) { + logger.error("Error encoding ModemDescriptor to JSON string", e); + } } return identifier; @@ -409,7 +463,7 @@ private TICIdentifier stopStream(TICPortDescriptor descriptor) { private void closeNativeStream( TICIdentifier identifier, TICCoreStream stream, ReadNextFrameSubscriber subscriber) { - TICPortDescriptor nativeDescriptor = this.portFinder.findNative(identifier.getPortName()); + ModemDescriptor nativeDescriptor = this.modemFinder.findNative(identifier.getPortName()); if (nativeDescriptor != null) { if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) { @@ -439,19 +493,15 @@ public boolean test(TICIdentifier identifier) { } private void notifyOnUnpluggedAndUnsubscribe(TICIdentifier identifier) { - try { - TICCoreError error = - new TICCoreError( - identifier, - TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), - "TICCore stream " + identifier + " has been unplugged (unsubscribe has been forced)"); - Collection subscriberList = this.findSubscribers(identifier, true); - this.notifyOnError(error, subscriberList); - subscriberList = this.findSubscribers(identifier, false); - this.unsubscribe(subscriberList); - } catch (DataDictionaryException e) { - logger.error(e.getMessage(), e); - } + TICCoreError error = + new TICCoreError( + identifier, + TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), + "TICCore stream " + identifier + " has been unplugged (unsubscribe has been forced)"); + Collection subscriberList = this.findSubscribers(identifier, true); + this.notifyOnError(error, subscriberList); + subscriberList = this.findSubscribers(identifier, false); + this.unsubscribe(subscriberList); } private void notifyOnData(TICCoreFrame frame, Collection subscriberList) { diff --git a/src/main/java/tic/core/TICCoreError.java b/src/main/java/tic/core/TICCoreError.java new file mode 100644 index 0000000..85c4c0d --- /dev/null +++ b/src/main/java/tic/core/TICCoreError.java @@ -0,0 +1,112 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.core; + +import java.util.Objects; + +import tic.frame.TICFrame; + +/** + * Class representing a core error with identifier, code, message, and optional data. + * + *

    This class provides mechanisms for constructing, accessing, and managing error information + * including error codes, messages, and associated data. It is designed for general-purpose error + * handling. + * + *

    Common use cases include: + * + *

      + *
    • Representing errors with structured data + *
    • Managing error codes and messages + *
    • Associating additional data with errors + *
    + * + * @author Enedis Smarties team + */ +public class TICCoreError { + + private final TICIdentifier identifier; + private final Number errorCode; + private final String errorMessage; + private final TICFrame frame; + + /** + * Constructor setting parameters to specific values + * + * @param identifier + * @param errorCode + * @param errorMessage + */ + public TICCoreError(TICIdentifier identifier, Number errorCode, String errorMessage) { + this(identifier, errorCode, errorMessage, null); + } + + /** + * Constructor setting parameters to specific values + * + * @param identifier + * @param errorCode + * @param errorMessage + * @param frame + */ + public TICCoreError( + TICIdentifier identifier, Number errorCode, String errorMessage, TICFrame frame) { + this.identifier = Objects.requireNonNull(identifier, "identifier must not be null"); + this.errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null"); + this.errorMessage = Objects.requireNonNull(errorMessage, "errorMessage must not be null"); + this.frame = frame; + } + + /** + * Get identifier + * + * @return the identifier + */ + public TICIdentifier getIdentifier() { + return this.identifier; + } + + /** + * Get error code + * + * @return the error code + */ + public Number getErrorCode() { + return this.errorCode; + } + + /** + * Get error message + * + * @return the error message + */ + public String getErrorMessage() { + return this.errorMessage; + } + + /** + * Get frame + * + * @return the frame + */ + public TICFrame getFrame() { + return this.frame; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("TICCoreError {\n"); + sb.append(" identifier: ").append(this.identifier).append(",\n"); + sb.append(" errorCode: ").append(this.errorCode).append(",\n"); + sb.append(" errorMessage: ").append(this.errorMessage).append(",\n"); + sb.append(" frame: ").append(this.frame).append("\n"); + sb.append("}"); + return sb.toString(); + } +} diff --git a/src/main/java/enedis/tic/core/TICCoreErrorCode.java b/src/main/java/tic/core/TICCoreErrorCode.java similarity index 98% rename from src/main/java/enedis/tic/core/TICCoreErrorCode.java rename to src/main/java/tic/core/TICCoreErrorCode.java index c4baf36..b25acf7 100644 --- a/src/main/java/enedis/tic/core/TICCoreErrorCode.java +++ b/src/main/java/tic/core/TICCoreErrorCode.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; +package tic.core; /** * Enumeration of core error codes for error handling and diagnostics. diff --git a/src/main/java/enedis/tic/core/TICCoreException.java b/src/main/java/tic/core/TICCoreException.java similarity index 52% rename from src/main/java/enedis/tic/core/TICCoreException.java rename to src/main/java/tic/core/TICCoreException.java index 1d97620..815210f 100644 --- a/src/main/java/enedis/tic/core/TICCoreException.java +++ b/src/main/java/tic/core/TICCoreException.java @@ -5,9 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; - -import enedis.lab.types.ExceptionBase; +package tic.core; /** * Exception class for representing core errors and failures. @@ -17,10 +15,27 @@ * * @author Enedis Smarties team */ -public class TICCoreException extends ExceptionBase { +public class TICCoreException extends Exception { private static final long serialVersionUID = -3285641164559292710L; - public TICCoreException(int code, String info) { - super(code, info); + private final int errorCode; + private final String errorInfo; + + public TICCoreException(int errorCode, String errorInfo) { + this.errorCode = errorCode; + this.errorInfo = errorInfo; + } + + @Override + public String getMessage() { + return this.errorInfo + " (" + this.errorCode + ")"; + } + + public int getErrorCode() { + return this.errorCode; + } + + public String getErrorInfo() { + return this.errorInfo; } } diff --git a/src/main/java/tic/core/TICCoreFrame.java b/src/main/java/tic/core/TICCoreFrame.java new file mode 100644 index 0000000..2904fc9 --- /dev/null +++ b/src/main/java/tic/core/TICCoreFrame.java @@ -0,0 +1,96 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.core; + +import java.time.LocalDateTime; +import java.util.Objects; +import tic.frame.TICFrame; +import tic.frame.TICMode; + +/** + * Class representing a core frame with identifier, mode, capture time, and content. + * + *

    This class provides mechanisms for constructing, accessing, and managing frame information + * including identifiers, modes, timestamps, and associated content. It is designed for + * general-purpose frame handling. + * + *

    Common use cases include: + * + *

      + *
    • Representing frames with structured data + *
    • Managing frame identifiers and modes + *
    • Associating content and timestamps with frames + *
    + * + * @author Enedis Smarties team + */ +public class TICCoreFrame { + + private final TICIdentifier identifier; + private final TICMode mode; + private final LocalDateTime captureDateTime; + private final TICFrame frame; + + public TICCoreFrame( + TICIdentifier identifier, TICMode mode, LocalDateTime captureDateTime, TICFrame frame) { + this.identifier = Objects.requireNonNull(identifier, "identifier must not be null"); + this.mode = Objects.requireNonNull(mode, "mode must not be null"); + this.captureDateTime = + Objects.requireNonNull(captureDateTime, "captureDateTime must not be null"); + this.frame = Objects.requireNonNull(frame, "frame must not be null"); + } + + /** + * Get identifier + * + * @return the identifier + */ + public TICIdentifier getIdentifier() { + return this.identifier; + } + + /** + * Get mode + * + * @return the mode + */ + public TICMode getMode() { + return this.mode; + } + + /** + * Get capture date time + * + * @return the capture date time + */ + public LocalDateTime getCaptureDateTime() { + return this.captureDateTime; + } + + /** + * Get content + * + * @return the content + */ + public TICFrame getFrame() { + return this.frame; + } + + @Override + public String toString() { + return "{identifier=" + + this.identifier + + ", mode=" + + this.mode + + ", captureDateTime=" + + this.captureDateTime + + ", frame=" + + this.frame + + "}"; + } +} diff --git a/src/main/java/enedis/tic/core/TICCoreStream.java b/src/main/java/tic/core/TICCoreStream.java similarity index 90% rename from src/main/java/enedis/tic/core/TICCoreStream.java rename to src/main/java/tic/core/TICCoreStream.java index c9fa0b2..b1351dd 100644 --- a/src/main/java/enedis/tic/core/TICCoreStream.java +++ b/src/main/java/tic/core/TICCoreStream.java @@ -5,10 +5,10 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; +package tic.core; -import enedis.lab.util.task.Notifier; -import enedis.lab.util.task.Task; +import tic.util.task.Notifier; +import tic.util.task.Task; /** * Interface for managing core streams, frame acquisition, and subscriber notifications. diff --git a/src/main/java/tic/core/TICCoreStreamBase.java b/src/main/java/tic/core/TICCoreStreamBase.java new file mode 100644 index 0000000..d569a23 --- /dev/null +++ b/src/main/java/tic/core/TICCoreStreamBase.java @@ -0,0 +1,277 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.core; + +import java.time.LocalDateTime; +import java.util.Collection; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tic.frame.TICFrame; +import tic.frame.TICMode; +import tic.frame.group.TICGroup; +import tic.io.modem.ModemDescriptor; +import tic.io.modem.ModemFinder; +import tic.stream.TICStream; +import tic.stream.TICStreamListener; +import tic.stream.configuration.TICStreamConfiguration; +import tic.stream.identifier.SerialPortName; +import tic.stream.identifier.TICStreamIdentifier; +import tic.util.task.Notifier; +import tic.util.task.NotifierBase; +import tic.util.task.Task; +import tic.util.task.TaskBase; + +/** + * Core stream implementation for frame acquisition and subscriber notifications. + * + *

    This class provides mechanisms for managing streams, acquiring frames, handling errors, and + * notifying subscribers. It implements the contract for event-driven stream operations. + * + *

    Common use cases include: + * + *

      + *
    • Acquiring frames from streams + *
    • Managing and notifying subscribers + *
    • Handling errors and stream lifecycle + *
    + * + * @author Enedis Smarties team + */ +public class TICCoreStreamBase implements TICCoreStream { + + private final Object identifierLock = new Object(); + private TICIdentifier identifier; + private final TICStream stream; + private final TICStreamListener streamListener; + private final Notifier notifier; + private static Logger logger = LogManager.getLogger(); + + public static TICCoreStream create( + String portId, String portName, TICMode ticMode, ModemFinder modemFinder) + throws TICCoreException { + ModemDescriptor descriptor = null; + TICIdentifier identifier = null; + TICStream stream = null; + Notifier notifier = null; + + if (portId != null) { + descriptor = modemFinder.findByPortId(portId); + if (descriptor == null) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_PORT_ID_NOT_FOUND.getCode(), + "TICCore stream port id " + portId + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + } else if (portName != null) { + descriptor = modemFinder.findByPortName(portName); + if (descriptor == null) { + descriptor = modemFinder.findNative(portName); + if (descriptor == null) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_PORT_NAME_NOT_FOUND.getCode(), + "TICCore stream port name " + portName + " not found!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + } + } else { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_PORT_DESCRIPTOR_EMPTY.getCode(), + "TICCore stream port descriptor empty!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + if (ticMode == null) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_MODE_NOT_DEFINED.getCode(), + "TICCore stream mode not defined!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + + String resolvedPortId = descriptor.portId(); + String resolvedPortName = descriptor.portName(); + if (resolvedPortName == null || resolvedPortName.trim().isEmpty()) { + TICCoreException exception = + new TICCoreException( + TICCoreErrorCode.STREAM_PORT_NAME_NOT_FOUND.getCode(), + "TICCore stream resolved port name is empty!"); + logger.error(exception.getMessage(), exception); + throw exception; + } + + identifier = + new TICIdentifier.Builder() + .portId(resolvedPortId) + .portName(resolvedPortName) + .build(); + notifier = new NotifierBase(); + + TICStreamIdentifier streamIdentifier = + new TICStreamIdentifier(new SerialPortName(resolvedPortName)); + TICStreamConfiguration configuration = + new TICStreamConfiguration( + ticMode, streamIdentifier, TICStreamConfiguration.DEFAULT_TIMEOUT); + stream = new TICStream(configuration); + + return new TICCoreStreamBase(identifier, stream, notifier); + } + + private TICCoreStreamBase( + TICIdentifier identifier, TICStream stream, Notifier notifier) + throws TICCoreException { + this.identifier = identifier; + this.stream = stream; + this.notifier = notifier; + + this.streamListener = + new TICStreamListener() { + @Override + public void onFrame(TICFrame ticFrame) { + TICCoreStreamBase.this.onFrame(ticFrame); + } + + @Override + public void onError(String errorMessage) { + TICCoreStreamBase.this.onError(errorMessage); + } + }; + } + + @Override + public TICIdentifier getIdentifier() { + synchronized (this.identifierLock) { + return this.identifier; + } + } + + @Override + public Collection getSubscribers() { + return this.notifier.getSubscribers(); + } + + @Override + public boolean hasSubscriber(TICCoreSubscriber subscriber) { + return this.notifier.hasSubscriber(subscriber); + } + + @Override + public void subscribe(TICCoreSubscriber subscriber) { + this.notifier.subscribe(subscriber); + } + + @Override + public void unsubscribe(TICCoreSubscriber subscriber) { + this.notifier.unsubscribe(subscriber); + } + + @Override + public void start() { + this.stream.subscribe(this.streamListener); + this.stream.start(); + } + + @Override + public void stop() { + this.stream.unsubscribe(this.streamListener); + this.stream.stop(); + } + + @Override + public boolean isRunning() { + return this.stream.isRunning(); + } + + private void onFrame(TICFrame ticFrame) { + if (ticFrame == null) { + return; + } + + String serialNumber = null; + if (ticFrame.getMode() == TICMode.STANDARD) { + TICGroup group = ticFrame.getGroup("ADSC"); + serialNumber = group != null ? group.getValue() : null; + } else if (ticFrame.getMode() == TICMode.HISTORIC) { + TICGroup group = ticFrame.getGroup("ADCO"); + serialNumber = group != null ? group.getValue() : null; + } + + TICIdentifier frameIdentifier; + synchronized (this.identifierLock) { + this.identifier = new TICIdentifier.Builder() + .portId(this.identifier.getPortId()) + .portName(this.identifier.getPortName()) + .serialNumber(serialNumber) + .build(); + frameIdentifier = this.identifier; + } + + TICCoreFrame frame = + new TICCoreFrame(frameIdentifier, ticFrame.getMode(), LocalDateTime.now(), ticFrame); + this.notifyOnData(frame); + } + + private void onError(String errorMessage) { + int code = TICCoreErrorCode.OTHER_REASON.getCode(); + if (errorMessage != null && errorMessage.toLowerCase().contains("timeout")) { + code = TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(); + } + TICCoreError error = + new TICCoreError(this.getIdentifier(), code, errorMessage == null ? "" : errorMessage); + this.notifyOnError(error); + } + + private void notifyOnData(TICCoreFrame frame) { + if (frame == null) { + return; + } + + Collection subscriberList = this.notifier.getSubscribers(); + Task task = + new TaskBase() { + @Override + public void process() { + for (TICCoreSubscriber subscriber : subscriberList) { + try { + subscriber.onData(frame); + } catch (Exception exception) { + logger.error("TICCoreStream subscriber onData aborted", exception); + } + } + } + }; + task.start(); + } + + private void notifyOnError(TICCoreError error) { + if (error == null) { + return; + } + + Collection subscriberList = this.notifier.getSubscribers(); + Task task = + new TaskBase() { + @Override + public void process() { + for (TICCoreSubscriber subscriber : subscriberList) { + try { + subscriber.onError(error); + } catch (Exception exception) { + logger.error("TICCoreStream subscriber onError aborted", exception); + } + } + } + }; + task.start(); + } +} diff --git a/src/main/java/enedis/tic/core/TICCoreSubscriber.java b/src/main/java/tic/core/TICCoreSubscriber.java similarity index 93% rename from src/main/java/enedis/tic/core/TICCoreSubscriber.java rename to src/main/java/tic/core/TICCoreSubscriber.java index 8d17de3..42f5764 100644 --- a/src/main/java/enedis/tic/core/TICCoreSubscriber.java +++ b/src/main/java/tic/core/TICCoreSubscriber.java @@ -5,9 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; +package tic.core; -import enedis.lab.util.task.Subscriber; +import tic.util.task.Subscriber; /** * Interface for subscribing to core frame and error notifications. diff --git a/src/main/java/tic/core/TICIdentifier.java b/src/main/java/tic/core/TICIdentifier.java new file mode 100644 index 0000000..cdc029c --- /dev/null +++ b/src/main/java/tic/core/TICIdentifier.java @@ -0,0 +1,167 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.core; + +import java.util.Objects; + +/** + * Class representing a core identifier with port ID, port name, and serial number. + * + *

    This class provides mechanisms for constructing, accessing, and managing identifier + * information including port IDs, port names, and serial numbers. It is designed for + * general-purpose identifier handling. + * + *

    Common use cases include: + * + *

      + *
    • Representing identifiers with structured data + *
    • Matching identifiers for streams and devices + *
    • Managing port and serial information + *
    + * + * @author Enedis Smarties team + */ +public class TICIdentifier { + private final String portId; + private final String portName; + private final String serialNumber; + + public static class Builder { + private String portId; + private String portName; + private String serialNumber; + + /** + * Sets the portId field. + * + * @param portId the port ID + * @return the Builder instance + */ + public Builder portId(String portId) { + this.portId = portId; + return this; + } + + /** + * Sets the portName field. + * + * @param portName the port name + * @return the Builder instance + */ + public Builder portName(String portName) { + this.portName = portName; + return this; + } + + /** + * Sets the serialNumber field. + * + * @param serialNumber the serial number + * @return the Builder instance + */ + public Builder serialNumber(String serialNumber) { + this.serialNumber = serialNumber; + return this; + } + + /** + * Validates the builder's fields. + * + * @throws IllegalArgumentException if any required field is invalid + */ + protected void validate() { + if (this.portId == null && this.portName == null && this.serialNumber == null) { + throw new IllegalArgumentException("Empty TICIdentifier not allowed!"); + } + } + + public TICIdentifier build() { + this.validate(); + return new TICIdentifier(this.portId, this.portName, this.serialNumber); + } + } + + private TICIdentifier(String portId, String portName, String serialNumber) { + this.portId = portId; + this.portName = portName; + this.serialNumber = serialNumber; + } + + public boolean matches(TICIdentifier identifier) { + if (identifier != null) { + if (this.getSerialNumber() != null && identifier.getSerialNumber() != null) { + return this.getSerialNumber().equals(identifier.getSerialNumber()); + } + if (this.getPortId() != null && identifier.getPortId() != null) { + return this.getPortId().equals(identifier.getPortId()); + } + if (this.getPortName() != null && identifier.getPortName() != null) { + return this.getPortName().equals(identifier.getPortName()); + } + } + + return false; + } + + /** + * Get port id + * + * @return the port id + */ + public String getPortId() { + return this.portId; + } + + /** + * Get port name + * + * @return the port name + */ + public String getPortName() { + return this.portName; + } + + /** + * Get serial number + * + * @return the serial number + */ + public String getSerialNumber() { + return this.serialNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TICIdentifier)) { + return false; + } + TICIdentifier that = (TICIdentifier) o; + return Objects.equals(this.portId, that.portId) + && Objects.equals(this.portName, that.portName) + && Objects.equals(this.serialNumber, that.serialNumber); + } + + @Override + public int hashCode() { + return Objects.hash(this.portId, this.portName, this.serialNumber); + } + + @Override + public String toString() { + return "{portId=" + + this.portId + + ", portName=" + + this.portName + + ", serialNumber=" + + this.serialNumber + + "}"; + } +} diff --git a/src/main/java/tic/core/codec/TICCoreErrorCodec.java b/src/main/java/tic/core/codec/TICCoreErrorCodec.java new file mode 100644 index 0000000..19d5fd5 --- /dev/null +++ b/src/main/java/tic/core/codec/TICCoreErrorCodec.java @@ -0,0 +1,71 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.core.codec; + +import org.json.JSONObject; +import tic.core.TICCoreError; +import tic.frame.TICFrame; +import tic.frame.codec.TICFrameCodec; +import tic.util.codec.JsonObjectCodec; +import tic.util.codec.JsonStringCodec; + +/** + * Codec utilities for {@link tic.core.TICCoreError}. + * + *

    Currently focuses on JSON encoding (pretty-printed) for diagnostics and logging. + */ +public final class TICCoreErrorCodec + implements JsonStringCodec, JsonObjectCodec { + + public static TICCoreErrorCodec instance = new TICCoreErrorCodec(); + + public static TICCoreErrorCodec getInstance() { + if (instance == null) { + instance = new TICCoreErrorCodec(); + } + return instance; + } + + private TICCoreErrorCodec() {} + + @Override + public Object encodeToJsonObject(TICCoreError error) throws Exception { + if (error == null) { + throw new IllegalArgumentException("error cannot be null"); + } + + JSONObject json = new JSONObject(); + json.put( + "identifier", TICIdentifierCodec.getInstance().encodeToJsonObject(error.getIdentifier())); + json.put("errorMessage", error.getErrorMessage()); + json.put("errorCode", error.getErrorCode()); + + TICFrame frame = error.getFrame(); + if (frame != null) { + json.put("frame", TICFrameCodec.encode(frame)); + } + return json; + } + + @Override + public TICCoreError decodeFromJsonObject(Object jsonObject) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonObject'"); + } + + @Override + public String encodeToJsonString(TICCoreError error, int indentFactor) throws Exception { + JSONObject json = (JSONObject) encodeToJsonObject(error); + return indentFactor > 0 ? json.toString(indentFactor) : json.toString(); + } + + @Override + public TICCoreError decodeFromJsonString(String jsonString, int indentFactor) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonString'"); + } +} diff --git a/src/main/java/tic/core/codec/TICCoreFrameCodec.java b/src/main/java/tic/core/codec/TICCoreFrameCodec.java new file mode 100644 index 0000000..f7fc354 --- /dev/null +++ b/src/main/java/tic/core/codec/TICCoreFrameCodec.java @@ -0,0 +1,66 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.core.codec; + +import org.json.JSONObject; +import tic.core.TICCoreFrame; +import tic.frame.codec.TICFrameSummarizedCodec; +import tic.util.codec.JsonObjectCodec; +import tic.util.codec.JsonStringCodec; + +/** + * Codec utilities for {@link tic.core.TICCoreFrame}. + * + *

    Currently focuses on JSON encoding (pretty-printed) for diagnostics and logging. + */ +public final class TICCoreFrameCodec + implements JsonStringCodec, JsonObjectCodec { + + public static TICCoreFrameCodec instance = new TICCoreFrameCodec(); + + private TICCoreFrameCodec() {} + + public static TICCoreFrameCodec getInstance() { + if (instance == null) { + instance = new TICCoreFrameCodec(); + } + return instance; + } + + @Override + public Object encodeToJsonObject(TICCoreFrame frame) throws Exception { + if (frame == null) { + throw new IllegalArgumentException("frame cannot be null"); + } + + JSONObject json = new JSONObject(); + json.put( + "identifier", TICIdentifierCodec.getInstance().encodeToJsonObject(frame.getIdentifier())); + json.put("mode", frame.getMode().name()); + json.put("captureDateTime", frame.getCaptureDateTime().toString()); + json.put("frame", TICFrameSummarizedCodec.getInstance().encodeToJsonObject(frame.getFrame())); + + return json; + } + + @Override + public TICCoreFrame decodeFromJsonObject(Object jsonObject) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonObject'"); + } + + @Override + public String encodeToJsonString(TICCoreFrame frame, int indentFactor) throws Exception { + JSONObject json = (JSONObject) encodeToJsonObject(frame); + return indentFactor < 0 ? json.toString() : json.toString(indentFactor); + } + + @Override + public TICCoreFrame decodeFromJsonString(String jsonString, int indentFactor) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonString'"); + } +} diff --git a/src/main/java/tic/core/codec/TICIdentifierCodec.java b/src/main/java/tic/core/codec/TICIdentifierCodec.java new file mode 100644 index 0000000..8911af1 --- /dev/null +++ b/src/main/java/tic/core/codec/TICIdentifierCodec.java @@ -0,0 +1,119 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.core.codec; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONObject; +import tic.core.TICIdentifier; +import tic.util.codec.JsonArrayCodec; +import tic.util.codec.JsonObjectCodec; +import tic.util.codec.JsonStringCodec; + +/** + * Codec utilities for {@link tic.core.TICIdentifier}. + * + *

    Currently focuses on JSON encoding (pretty-printed) for diagnostics and logging. + */ +public final class TICIdentifierCodec + implements JsonObjectCodec, + JsonStringCodec, + JsonArrayCodec> { + + public static TICIdentifierCodec instance = null; + + public static final TICIdentifierCodec getInstance() { + if (instance == null) { + return new TICIdentifierCodec(); + } + return instance; + } + + private TICIdentifierCodec() {} + + @Override + public JSONObject encodeToJsonObject(TICIdentifier identifier) { + if (identifier == null) { + throw new IllegalArgumentException("identifier cannot be null"); + } + + JSONObject json = new JSONObject(); + json.put( + "portName", identifier.getPortName() == null ? JSONObject.NULL : identifier.getPortName()); + json.put("portId", identifier.getPortId() == null ? JSONObject.NULL : identifier.getPortId()); + json.put( + "serialNumber", + identifier.getSerialNumber() == null ? JSONObject.NULL : identifier.getSerialNumber()); + return json; + } + + @Override + public TICIdentifier decodeFromJsonObject(Object object) { + if (object == null || object instanceof JSONObject == false) { + throw new IllegalArgumentException("Input is not a valid JSON object"); + } + + JSONObject jsonObject = (JSONObject) object; + + String portName = jsonObject.isNull("portName") ? null : jsonObject.getString("portName"); + String portId = jsonObject.isNull("portId") ? null : jsonObject.getString("portId"); + String serialNumber = + jsonObject.isNull("serialNumber") ? null : jsonObject.getString("serialNumber"); + + return new TICIdentifier.Builder() + .portName(portName) + .portId(portId) + .serialNumber(serialNumber) + .build(); + } + + @Override + public JSONArray encodeToJsonArray(List identifiers) { + List safeList = identifiers == null ? Collections.emptyList() : identifiers; + JSONArray jsonArray = new JSONArray(); + + for (TICIdentifier identifier : safeList) { + jsonArray.put(encodeToJsonObject(identifier)); + } + return jsonArray; + } + + @Override + public List decodeFromJsonArray(Object object) { + if (object == null || object instanceof JSONArray == false) { + throw new IllegalArgumentException("Input is not a valid JSON array"); + } + + JSONArray jsonArray = (JSONArray) object; + + List identifiers = new ArrayList<>(); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + TICIdentifier identifier = decodeFromJsonObject(jsonObject); + identifiers.add(identifier); + } + return identifiers; + } + + @Override + public String encodeToJsonString(TICIdentifier identifier, int indentFactor) { + if (identifier == null) { + throw new IllegalArgumentException("identifier cannot be null"); + } + + JSONObject json = encodeToJsonObject(identifier); + return indentFactor < 0 ? json.toString() : json.toString(indentFactor); + } + + @Override + public TICIdentifier decodeFromJsonString(String jsonString, int indentFactor) { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonString'"); + } +} diff --git a/src/main/java/tic/diagnostic/core/TICCoreApp.java b/src/main/java/tic/diagnostic/core/TICCoreApp.java new file mode 100644 index 0000000..a1a9fc5 --- /dev/null +++ b/src/main/java/tic/diagnostic/core/TICCoreApp.java @@ -0,0 +1,16 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.core; + +/** Minimal launcher: all CLI parsing lives in {@link TICCoreCli}. */ +public final class TICCoreApp { + public static void main(String[] args) { + int exitCode = TICCoreCli.execute(args); + System.exit(exitCode); + } +} diff --git a/src/main/java/tic/diagnostic/core/TICCoreCli.java b/src/main/java/tic/diagnostic/core/TICCoreCli.java new file mode 100644 index 0000000..ae84d21 --- /dev/null +++ b/src/main/java/tic/diagnostic/core/TICCoreCli.java @@ -0,0 +1,188 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.core; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ExitCode; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; +import tic.core.TICCoreBase; +import tic.core.TICCoreError; +import tic.core.TICCoreFrame; +import tic.core.TICCoreSubscriber; +import tic.core.TICIdentifier; +import tic.diagnostic.core.commands.TICCoreAvailableCommand; +import tic.diagnostic.core.commands.TICCoreModemsCommand; +import tic.diagnostic.core.commands.TICCoreReadCommand; +import tic.diagnostic.core.commands.TICCoreSubscribeCommand; +import tic.frame.TICMode; +import tic.io.modem.ModemDescriptor; +import tic.io.modem.ModemFinder; +import tic.io.modem.ModemFinderBase; +import tic.io.serialport.SerialPortFinderBase; +import tic.io.usb.UsbPortFinderBase; +import tic.util.time.Time; + +/** + * Root Picocli command for TICCore diagnostic. + * + *

    This class contains all CLI parsing and shared behaviors; {@link TICCoreApp} should only + * delegate to it. + */ +@Command( + name = "tic-core", + mixinStandardHelpOptions = true, + description = + "TICCore diagnostic (getAvailableTICs, getModemsInfo, readNextFrame, subscribe," + + " unsubscribe).", + subcommands = { + TICCoreAvailableCommand.class, + TICCoreModemsCommand.class, + TICCoreReadCommand.class, + TICCoreSubscribeCommand.class + }) +public final class TICCoreCli implements Callable { + private static final TICCoreSubscriber NOOP_SUBSCRIBER = + new TICCoreSubscriber() { + @Override + public void onData(TICCoreFrame frame) { + // no-op + } + + @Override + public void onError(TICCoreError error) { + // no-op + } + }; + + @Spec CommandSpec spec; + + public static int execute(String[] args) { + return new CommandLine(new TICCoreCli()).execute(args); + } + + @Option( + names = {"-m", "--mode"}, + defaultValue = "AUTO", + description = "TIC mode: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE})") + TICMode mode = TICMode.AUTO; + + @Option( + names = {"--startPort"}, + paramLabel = "PORT", + description = + "Ports to start at launch (useful if discovery is slow). " + "Example: --startPort COM3") + String startPort; + + @Option( + names = {"--waitMs"}, + defaultValue = "200", + paramLabel = "MS", + description = "Wait after startup to let discovery happen (default: ${DEFAULT-VALUE}).") + int waitMs = 200; + + @Option( + names = {"--scan"}, + negatable = true, + defaultValue = "true", + description = + "Immediately scan already-plugged modems (recommended). Use --no-scan to disable.") + boolean scanAlreadyPlugged = true; + + @Override + public Integer call() { + // If no subcommand is provided, show usage. + if (this.spec != null) { + this.spec.commandLine().usage(System.out); + } + return ExitCode.USAGE; + } + + public TICCoreBase startCore() { + List startPorts = + (this.startPort == null || this.startPort.trim().isEmpty()) + ? null + : Collections.singletonList(this.startPort.trim()); + + TICCoreBase ticCore = new TICCoreBase(this.mode, startPorts); + System.out.println("Starting TICCore (mode=" + this.mode + ")..."); + ticCore.start(); + + if (this.waitMs > 0) { + Time.sleep(this.waitMs); + } + + if (this.scanAlreadyPlugged) { + this.scanAndStartAlreadyPluggedStreams(ticCore); + } + + return ticCore; + } + + public void stopCore(TICCoreBase ticCore) { + if (ticCore == null) { + return; + } + try { + System.out.println("Stopping TICCore..."); + ticCore.stop(); + } catch (Exception e) { + System.err.println("[WARN] Failed to stop TICCore: " + e.getMessage()); + e.printStackTrace(System.err); + } + } + + void scanAndStartAlreadyPluggedStreams(TICCoreBase ticCore) { + // Start streams immediately to avoid waiting for the notifier tick. + try { + ModemFinder modemFinder = + ModemFinderBase.create( + SerialPortFinderBase.getInstance(), UsbPortFinderBase.getInstance()); + List detected = modemFinder.findAll(); + if (detected == null || detected.isEmpty()) { + return; + } + for (ModemDescriptor descriptor : detected) { + if (descriptor == null) { + continue; + } + + TICIdentifier identifier = new TICIdentifier.Builder() + .portId(descriptor.portId()) + .portName(descriptor.portName()) + .build(); + + // subscribe() will start the stream if needed. + ticCore.subscribe(identifier, NOOP_SUBSCRIBER); + } + } catch (Exception e) { + System.err.println("[WARN] Unable to scan already-plugged modems: " + e.getMessage()); + e.printStackTrace(System.err); + } + } + + public TICIdentifier resolveIdentifier(TICCoreBase ticCore, TICCoreIdentifierOptions idOptions) + throws IllegalArgumentException { + TICIdentifier specified = (idOptions == null) ? null : idOptions.toIdentifierOrNull(); + if (specified != null) { + return specified; + } + + List available = ticCore.getAvailableTICs(); + if (available == null || available.isEmpty()) { + throw new IllegalArgumentException( + "No TIC detected. Plug a modem or provide --portName/--portId/--serialNumber."); + } + return available.get(0); + } +} diff --git a/src/main/java/tic/diagnostic/core/TICCoreIdentifierOptions.java b/src/main/java/tic/diagnostic/core/TICCoreIdentifierOptions.java new file mode 100644 index 0000000..e960460 --- /dev/null +++ b/src/main/java/tic/diagnostic/core/TICCoreIdentifierOptions.java @@ -0,0 +1,43 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.core; + +import picocli.CommandLine.Option; +import tic.core.TICIdentifier; + +/** Options to identify a TIC (portName/portId/serialNumber). */ +public final class TICCoreIdentifierOptions { + @Option( + names = {"--portName"}, + paramLabel = "NAME", + description = "Port name (e.g. COM3, /dev/ttyUSB0).") + String portName; + + @Option( + names = {"--portId"}, + paramLabel = "ID", + description = "Port identifier (if available).") + String portId; + + @Option( + names = {"--serialNumber"}, + paramLabel = "SN", + description = "Modem serial number (if available).") + String serialNumber; + + TICIdentifier toIdentifierOrNull() { + if (this.portName == null && this.portId == null && this.serialNumber == null) { + return null; + } + return new TICIdentifier.Builder() + .portId(this.portId) + .portName(this.portName) + .serialNumber(this.serialNumber) + .build(); + } +} diff --git a/src/main/java/tic/diagnostic/core/TICCorePrintingSubscriber.java b/src/main/java/tic/diagnostic/core/TICCorePrintingSubscriber.java new file mode 100644 index 0000000..3244a59 --- /dev/null +++ b/src/main/java/tic/diagnostic/core/TICCorePrintingSubscriber.java @@ -0,0 +1,64 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.core; + +import tic.core.TICCoreError; +import tic.core.TICCoreFrame; +import tic.core.TICCoreSubscriber; +import tic.core.codec.TICCoreErrorCodec; +import tic.core.codec.TICCoreFrameCodec; + +/** Subscriber that prints received frames/errors to stdout/stderr. */ +public final class TICCorePrintingSubscriber implements TICCoreSubscriber { + private final int indent; + private volatile long frameCount; + private volatile TICCoreError lastError; + + public TICCorePrintingSubscriber(int indent) { + this.indent = indent; + this.frameCount = 0; + this.lastError = null; + } + + @Override + public void onData(TICCoreFrame frame) { + this.frameCount++; + System.out.println("--- Frame #" + this.frameCount + " ---"); + try { + System.out.println(frame == null ? "null" : TICCoreFrameCodec.getInstance().encodeToJsonString(frame, this.indent)); + } catch (Exception e) { + System.err.println("[ERROR] Failed to encode frame to JSON string: " + e.getMessage()); + } + } + + @Override + public void onError(TICCoreError error) { + this.lastError = error; + System.err.println("--- Error ---"); + try { + System.err.println( + error == null + ? "null" + : TICCoreErrorCodec.getInstance().encodeToJsonString(error, this.indent)); + } catch (Exception e) { + System.err.println("[ERROR] Failed to encode error to JSON string: " + e.getMessage()); + } + } + + public long getFrameCount() { + return this.frameCount; + } + + public boolean hasError() { + return this.lastError != null; + } + + public String getLastErrorMessage() { + return this.lastError == null ? null : this.lastError.getErrorMessage(); + } +} diff --git a/src/main/java/tic/diagnostic/core/commands/TICCoreAvailableCommand.java b/src/main/java/tic/diagnostic/core/commands/TICCoreAvailableCommand.java new file mode 100644 index 0000000..f583e53 --- /dev/null +++ b/src/main/java/tic/diagnostic/core/commands/TICCoreAvailableCommand.java @@ -0,0 +1,48 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.core.commands; + +import java.util.List; +import java.util.concurrent.Callable; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ExitCode; +import tic.core.TICCoreBase; +import tic.core.TICIdentifier; +import tic.diagnostic.core.TICCoreCli; + +@Command( + name = "available", + mixinStandardHelpOptions = true, + description = "List available TICs (TICCore.getAvailableTICs).") +public final class TICCoreAvailableCommand implements Callable { + @CommandLine.ParentCommand TICCoreCli parent; + + @Override + public Integer call() { + TICCoreBase ticCore = null; + try { + ticCore = parent.startCore(); + List available = ticCore.getAvailableTICs(); + int count = (available == null) ? 0 : available.size(); + System.out.println("Available TICs (" + count + "):"); + if (available != null) { + for (TICIdentifier id : available) { + System.out.println("- " + id); + } + } + return ExitCode.OK; + } catch (Exception e) { + System.err.println("[ERROR] " + e.getMessage()); + e.printStackTrace(System.err); + return ExitCode.SOFTWARE; + } finally { + parent.stopCore(ticCore); + } + } +} diff --git a/src/main/java/tic/diagnostic/core/commands/TICCoreModemsCommand.java b/src/main/java/tic/diagnostic/core/commands/TICCoreModemsCommand.java new file mode 100644 index 0000000..368dde5 --- /dev/null +++ b/src/main/java/tic/diagnostic/core/commands/TICCoreModemsCommand.java @@ -0,0 +1,49 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.core.commands; + +import java.util.List; +import java.util.concurrent.Callable; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ExitCode; +import tic.core.TICCoreBase; +import tic.diagnostic.core.TICCoreCli; +import tic.io.modem.ModemDescriptor; +import tic.io.modem.ModemJsonCodec; + +@Command( + name = "modems", + mixinStandardHelpOptions = true, + description = "Show modem information (TICCore.getModemsInfo).") +public final class TICCoreModemsCommand implements Callable { + @CommandLine.ParentCommand TICCoreCli parent; + + @Override + public Integer call() { + TICCoreBase ticCore = null; + try { + ticCore = parent.startCore(); + List modems = ticCore.getModemsInfo(); + int count = (modems == null) ? 0 : modems.size(); + System.out.println("Modems (" + count + "):"); + if (modems != null) { + for (ModemDescriptor modem : modems) { + System.out.println("- " + (modem == null ? "null" : ModemJsonCodec.getInstance().encodeToJsonString(modem))); + } + } + return ExitCode.OK; + } catch (Exception e) { + System.err.println("[ERROR] " + e.getMessage()); + e.printStackTrace(System.err); + return ExitCode.SOFTWARE; + } finally { + parent.stopCore(ticCore); + } + } +} diff --git a/src/main/java/tic/diagnostic/core/commands/TICCoreReadCommand.java b/src/main/java/tic/diagnostic/core/commands/TICCoreReadCommand.java new file mode 100644 index 0000000..9ac28cf --- /dev/null +++ b/src/main/java/tic/diagnostic/core/commands/TICCoreReadCommand.java @@ -0,0 +1,70 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.core.commands; + +import java.util.concurrent.Callable; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ExitCode; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import tic.core.TICCoreBase; +import tic.core.TICCoreFrame; +import tic.core.TICIdentifier; +import tic.core.codec.TICCoreFrameCodec; +import tic.diagnostic.core.TICCoreCli; +import tic.diagnostic.core.TICCoreIdentifierOptions; + +@Command( + name = "read", + mixinStandardHelpOptions = true, + description = "Read the next frame (TICCore.readNextFrame).") +public final class TICCoreReadCommand implements Callable { + @CommandLine.ParentCommand TICCoreCli parent; + + @Mixin TICCoreIdentifierOptions identifierOptions = new TICCoreIdentifierOptions(); + + @Option( + names = {"--timeoutMs"}, + defaultValue = "30000", + paramLabel = "MS", + description = "Read timeout in milliseconds (default: ${DEFAULT-VALUE}).") + int timeoutMs = 30000; + + @Option( + names = {"--indent"}, + defaultValue = "2", + paramLabel = "N", + description = "Frame JSON indentation (default: ${DEFAULT-VALUE}).") + int indent = 2; + + @Override + public Integer call() { + TICCoreBase ticCore = null; + try { + ticCore = parent.startCore(); + TICIdentifier identifier = parent.resolveIdentifier(ticCore, identifierOptions); + System.out.println("Reading next frame from " + identifier + " ..."); + TICCoreFrame frame = ticCore.readNextFrame(identifier, timeoutMs); + System.out.println( + frame == null + ? "null" + : TICCoreFrameCodec.getInstance().encodeToJsonString(frame, indent)); + return ExitCode.OK; + } catch (IllegalArgumentException e) { + System.err.println("[ERROR] " + e.getMessage()); + return ExitCode.USAGE; + } catch (Exception e) { + System.err.println("[ERROR] " + e.getMessage()); + e.printStackTrace(System.err); + return ExitCode.SOFTWARE; + } finally { + parent.stopCore(ticCore); + } + } +} diff --git a/src/main/java/tic/diagnostic/core/commands/TICCoreSubscribeCommand.java b/src/main/java/tic/diagnostic/core/commands/TICCoreSubscribeCommand.java new file mode 100644 index 0000000..b893366 --- /dev/null +++ b/src/main/java/tic/diagnostic/core/commands/TICCoreSubscribeCommand.java @@ -0,0 +1,115 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.core.commands; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.concurrent.Callable; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ExitCode; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import tic.core.TICCoreBase; +import tic.core.TICIdentifier; +import tic.diagnostic.core.TICCoreCli; +import tic.diagnostic.core.TICCoreIdentifierOptions; +import tic.diagnostic.core.TICCorePrintingSubscriber; +import tic.util.time.Time; + +@Command( + name = "subscribe", + mixinStandardHelpOptions = true, + description = + "Subscribe to a TIC and print received frames (TICCore.subscribe). " + + "Subscription is automatically cancelled at the end (TICCore.unsubscribe).") +public final class TICCoreSubscribeCommand implements Callable { + @CommandLine.ParentCommand TICCoreCli parent; + + @Mixin TICCoreIdentifierOptions identifierOptions = new TICCoreIdentifierOptions(); + + @Option( + names = {"--durationSec"}, + defaultValue = "0", + paramLabel = "S", + description = + "Listen duration in seconds (default: ${DEFAULT-VALUE}). " + + "Use 0 to wait until Enter is pressed.") + int durationSec = 0; + + @Option( + names = {"--frames"}, + defaultValue = "0", + paramLabel = "N", + description = "Stop after N frames (0 = unlimited).") + int maxFrames = 0; + + @Option( + names = {"--indent"}, + defaultValue = "2", + paramLabel = "N", + description = "Frame JSON indentation (default: ${DEFAULT-VALUE}).") + int indent = 2; + + @Override + public Integer call() { + TICCoreBase ticCore = null; + TICCorePrintingSubscriber subscriber = null; + TICIdentifier identifier = null; + try { + ticCore = parent.startCore(); + identifier = parent.resolveIdentifier(ticCore, identifierOptions); + + subscriber = new TICCorePrintingSubscriber(indent); + System.out.println("Subscribing to " + identifier + " ..."); + ticCore.subscribe(identifier, subscriber); + + if (durationSec <= 0) { + System.out.println("Listening. Press Enter to stop..."); + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + br.readLine(); + } else { + long deadlineMs = System.currentTimeMillis() + (durationSec * 1000L); + System.out.println("Listening for " + durationSec + "s (Ctrl+C to interrupt)..."); + while (System.currentTimeMillis() < deadlineMs) { + if (subscriber.hasError()) { + break; + } + if (maxFrames > 0 && subscriber.getFrameCount() >= maxFrames) { + break; + } + Time.sleep(200); + } + } + + if (subscriber.hasError()) { + System.err.println("[ERROR] Error received: " + subscriber.getLastErrorMessage()); + return ExitCode.SOFTWARE; + } + return ExitCode.OK; + } catch (IllegalArgumentException e) { + System.err.println("[ERROR] " + e.getMessage()); + return ExitCode.USAGE; + } catch (Exception e) { + System.err.println("[ERROR] " + e.getMessage()); + e.printStackTrace(System.err); + return ExitCode.SOFTWARE; + } finally { + // Explicit unsubscribe test with the same subscriber instance. + if (ticCore != null && subscriber != null && identifier != null) { + try { + System.out.println("Unsubscribing from " + identifier + " ..."); + ticCore.unsubscribe(identifier, subscriber); + } catch (Exception e) { + System.err.println("[WARN] Unsubscribe failed: " + e.getMessage()); + } + } + parent.stopCore(ticCore); + } + } +} diff --git a/src/main/java/tic/diagnostic/modem/ModemFinderApp.java b/src/main/java/tic/diagnostic/modem/ModemFinderApp.java new file mode 100644 index 0000000..1f13675 --- /dev/null +++ b/src/main/java/tic/diagnostic/modem/ModemFinderApp.java @@ -0,0 +1,39 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.modem; + +import java.util.List; + +import org.json.JSONArray; + +import tic.io.modem.ModemDescriptor; +import tic.io.modem.ModemFinder; +import tic.io.modem.ModemFinderBase; +import tic.io.modem.ModemJsonCodec; +import tic.io.serialport.SerialPortFinderBase; +import tic.io.usb.UsbPortFinderBase; + +public class ModemFinderApp { + + /** + * Program writing the modem descriptor list (JSON format) on the output stream + * + * @param args not used + */ + public static void main(String[] args) { + try { + ModemFinder modemFinder = + ModemFinderBase.create( + SerialPortFinderBase.getInstance(), UsbPortFinderBase.getInstance()); + List descriptors = modemFinder.findAll(); + System.out.println((JSONArray) ModemJsonCodec.getInstance().encodeToJsonArray(descriptors)); + } catch (Exception exception) { + System.err.println(exception.getMessage()); + } + } +} diff --git a/src/main/java/tic/diagnostic/modem/ModemPlugNotifierApp.java b/src/main/java/tic/diagnostic/modem/ModemPlugNotifierApp.java new file mode 100644 index 0000000..afa08e4 --- /dev/null +++ b/src/main/java/tic/diagnostic/modem/ModemPlugNotifierApp.java @@ -0,0 +1,69 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.modem; + +import java.util.Arrays; +import java.util.List; + +import org.json.JSONArray; + +import tic.io.PlugSubscriber; +import tic.io.PortPlugNotifier; +import tic.io.modem.ModemDescriptor; +import tic.io.modem.ModemFinder; +import tic.io.modem.ModemFinderBase; +import tic.io.modem.ModemJsonCodec; +import tic.io.modem.ModemPlugNotifier; +import tic.io.serialport.SerialPortFinderBase; +import tic.io.usb.UsbPortFinderBase; + +public class ModemPlugNotifierApp { + + /** + * Program writing on the output stream when a modem has been plugged or unplugged + * + * @param args not used + */ + public static void main(String[] args) { + ModemFinder modemFinder = + ModemFinderBase.create(SerialPortFinderBase.getInstance(), UsbPortFinderBase.getInstance()); + /* 1. Create notification service */ + ModemPlugNotifier notifier = new ModemPlugNotifier(1000, modemFinder); + /* 2. Create subscriber to print when a modem has been plugged or unplugged */ + PlugSubscriber subscriber = + new PlugSubscriber() { + @Override + public void onPlugged(ModemDescriptor descriptor) { + List list = new java.util.ArrayList<>(Arrays.asList(descriptor)); + JSONArray array; + try { + array = (JSONArray) ModemJsonCodec.getInstance().encodeToJsonArray(list); + } catch (Exception e) { + array = new JSONArray(); + } + String payload = array.toString(2); + System.out.println("onPlugged event:\n" + payload + "\n"); + } + + @Override + public void onUnplugged(ModemDescriptor descriptor) { + List list = new java.util.ArrayList<>(Arrays.asList(descriptor)); + JSONArray array; + try { + array = (JSONArray) ModemJsonCodec.getInstance().encodeToJsonArray(list); + } catch (Exception e) { + array = new JSONArray(); + } + String payload = array.toString(2); + System.out.println("onUnplugged event:\n" + payload + "\n"); + } + }; + /* 3. Run program printing when a modem has been plugged or unplugged until CTRL+C is pressed */ + PortPlugNotifier.main(notifier, subscriber); + } +} diff --git a/src/main/java/tic/diagnostic/serialport/SerialPortFinderApp.java b/src/main/java/tic/diagnostic/serialport/SerialPortFinderApp.java new file mode 100644 index 0000000..e5d385f --- /dev/null +++ b/src/main/java/tic/diagnostic/serialport/SerialPortFinderApp.java @@ -0,0 +1,33 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.serialport; + +import java.util.List; +import tic.io.serialport.SerialPortDescriptor; +import tic.io.serialport.SerialPortFinderBase; +import tic.io.serialport.SerialPortJsonEncoder; + +public class SerialPortFinderApp { + + /** + * Main method that outputs all serial port descriptors in JSON format. + * + *

    This utility method can be used to quickly inspect available serial ports on the system. The + * output is JSON-formatted with an indentation of 2 spaces. + * + * @param args command-line arguments (not used) + */ + public static void main(String[] args) { + try { + List descriptors = SerialPortFinderBase.getInstance().findAll(); + System.out.println(SerialPortJsonEncoder.encode(descriptors)); + } catch (Exception exception) { + System.err.println(exception.getMessage()); + } + } +} diff --git a/src/main/java/tic/diagnostic/usb/UsbPortFinderApp.java b/src/main/java/tic/diagnostic/usb/UsbPortFinderApp.java new file mode 100644 index 0000000..50b0f5d --- /dev/null +++ b/src/main/java/tic/diagnostic/usb/UsbPortFinderApp.java @@ -0,0 +1,30 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.diagnostic.usb; + +import java.util.List; +import tic.io.usb.UsbPortDescriptor; +import tic.io.usb.UsbPortFinderBase; +import tic.io.usb.UsbPortJsonEncoder; + +public class UsbPortFinderApp { + + /** + * Program writing the USB port descriptor list (JSON format) on the output stream + * + * @param args not used + */ + public static void main(String[] args) { + try { + List descriptors = UsbPortFinderBase.getInstance().findAll(); + System.out.println(UsbPortJsonEncoder.encode(descriptors)); + } catch (Exception exception) { + System.err.println(exception.getMessage()); + } + } +} diff --git a/src/main/java/tic/frame/TICFrame.java b/src/main/java/tic/frame/TICFrame.java new file mode 100644 index 0000000..28b5283 --- /dev/null +++ b/src/main/java/tic/frame/TICFrame.java @@ -0,0 +1,111 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame; + +import java.util.ArrayList; +import java.util.List; +import tic.frame.group.TICGroup; + +/** + * Abstract base class for TIC frames. + * + *

    This class provides the structure for TIC frames. + * + * @author Enedis Smarties team + */ +public class TICFrame { + + private TICMode mode = null; + private List groupList = new ArrayList<>(); + + /** + * Constructs a TIC frame with the specified mode. + * + * @param mode the TIC mode + */ + public TICFrame(TICMode mode) { + if (mode == null) { + throw new IllegalArgumentException("TICFrame mode must not be null"); + } + if (mode == TICMode.AUTO) { + throw new IllegalArgumentException("TICFrame cannot be created with AUTO mode"); + } + this.mode = mode; + } + + /** + * Adds a new group to the TIC frame. + * + * @param group the TIC group to add + */ + public void addGroup(TICGroup group) { + this.groupList.add(group); + } + + /** + * Returns the TIC mode for this frame. + * + * @return the TIC mode + */ + public TICMode getMode() { + return this.mode; + } + + /** + * Returns the list of info groups in this TIC frame. + * + * @return the list of info groups + */ + public List getGroupList() { + return this.groupList; + } + + /** + * Retrieves the TIC group with the specified label. + * + * @param label the label of the group to retrieve + * @return the TIC group with the specified label, or null if not found + */ + public TICGroup getGroup(String label) { + for (TICGroup group : this.groupList) { + if (group.getLabel().equals(label)) { + return group; + } + } + return null; + } + + /** + * Checks if the TIC frame contains any invalid groups. + * + * @return true if there is at least one invalid group, false otherwise + */ + public boolean hasInvalidGroup() { + for (TICGroup group : this.groupList) { + if (!group.isValid()) { + return true; + } + } + return false; + } + + /** + * Checks if the TIC frame contains a group with the specified label. + * + * @param label the label to search for + * @return true if a group with the specified label exists, false otherwise + */ + public boolean containsGroupLabel(String label) { + return (this.getGroup(label) != null) ? true : false; + } + + @Override + public String toString() { + return "mode=" + this.mode + ", groupList=" + this.groupList; + } +} diff --git a/src/main/java/tic/frame/TICMode.java b/src/main/java/tic/frame/TICMode.java new file mode 100644 index 0000000..efc3508 --- /dev/null +++ b/src/main/java/tic/frame/TICMode.java @@ -0,0 +1,22 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame; + +/** + * Enumeration of TIC modes. + * + * @author Enedis Smarties team + */ +public enum TICMode { + /** Standard mode. */ + STANDARD, + /** Historic mode. */ + HISTORIC, + /** Auto-detect mode. */ + AUTO; +} diff --git a/src/main/java/tic/frame/TICModeDetector.java b/src/main/java/tic/frame/TICModeDetector.java new file mode 100644 index 0000000..858a26a --- /dev/null +++ b/src/main/java/tic/frame/TICModeDetector.java @@ -0,0 +1,86 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame; + +import java.util.Arrays; +import tic.frame.delimiter.TICSeparator; +import tic.frame.delimiter.TICStartPattern; + +/** + * Utility class for detecting TIC modes from frame or group buffers. + * + *

    This class provides methods to identify the TIC mode (STANDARD, HISTORIC) based on the + * contents of frame or group byte arrays. + * + * @author Enedis Smarties team + */ +public class TICModeDetector { + + private TICModeDetector() {} + + /** + * Detects the TIC mode from the given frame buffer. + * + * @param frameBuffer the byte array containing the frame start + * @return the detected {@link TICMode}, or null if not recognized + */ + public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) { + if (frameBuffer == null) { + throw new IllegalArgumentException("Tic frame buffer is null, unable to determine TIC Mode!"); + } + byte[] frameBufferStart = new byte[TICStartPattern.length()]; + if (frameBuffer.length < frameBufferStart.length) { + throw new IllegalArgumentException( + "Tic frame buffer 0x" + bytesToHex(frameBuffer) + " too short to determine TIC Mode!"); + } + System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); + if (Arrays.equals(frameBufferStart, TICStartPattern.HISTORIC.getValue())) { + return TICMode.HISTORIC; + } else { + if (Arrays.equals(frameBufferStart, TICStartPattern.STANDARD.getValue())) { + return TICMode.STANDARD; + } + return null; + } + } + + /** + * Detects the TIC mode from the given group buffer. + * + * @param groupBuffer the byte array containing the group data + * @return the detected {@link TICMode}, or null if not recognized + */ + public static TICMode findModeFromGroupBuffer(byte[] groupBuffer) { + if (groupBuffer == null) { + throw new IllegalArgumentException("Tic group buffer is null, unable to determine TIC Mode!"); + } + for (int i = 0; i < groupBuffer.length; i++) { + if (groupBuffer[i] == TICSeparator.HISTORIC.getValue()) { + return TICMode.HISTORIC; + } else if (groupBuffer[i] == TICSeparator.STANDARD.getValue()) { + return TICMode.STANDARD; + } + } + return null; + } + + /** + * Converts a byte array to a hexadecimal string (replacement for + * DatatypeConverter.printHexBinary). + * + * @param bytes the byte array to convert + * @return the hexadecimal string representation + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02X", b)); + } + return result.toString(); + } +} diff --git a/src/main/java/tic/frame/checksum/TICChecksum.java b/src/main/java/tic/frame/checksum/TICChecksum.java new file mode 100644 index 0000000..d8e75f3 --- /dev/null +++ b/src/main/java/tic/frame/checksum/TICChecksum.java @@ -0,0 +1,106 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.checksum; + +import tic.frame.TICMode; + +public class TICChecksum { + + private TICChecksum() {} + + /** + * Verifies the checksum for the given byte array. + * + * @param groupBuffer the byte array to verify the checksum for. Starting with and ending + * with + * @param mode the TIC mode used. If auto mode is used, the mode will be detected from the group + * buffer + * @return true if the checksum is valid, false otherwise + */ + public static boolean verifyChecksum(byte[] groupBuffer, TICMode mode) { + int offsetBegin = TICChecksumOffset.getOffsetBegin(); + int offsetEnd = TICChecksumOffset.getOffsetEnd(groupBuffer, mode); + + return verifyChecksum(groupBuffer, offsetBegin, offsetEnd); + } + + /** + * Computes the checksum for the given byte array. + * + * @param groupBuffer the byte array to compute the checksum for. Starting with and ending + * with + * @param mode the TIC mode used. If auto mode is used, the mode will be detected from the group + * buffer + * @return the computed checksum byte + */ + public static int computeChecksum(byte[] groupBuffer, TICMode mode) { + int offsetBegin = TICChecksumOffset.getOffsetBegin(); + int offsetEnd = TICChecksumOffset.getOffsetEnd(groupBuffer, mode); + + return computeChecksum(groupBuffer, offsetBegin, offsetEnd); + } + + /** + * Verifies the checksum for the given byte array between the specified offsets. + * + * @param groupBuffer the byte array to verify the checksum for. Starting with and ending + * with + * @param offsetBegin tthe begin offset for checksum computation + * @param offsetEnd the end offset for checksum computation + * @return true if the checksum is valid, false otherwise + */ + private static boolean verifyChecksum(byte[] groupBuffer, int offsetBegin, int offsetEnd) { + int computedChecksum = computeChecksum(groupBuffer, offsetBegin, offsetEnd); + int offsetChecksum = TICChecksumOffset.getOffsetChecksum(groupBuffer); + int receivedChecksum = groupBuffer[offsetChecksum] & 0xFF; + + return computedChecksum == receivedChecksum; + } + + /** + * Computes the checksum for the given byte array between the specified offsets. + * + * @param buffer the byte array to compute the checksum for. Starting with and ending with + * + * @param offsetBegin the begin offset for checksum computation + * @param offsetEnd the end offset for checksum computation + * @return the computed checksum byte + */ + private static int computeChecksum(byte[] buffer, int offsetBegin, int offsetEnd) { + int crc = 0; + + TICChecksumOffset.checkBufferOffsets(buffer, offsetBegin, offsetEnd); + + for (int i = offsetBegin; i < offsetEnd; i++) { + crc = computeUpdate(crc, buffer[i]); + } + + return computeEnd(crc); + } + + /** + * Update checksum computation of a TIC group + * + * @param crc current checksum value + * @param octet current byte of the TIC group + * @return checksum updated + */ + private static int computeUpdate(int crc, byte octet) { + return crc + (octet & 0xff); + } + + /** + * Terminate checksum computation of a TIC group + * + * @param crc current checksum value + * @return checksum computed + */ + private static int computeEnd(int crc) { + return (crc & 0x3F) + 0x20; + } +} diff --git a/src/main/java/tic/frame/checksum/TICChecksumOffset.java b/src/main/java/tic/frame/checksum/TICChecksumOffset.java new file mode 100644 index 0000000..3277f73 --- /dev/null +++ b/src/main/java/tic/frame/checksum/TICChecksumOffset.java @@ -0,0 +1,113 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.checksum; + +import java.util.Objects; +import tic.frame.TICMode; +import tic.frame.TICModeDetector; + +public class TICChecksumOffset { + public static final int OFFSET_BEGIN = 1; + public static final int OFFSET_END_HISTORIC = 3; // excluding + public static final int OFFSET_END_STANDARD = 2; // excluding + public static final int OFFSET_CHECKSUM = 1; // excluding + + private TICChecksumOffset() {} + + /** + * Gets the begin offset for checksum computation. + * + * @return the begin offset for checksum computation + */ + public static int getOffsetBegin() { + return OFFSET_BEGIN; + } + + public static int getOffsetEnd(byte[] groupBuffer) { + return getOffsetEnd(groupBuffer, TICMode.AUTO); + } + + public static int getOffsetChecksum(byte[] groupBuffer) { + Objects.requireNonNull(groupBuffer, "groupBuffer must not be null"); + return getOffsetChecksum(groupBuffer, groupBuffer.length - 1); + } + + /** + * Determines the end offset for checksum computation based on the TIC mode. + * + * @param groupBuffer the byte array containing the group data + * @param mode the TIC mode used. If auto mode is used, the mode will be detected from the group + * buffer + * @return the end offset for checksum computation, or null if mode is unknown + */ + public static int getOffsetEnd(byte[] groupBuffer, TICMode mode) { + Objects.requireNonNull(groupBuffer, "groupBuffer must not be null"); + return getOffsetEnd(groupBuffer, mode, 0, groupBuffer.length - 1); + } + + public static int getOffsetChecksum(byte[] buffer, int endOffset) { + return endOffset - OFFSET_CHECKSUM; + } + + /** + * Determines the end offset for checksum computation based on the TIC mode and a given offset. + * + * @param buffer the byte array containing the group data + * @param mode the TIC mode used. If auto mode is used, the mode will be detected from the group + * @param beginOffset the begin offset for the buffer + * @return the end offset for checksum computation, or null if mode is unknown + */ + public static int getOffsetEnd(byte[] buffer, TICMode mode, int beginOffset, int endOffset) { + checkBufferOffsets(buffer, beginOffset, endOffset); + if (mode == null) { + throw new IllegalArgumentException("mode must not be null"); + } + if (mode == TICMode.AUTO) { + mode = TICModeDetector.findModeFromGroupBuffer(buffer); + } + if (mode == null) { + throw new IllegalArgumentException("Unable to determine TIC mode from group buffer"); + } + int length = endOffset - beginOffset + 1; + if (mode == TICMode.HISTORIC) { + return beginOffset + (length - OFFSET_END_HISTORIC); + } else { + return beginOffset + (length - OFFSET_END_STANDARD); + } + } + + /** + * Check if the buffer length is valid for checksum computation + * + * @param buffer the TIC buffer to check + * @param offsetBegin the begin offset for checksum computation + * @param offsetEnd the end offset for checksum computation + */ + public static void checkBufferOffsets(byte[] buffer, int offsetBegin, int offsetEnd) { + Objects.requireNonNull(buffer, "groupBuffer must not be null"); + if (offsetBegin < 0) { + throw new IllegalArgumentException( + "Invalid offsetBegin for checksum computation (must be positive)"); + } + + if (offsetEnd > buffer.length) { + throw new IllegalArgumentException( + "Invalid offsetEnd for checksum computation (must be lower than data length)"); + } + + if (offsetBegin >= offsetEnd) { + throw new IllegalArgumentException( + "Invalid offset range for checksum computation (offsetBegin must be lower than" + + " offsetEnd)"); + } + if (offsetEnd - offsetBegin + 1 > buffer.length) { + throw new IllegalArgumentException( + "Invalid length for checksum computation (length must be lower than data length)"); + } + } +} diff --git a/src/main/java/tic/frame/codec/TICFrameCodec.java b/src/main/java/tic/frame/codec/TICFrameCodec.java new file mode 100644 index 0000000..abd8a0d --- /dev/null +++ b/src/main/java/tic/frame/codec/TICFrameCodec.java @@ -0,0 +1,136 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.codec; + +import tic.frame.TICFrame; +import tic.frame.TICMode; +import tic.frame.TICModeDetector; +import tic.frame.checksum.TICChecksum; +import tic.frame.checksum.TICChecksumOffset; +import tic.frame.delimiter.TICFrameDelimiter; +import tic.frame.delimiter.TICGroupDelimiter; +import tic.frame.delimiter.TICSeparator; +import tic.frame.group.TICGroup; + +public class TICFrameCodec { + + private TICFrameCodec() {} + + public static TICFrame decode(byte[] frameBuffer) { + TICMode mode = checkFrameBuffer(frameBuffer); + TICFrame frame = new TICFrame(mode); + StringBuffer groupBuffer = new StringBuffer(); + + for (int i = 1; i < frameBuffer.length; i++) { + if (groupBuffer.length() == 0) { + if (frameBuffer[i] == TICGroupDelimiter.BEGIN.getValue()) { + groupBuffer.append((char) frameBuffer[i]); + } + } else { + groupBuffer.append((char) frameBuffer[i]); + if (frameBuffer[i] == TICGroupDelimiter.END.getValue()) { + TICGroup group = decodeGroup(groupBuffer.toString().getBytes(), mode); + frame.addGroup(group); + groupBuffer.setLength(0); + } + } + } + + return frame; + } + + public static byte[] encode(TICFrame frame) { + if (frame == null) { + throw new IllegalArgumentException("frame cannot be null"); + } + StringBuffer frameBuffer = new StringBuffer(); + frameBuffer.append((char) TICFrameDelimiter.BEGIN.getValue()); + for (TICGroup group : frame.getGroupList()) { + byte[] groupBuffer = encodeGroup(group, frame.getMode()); + frameBuffer.append(new String(groupBuffer)); + } + frameBuffer.append((char) TICFrameDelimiter.END.getValue()); + return frameBuffer.toString().getBytes(); + } + + private static TICGroup decodeGroup(byte[] groupBuffer, TICMode mode) { + boolean endLabelFound = false; + StringBuffer label = new StringBuffer(); + StringBuffer value = new StringBuffer(); + byte separator = TICSeparator.getValueFromMode(mode); + int beginChecksumOffset = TICChecksumOffset.getOffsetBegin(); + int checksumOffset = TICChecksumOffset.getOffsetChecksum(groupBuffer); + int endValueOffset = checksumOffset - 1; // excluding separator before checksum + + for (int i = beginChecksumOffset; i < endValueOffset; i++) { + if (!endLabelFound) { + if (groupBuffer[i] == separator) { + endLabelFound = true; + continue; + } + label.append((char) groupBuffer[i]); + } else { + value.append((char) groupBuffer[i]); + } + } + boolean isValid = TICChecksum.verifyChecksum(groupBuffer, mode); + TICGroup group = new TICGroup(label.toString(), value.toString(), isValid); + + return group; + } + + private static byte[] encodeGroup(TICGroup group, TICMode mode) { + int checksum = 0; + byte separator = TICSeparator.getValueFromMode(mode); + StringBuffer groupBuffer = new StringBuffer(); + + groupBuffer.append((char) TICGroupDelimiter.BEGIN.getValue()); + groupBuffer.append(group.getLabel()); + groupBuffer.append((char) separator); + groupBuffer.append(group.getValue()); + groupBuffer.append((char) separator); + groupBuffer.append((char) checksum); + groupBuffer.append((char) TICGroupDelimiter.END.getValue()); + byte[] groupBufferBytes = groupBuffer.toString().getBytes(); + checksum = TICChecksum.computeChecksum(groupBufferBytes, mode); + if (!group.isValid()) { + checksum = (checksum + 1) & 0xFF; + } + int checksumOffset = TICChecksumOffset.getOffsetChecksum(groupBufferBytes); + groupBuffer.setCharAt(checksumOffset, (char) checksum); + + return groupBuffer.toString().getBytes(); + } + + private static TICMode checkFrameBuffer(byte[] frameBuffer) { + if (frameBuffer == null) { + throw new IllegalArgumentException("frameBuffer cannot be null"); + } + if (frameBuffer.length + < TICFrameDelimiter.values().length + TICGroupDelimiter.values().length) { + throw new IllegalArgumentException( + "frameBuffer length must be >= " + + (TICFrameDelimiter.values().length + TICGroupDelimiter.values().length) + + " bytes"); + } + if (frameBuffer[0] != TICFrameDelimiter.BEGIN.getValue()) { + throw new IllegalArgumentException( + "frameBuffer begin must be " + TICFrameDelimiter.BEGIN.getValue()); + } + if (frameBuffer[frameBuffer.length - 1] != TICFrameDelimiter.END.getValue()) { + throw new IllegalArgumentException( + "frameBuffer end must be " + TICFrameDelimiter.END.getValue()); + } + TICMode mode = TICModeDetector.findModeFromFrameBuffer(frameBuffer); + if (mode == null) { + throw new IllegalArgumentException("Unable to determine TIC Mode from frame buffer!"); + } + + return mode; + } +} diff --git a/src/main/java/tic/frame/codec/TICFrameDetailledCodec.java b/src/main/java/tic/frame/codec/TICFrameDetailledCodec.java new file mode 100644 index 0000000..9e68ccf --- /dev/null +++ b/src/main/java/tic/frame/codec/TICFrameDetailledCodec.java @@ -0,0 +1,67 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.codec; + +import java.util.List; +import org.json.JSONArray; +import org.json.JSONObject; +import tic.frame.TICFrame; +import tic.frame.group.TICGroup; +import tic.util.codec.JsonObjectCodec; +import tic.util.codec.JsonStringCodec; + +public class TICFrameDetailledCodec implements JsonObjectCodec, JsonStringCodec { + public static TICFrameDetailledCodec instance = null; + + private TICFrameDetailledCodec() {} + + public static final TICFrameDetailledCodec getInstance() { + if (instance == null) { + instance = new TICFrameDetailledCodec(); + } + return instance; + } + + @Override + public String encodeToJsonString(TICFrame frame, int indentFactor) throws Exception { + if (frame == null) { + throw new IllegalArgumentException("frame cannot be null"); + } + JSONObject jsonFrame = (JSONObject) encodeToJsonObject(frame); + return indentFactor > 0 ? jsonFrame.toString(indentFactor) : jsonFrame.toString(); + } + + @Override + public TICFrame decodeFromJsonString(String jsonString, int indentFactor) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonString'"); + } + + @Override + public Object encodeToJsonObject(TICFrame frame) throws Exception { + JSONObject jsonFrame = new JSONObject(); + jsonFrame.put("mode", frame.getMode().name()); + List groups = frame.getGroupList(); + JSONArray jsonGroups = new JSONArray(); + groups.forEach( + group -> { + JSONObject jsonGroup = new JSONObject(); + jsonGroup.put("label", group.getLabel()); + jsonGroup.put("value", group.getValue()); + jsonGroup.put("isValid", group.isValid()); + jsonGroups.put(jsonGroup); + }); + jsonFrame.put("groupList", jsonGroups); + return jsonFrame; + } + + @Override + public TICFrame decodeFromJsonObject(Object jsonObject) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonObject'"); + } + +} diff --git a/src/main/java/tic/frame/codec/TICFrameSummarizedCodec.java b/src/main/java/tic/frame/codec/TICFrameSummarizedCodec.java new file mode 100644 index 0000000..a93afa0 --- /dev/null +++ b/src/main/java/tic/frame/codec/TICFrameSummarizedCodec.java @@ -0,0 +1,61 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.codec; + +import java.util.List; +import org.json.JSONObject; +import tic.frame.TICFrame; +import tic.frame.group.TICGroup; +import tic.util.codec.JsonObjectCodec; +import tic.util.codec.JsonStringCodec; + +public class TICFrameSummarizedCodec + implements JsonStringCodec, JsonObjectCodec { + + public static TICFrameSummarizedCodec instance = null; + + private TICFrameSummarizedCodec() {} + + public static final TICFrameSummarizedCodec getInstance() { + if (instance == null) { + instance = new TICFrameSummarizedCodec(); + } + return instance; + } + + @Override + public String encodeToJsonString(TICFrame frame, int indentFactor) throws Exception { + JSONObject jsonFrame = (JSONObject) encodeToJsonObject(frame); + return indentFactor < 0 ? jsonFrame.toString() : jsonFrame.toString(indentFactor); + } + + @Override + public TICFrame decodeFromJsonString(String jsonString, int indentFactor) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonString'"); + } + + @Override + public Object encodeToJsonObject(TICFrame frame) throws Exception { + List groups = frame.getGroupList(); + JSONObject jsonFrame = new JSONObject(); + groups.forEach( + group -> { + if (group.isValid()) { + jsonFrame.put(group.getLabel(), group.getValue()); + } else { + jsonFrame.put("!" + group.getLabel(), group.getValue()); + } + }); + return jsonFrame; + } + + @Override + public TICFrame decodeFromJsonObject(Object jsonObject) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonObject'"); + } +} diff --git a/src/main/java/tic/frame/delimiter/TICFrameDelimiter.java b/src/main/java/tic/frame/delimiter/TICFrameDelimiter.java new file mode 100644 index 0000000..ea11779 --- /dev/null +++ b/src/main/java/tic/frame/delimiter/TICFrameDelimiter.java @@ -0,0 +1,30 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.delimiter; + +public enum TICFrameDelimiter { + /** Begin byte (STX, 0x02) for TIC frames. */ + BEGIN((byte) 0x02), + /** End byte (ETX, 0x03) for TIC frames. */ + END((byte) 0x03); + + private final byte value; + + private TICFrameDelimiter(byte value) { + this.value = value; + } + + /** + * Gets the hexadecimal value of the delimiter. + * + * @return the hexadecimal value as a byte + */ + public byte getValue() { + return value; + } +} diff --git a/src/main/java/tic/frame/delimiter/TICGroupDelimiter.java b/src/main/java/tic/frame/delimiter/TICGroupDelimiter.java new file mode 100644 index 0000000..2d5dc4c --- /dev/null +++ b/src/main/java/tic/frame/delimiter/TICGroupDelimiter.java @@ -0,0 +1,30 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.delimiter; + +public enum TICGroupDelimiter { + /** Begin byte (Line Feed, 0x0A) for TIC groups. */ + BEGIN((byte) 0x0A), + /** End byte (Carriage Return, 0x0D) for TIC groups. */ + END((byte) 0x0D); + + private final byte value; + + private TICGroupDelimiter(byte value) { + this.value = value; + } + + /** + * Gets the hexadecimal value of the delimiter. + * + * @return the hexadecimal value as a byte + */ + public byte getValue() { + return value; + } +} diff --git a/src/main/java/tic/frame/delimiter/TICSeparator.java b/src/main/java/tic/frame/delimiter/TICSeparator.java new file mode 100644 index 0000000..051bd7e --- /dev/null +++ b/src/main/java/tic/frame/delimiter/TICSeparator.java @@ -0,0 +1,56 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.delimiter; + +import tic.frame.TICMode; + +/** Enumeration representing TIC frame separators for different TIC modes. */ +public enum TICSeparator { + /** Separator character (SPACE = 0x20) for historic TIC frames. */ + HISTORIC((byte) 0x20), + /** Separator character (TAB = 0x09) for standard TIC frames. */ + STANDARD((byte) 0x09); + + private final byte value; + + /** + * Constructs a TICSeparator with the specified hexadecimal value. + * + * @param value the hexadecimal byte value of the separator + */ + private TICSeparator(byte value) { + this.value = value; + } + + /** + * Gets the hexadecimal value of the separator. + * + * @return the hexadecimal value as a byte + */ + public byte getValue() { + return value; + } + + /** + * Gets the separator value corresponding to the given TIC mode. + * + * @param mode the TIC mode + * @return the separator value as a byte + */ + public static byte getValueFromMode(TICMode mode) { + if (mode == null) { + throw new IllegalArgumentException("cannot get separator value from null mode"); + } + if (mode == TICMode.HISTORIC) { + return HISTORIC.getValue(); + } else if (mode == TICMode.STANDARD) { + return STANDARD.getValue(); + } + throw new IllegalArgumentException("cannot get separator value from " + mode.name() + " mode"); + } +} diff --git a/src/main/java/tic/frame/delimiter/TICStartPattern.java b/src/main/java/tic/frame/delimiter/TICStartPattern.java new file mode 100644 index 0000000..b65b623 --- /dev/null +++ b/src/main/java/tic/frame/delimiter/TICStartPattern.java @@ -0,0 +1,67 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.delimiter; + +import tic.frame.TICMode; + +/** Enumeration representing TIC frame start patterns for different TIC modes. */ +public enum TICStartPattern { + /** Start pattern (STX, LF, A, D, C, O, SPACE) for historic TIC frames. */ + HISTORIC(new byte[] {2, 10, 65, 68, 67, 79, 32}), + /** Start pattern (STX, LF, A, D, S, C, TAB) for standard TIC frames. */ + STANDARD(new byte[] {2, 10, 65, 68, 83, 67, 9}); + + private final byte[] value; + + /** + * Constructs a TICStartPattern with the specified hexadecimal value. + * + * @param value the byte array representing the start pattern + */ + private TICStartPattern(byte[] value) { + this.value = value; + } + + /** + * Gets the hexadecimal value of the start pattern. + * + * @return the hexadecimal value as a byte array + */ + public byte[] getValue() { + return value; + } + + /** + * Gets the start pattern value corresponding to the given TIC mode. + * + * @param mode the TIC mode + * @return the byte array representing the start pattern for the specified mode + * @throws IllegalArgumentException if the TIC mode is unsupported + */ + public static byte[] getValueFromMode(TICMode mode) { + if (mode == null) { + throw new IllegalArgumentException("cannot get start pattern value from null mode"); + } + if (mode == TICMode.HISTORIC) { + return HISTORIC.getValue(); + } else if (mode == TICMode.STANDARD) { + return STANDARD.getValue(); + } + throw new IllegalArgumentException( + "cannot get start pattern value from " + mode.name() + " mode"); + } + + /** + * Gets the length of the start pattern. + * + * @return the length as an integer + */ + public static int length() { + return 7; + } +} diff --git a/src/main/java/tic/frame/group/TICGroup.java b/src/main/java/tic/frame/group/TICGroup.java new file mode 100644 index 0000000..e345815 --- /dev/null +++ b/src/main/java/tic/frame/group/TICGroup.java @@ -0,0 +1,90 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.group; + +public class TICGroup { + private String label; + private String value; + private boolean isValid; + + /** + * Constructs a TIC group with the specified label and value. + * + * @param label the label of the group + * @param value the value of the group + */ + public TICGroup(String label, String value) { + this.label = label; + this.value = value; + this.isValid = true; + } + + /** + * Constructs a TIC group with the specified label, value, and validity. + * + * @param label the label of the group + * @param value the value of the group + * @param isValid the validity of the group + */ + public TICGroup(String label, String value, boolean isValid) { + this.label = label; + this.value = value; + this.isValid = isValid; + } + + @Override + public int hashCode() { + return this.label.hashCode() + this.value.hashCode() + Boolean.hashCode(this.isValid); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + TICGroup other = (TICGroup) obj; + return this.label.equals(other.label) + && this.value.equals(other.value) + && this.isValid == other.isValid; + } + + /** + * Sets the validity of the TIC group. + * + * @param isValid true if the group is valid, false otherwise + */ + public boolean isValid() { + return this.isValid; + } + + /** + * Returns the label of the TIC group. + * + * @return the label of the group + */ + public String getLabel() { + return this.label; + } + + /** + * Returns the value of the TIC group. + * + * @return the value of the group + */ + public String getValue() { + return this.value; + } + + @Override + public String toString() { + return "(label=" + this.label + ", value=" + this.value + ", isValid=" + this.isValid + ")"; + } +} diff --git a/src/main/java/enedis/lab/io/PlugSubscriber.java b/src/main/java/tic/io/PlugSubscriber.java similarity index 96% rename from src/main/java/enedis/lab/io/PlugSubscriber.java rename to src/main/java/tic/io/PlugSubscriber.java index 86f0b18..2be9052 100644 --- a/src/main/java/enedis/lab/io/PlugSubscriber.java +++ b/src/main/java/tic/io/PlugSubscriber.java @@ -5,9 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.io; +package tic.io; -import enedis.lab.util.task.Subscriber; +import tic.util.task.Subscriber; /** * Listener interface for receiving plug and unplug events from devices or hardware. diff --git a/src/main/java/enedis/lab/io/PortFinder.java b/src/main/java/tic/io/PortFinder.java similarity index 92% rename from src/main/java/enedis/lab/io/PortFinder.java rename to src/main/java/tic/io/PortFinder.java index 9b21b6c..ee2b66d 100644 --- a/src/main/java/enedis/lab/io/PortFinder.java +++ b/src/main/java/tic/io/PortFinder.java @@ -5,9 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.io; +package tic.io; -import enedis.lab.types.DataList; +import java.util.List; /** * Generic interface for discovering and enumerating ports on the system. @@ -22,7 +22,7 @@ * * @param the type of port descriptor returned by this finder * @author Enedis Smarties team - * @see DataList + * @see List */ public interface PortFinder { /** @@ -34,5 +34,5 @@ public interface PortFinder { * * @return a list of port descriptors representing all discovered ports */ - public DataList findAll(); + public List findAll(); } diff --git a/src/main/java/enedis/lab/io/PortPlugNotifier.java b/src/main/java/tic/io/PortPlugNotifier.java similarity index 93% rename from src/main/java/enedis/lab/io/PortPlugNotifier.java rename to src/main/java/tic/io/PortPlugNotifier.java index 2c0e251..4950109 100644 --- a/src/main/java/enedis/lab/io/PortPlugNotifier.java +++ b/src/main/java/tic/io/PortPlugNotifier.java @@ -5,14 +5,14 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.io; +package tic.io; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; -import enedis.lab.util.task.TaskPeriodicWithSubscribers; -import enedis.lab.util.time.Time; +import tic.util.time.Time; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import tic.util.task.TaskPeriodicWithSubscribers; /** * Periodic monitor that detects and notifies when ports are plugged or unplugged. @@ -43,7 +43,7 @@ public class PortPlugNotifier, T> extends TaskPeriodicWithSubscribers> { private AtomicReference finder = new AtomicReference(); - private DataList descriptors = new DataArrayList(); + private List descriptors = new ArrayList(); /** * Main utility method that runs a port plug monitoring loop. @@ -126,7 +126,7 @@ public void setFinder(F finder) { */ @Override protected void process() { - DataList newDescriptors = this.finder.get().findAll(); + List newDescriptors = this.finder.get().findAll(); this.checkAndNotifyIfUnplugged(newDescriptors); this.checkAndNotifyIfPlugged(newDescriptors); @@ -142,7 +142,7 @@ protected void process() { * * @param newDescriptors the newly discovered list of port descriptors */ - private void checkAndNotifyIfUnplugged(DataList newDescriptors) { + private void checkAndNotifyIfUnplugged(List newDescriptors) { Iterator it = this.descriptors.iterator(); while (it.hasNext()) { @@ -161,7 +161,7 @@ private void checkAndNotifyIfUnplugged(DataList newDescriptors) { * * @param newDescriptors the newly discovered list of port descriptors */ - private void checkAndNotifyIfPlugged(DataList newDescriptors) { + private void checkAndNotifyIfPlugged(List newDescriptors) { Iterator it = newDescriptors.iterator(); while (it.hasNext()) { @@ -180,7 +180,7 @@ private void checkAndNotifyIfPlugged(DataList newDescriptors) { * * @param newDescriptors the new list of port descriptors to cache */ - private void updateDescriptors(DataList newDescriptors) { + private void updateDescriptors(List newDescriptors) { synchronized (this.descriptors) { this.descriptors.clear(); this.descriptors.addAll(newDescriptors); diff --git a/src/main/java/tic/io/modem/ModemDescriptor.java b/src/main/java/tic/io/modem/ModemDescriptor.java new file mode 100644 index 0000000..fa41ce0 --- /dev/null +++ b/src/main/java/tic/io/modem/ModemDescriptor.java @@ -0,0 +1,126 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.modem; + +import java.util.Objects; +import tic.io.serialport.SerialPortDescriptor; +import tic.io.usb.UsbPortDescriptor; + +/** Descriptor of a modem. */ +public class ModemDescriptor extends SerialPortDescriptor { + private ModemType modemType; + + public static class Builder> extends SerialPortDescriptor.Builder { + private ModemType modemType; + + public T modemType(ModemType modemType) { + this.modemType = modemType; + if (modemType == null) { + this.productId = null; + this.vendorId = null; + } else { + this.productId = modemType.productId(); + this.vendorId = modemType.vendorId(); + } + return self(); + } + + public T copy(SerialPortDescriptor serialPortDescriptor) { + this.portId = serialPortDescriptor.portId(); + this.portName = serialPortDescriptor.portName(); + this.description = serialPortDescriptor.description(); + this.productId = serialPortDescriptor.productId(); + this.vendorId = serialPortDescriptor.vendorId(); + this.productName = serialPortDescriptor.productName(); + this.manufacturer = serialPortDescriptor.manufacturer(); + this.serialNumber = serialPortDescriptor.serialNumber(); + return self(); + } + + public T copy(UsbPortDescriptor usbPortDescriptor) { + this.productId = usbPortDescriptor.idProduct(); + this.vendorId = usbPortDescriptor.idVendor(); + this.productName = usbPortDescriptor.product(); + this.manufacturer = usbPortDescriptor.manufacturer(); + this.serialNumber = usbPortDescriptor.serialNumber(); + return self(); + } + + /** + * Validates the builder's fields. + * + * @throws IllegalArgumentException if any required field is invalid + */ + protected void validate() { + super.validate(); + if (this.modemType != null) { + if (this.productId == null) { + throw new IllegalArgumentException( + "Modem type is specified while productId is not provided"); + } + if (this.productId != this.modemType.productId()) { + throw new IllegalArgumentException("Modem type is inconsistent with the productId"); + } + if (this.vendorId == null) { + throw new IllegalArgumentException( + "Modem type is specified while vendorId is not provided"); + } + if (this.vendorId != this.modemType.vendorId()) { + throw new IllegalArgumentException("Modem type is inconsistent with the vendorId"); + } + } + } + + public ModemDescriptor build() { + this.validate(); + return new ModemDescriptor(this); + } + } + + /** + * Constructs a ModemDescriptor with all parameters explicitly set. + * + *

    This constructor creates a descriptor with all serial port and USB device properties + * specified according to the modem type. + * + * @param builder the builder instance containing all the parameters + */ + public ModemDescriptor(Builder builder) { + super(builder); + this.modemType = builder.modemType; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ModemDescriptor)) { + return false; + } + ModemDescriptor other = (ModemDescriptor) obj; + if (this.modemType != other.modemType) { + return false; + } + return super.equals(obj); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), this.modemType); + } + + /** + * Get modem type + * + * @return the modem type + */ + public ModemType modemType() { + return this.modemType; + } +} diff --git a/src/main/java/tic/io/modem/ModemFinder.java b/src/main/java/tic/io/modem/ModemFinder.java new file mode 100644 index 0000000..040fd75 --- /dev/null +++ b/src/main/java/tic/io/modem/ModemFinder.java @@ -0,0 +1,69 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.modem; + +import java.util.List; +import tic.io.PortFinder; + +/** Interface used to find all modem descriptors */ +public interface ModemFinder extends PortFinder { + /** + * Find modem descriptor matching with port id. + * + * @param portId the unique port identifier desired + * @return modem descriptor found, or null if nothing matches with portId + */ + public default ModemDescriptor findByPortId(String portId) { + return this.findAll().stream() + .filter(p -> (p.portId() != null) ? p.portId().equals(portId) : portId == null) + .findFirst() + .orElse(null); + } + + /** + * Find modem descriptor matching with port name. + * + * @param portName the port name desired + * @return modem descriptor found, or null if nothing matches with portName + */ + public default ModemDescriptor findByPortName(String portName) { + return this.findAll().stream() + .filter( + p -> (p.portName() != null) ? p.portName().equals(portName) : portName == null) + .findFirst() + .orElse(null); + } + + /** + * Find native modem (not USB) descriptor matching with port name. + * + * @param portName the port name desired + * @return modem descriptor found, or null if nothing matches with portName + */ + public ModemDescriptor findNative(String portName); + + /** + * Find modem descriptor matching with port id or port name. + * + * @param portId the unique port identifier desired + * @param portName the port name desired + * @return modem descriptor found, or null if nothing matches with portName + */ + public default ModemDescriptor findByPortIdOrPortName(String portId, String portName) { + ModemDescriptor descriptor = this.findByPortId(portId); + + if (descriptor == null) { + descriptor = this.findByPortName(portName); + } + + return descriptor; + } + + @Override + List findAll(); +} diff --git a/src/main/java/tic/io/modem/ModemFinderBase.java b/src/main/java/tic/io/modem/ModemFinderBase.java new file mode 100644 index 0000000..bbdc06e --- /dev/null +++ b/src/main/java/tic/io/modem/ModemFinderBase.java @@ -0,0 +1,114 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.modem; + +import java.util.ArrayList; +import java.util.List; +import tic.io.serialport.SerialPortDescriptor; +import tic.io.serialport.SerialPortFinder; +import tic.io.usb.UsbPortDescriptor; +import tic.io.usb.UsbPortFinder; + +/** Class used to find all modem descriptors */ +public class ModemFinderBase implements ModemFinder { + + /** + * Create ModemFinder instance + * + * @param serialPortFinder the SerialPortFinder instance + * @param usbPortFinder the UsbPortFinder instance + * @return ModemFinder instance created + * @throws IllegalArgumentException if any input instance is null + */ + public static ModemFinder create(SerialPortFinder serialPortFinder, UsbPortFinder usbPortFinder) { + + if (serialPortFinder == null) { + throw new IllegalArgumentException("Cannot set null serial port finder"); + } + if (usbPortFinder == null) { + throw new IllegalArgumentException("Cannot set null USB port finder"); + } + + return new ModemFinderBase(serialPortFinder, usbPortFinder); + } + + private final SerialPortFinder serialPortFinder; + private final UsbPortFinder usbPortFinder; + + /** + * Constructor with finder parameters + * + * @param serialPortFinder the serial port finder interface + * @param usbPortFinder the USB port finder interface + */ + private ModemFinderBase(SerialPortFinder serialPortFinder, UsbPortFinder usbPortFinder) { + this.serialPortFinder = serialPortFinder; + this.usbPortFinder = usbPortFinder; + } + + @Override + public List findAll() { + List descriptors = new ArrayList<>(); + + for (ModemType modemType : ModemType.values()) { + List tmpSerialPort = + this.serialPortFinder.findByProductIdAndVendorId( + modemType.productId(), modemType.vendorId()); + + if (tmpSerialPort.isEmpty()) { + List tmpUSBPort = + this.usbPortFinder.findByProductIdAndVendorId( + modemType.productId(), modemType.vendorId()); + + for (UsbPortDescriptor upd : tmpUSBPort) { + try { + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .copy(upd) + .modemType(modemType) + .build(); + descriptors.add(descriptor); + } catch (IllegalArgumentException e) { + // Ignore descriptors that fail validation + } + } + } else { + for (SerialPortDescriptor spd : tmpSerialPort) { + try { + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .copy(spd) + .modemType(modemType) + .build(); + descriptors.add(descriptor); + } catch (IllegalArgumentException e) { + // Ignore descriptors that fail validation + } + } + } + } + + return descriptors; + } + + @Override + public ModemDescriptor findNative(String portName) { + ModemDescriptor modemDescriptor = null; + SerialPortDescriptor serialPortDescriptor = this.serialPortFinder.findNative(portName); + + if (serialPortDescriptor != null) { + try { + modemDescriptor = new ModemDescriptor.Builder<>().copy(serialPortDescriptor).build(); + } catch (IllegalArgumentException e) { + // Ignore descriptors that fail validation + } + } + + return modemDescriptor; + } +} diff --git a/src/main/java/tic/io/modem/ModemJsonCodec.java b/src/main/java/tic/io/modem/ModemJsonCodec.java new file mode 100644 index 0000000..6297207 --- /dev/null +++ b/src/main/java/tic/io/modem/ModemJsonCodec.java @@ -0,0 +1,94 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.modem; + +import java.util.Collections; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONObject; +import tic.util.codec.JsonArrayCodec; +import tic.util.codec.JsonObjectCodec; +import tic.util.codec.JsonStringCodec; + +/** Utility class to convert modem descriptors to their JSON representation. */ +public final class ModemJsonCodec + implements JsonArrayCodec>, + JsonObjectCodec, + JsonStringCodec { + + private static ModemJsonCodec instance = new ModemJsonCodec(); + + public static ModemJsonCodec getInstance() { + if (instance == null) { + instance = new ModemJsonCodec(); + } + return instance; + } + + private ModemJsonCodec() {} + + @Override + public Object encodeToJsonArray(List descriptors) throws Exception { + List safeList = descriptors == null ? Collections.emptyList() : descriptors; + + JSONArray array = new JSONArray(); + safeList.forEach( + descriptor -> { + try { + array.put(encodeToJsonObject(descriptor)); + } catch (Exception e) { + array.put(JSONObject.NULL); + } + }); + + return array; + } + + @Override + public List decodeFromJsonArray(Object jsonArray) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonArray'"); + } + + @Override + public Object encodeToJsonObject(ModemDescriptor descriptor) throws Exception { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("portId", descriptor.portId()); + jsonObject.put("portName", descriptor.portName()); + jsonObject.put("description", descriptor.description()); + jsonObject.put("productId", descriptor.productId()); + jsonObject.put("vendorId", descriptor.vendorId()); + jsonObject.put("productName", descriptor.productName()); + jsonObject.put("manufacturer", descriptor.manufacturer()); + jsonObject.put("serialNumber", descriptor.serialNumber()); + jsonObject.put( + "modemType", + descriptor.modemType() == null ? JSONObject.NULL : descriptor.modemType().name()); + return jsonObject; + } + + @Override + public ModemDescriptor decodeFromJsonObject(Object jsonObject) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonObject'"); + } + + @Override + public String encodeToJsonString(ModemDescriptor descriptor, int indentFactor) throws Exception { + if (descriptor == null) { + throw new IllegalArgumentException("descriptor cannot be null"); + } + + JSONObject json = (JSONObject) encodeToJsonObject(descriptor); + return indentFactor < 0 ? json.toString() : json.toString(indentFactor); + } + + @Override + public ModemDescriptor decodeFromJsonString(String jsonString, int indentFactor) + throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'decodeFromJsonString'"); + } +} diff --git a/src/main/java/tic/io/modem/ModemPlugNotifier.java b/src/main/java/tic/io/modem/ModemPlugNotifier.java new file mode 100644 index 0000000..3b58e8e --- /dev/null +++ b/src/main/java/tic/io/modem/ModemPlugNotifier.java @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.modem; + +import tic.io.PortPlugNotifier; + +/** Class used to notify when a modem has been plugged or unplugged */ +public class ModemPlugNotifier extends PortPlugNotifier { + + /** + * Constructor with all parameters + * + * @param period the period (in milliseconds) used to look for plugged or unplugged modems + * @param finder the modem finder interface used to find all modem descriptors + */ + public ModemPlugNotifier(long period, ModemFinder finder) { + super(period, finder); + } +} diff --git a/src/main/java/enedis/lab/io/tic/TICModemType.java b/src/main/java/tic/io/modem/ModemType.java similarity index 60% rename from src/main/java/enedis/lab/io/tic/TICModemType.java rename to src/main/java/tic/io/modem/ModemType.java index c99f0bd..532741d 100644 --- a/src/main/java/enedis/lab/io/tic/TICModemType.java +++ b/src/main/java/tic/io/modem/ModemType.java @@ -5,19 +5,19 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.io.tic; +package tic.io.modem; -/** TIC Modem type */ -public enum TICModemType { +/** Supported modem types. */ +public enum ModemType { /** Modem michaud */ - MICHAUD(0x6001, 0x0403), + MICHAUD((short) 0x6001, (short) 0x0403), /** Télé info */ - TELEINFO(0x6015, 0x0403); + TELEINFO((short) 0x6015, (short) 0x0403); - private int productId; - private int vendorId; + private final short productId; + private final short vendorId; - TICModemType(int productId, int vendorId) { + ModemType(short productId, short vendorId) { this.productId = productId; this.vendorId = vendorId; } @@ -27,7 +27,7 @@ public enum TICModemType { * * @return product id */ - public int getProductId() { + public short productId() { return this.productId; } @@ -36,7 +36,7 @@ public int getProductId() { * * @return vendor id */ - public int getVendorId() { + public short vendorId() { return this.vendorId; } } diff --git a/src/main/java/tic/io/serialport/SerialPortDescriptor.java b/src/main/java/tic/io/serialport/SerialPortDescriptor.java new file mode 100644 index 0000000..34c24b4 --- /dev/null +++ b/src/main/java/tic/io/serialport/SerialPortDescriptor.java @@ -0,0 +1,269 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.serialport; + +import java.util.Objects; +import tic.util.ValueChecker; + +/** + * Descriptor class for serial port information and USB device properties. + * + *

    This class provides a structured way to store and access serial port information, including + * both native serial ports and USB-based serial devices, without relying on the legacy + * DataDictionary infrastructure. It encapsulates all relevant properties needed to identify, + * configure, and connect to serial ports. + * + *

    The descriptor includes the following information: + * + *

      + *
    • Port unique identifier + *
    • Port name used to open the serial port + *
    • Port description + *
    • USB device product identifier (PID) + *
    • USB device vendor identifier (VID) + *
    • USB device product name + *
    • USB device manufacturer + *
    • USB device serial number + *
    + * + *

    The class supports distinguishing between native serial ports (e.g., COM ports) and USB serial + * devices through the {@link #isNative()} method. + * + * @author Enedis Smarties team + */ +public class SerialPortDescriptor { + + private String portId; + private String portName; + private String description; + private Short productId; + private Short vendorId; + private String productName; + private String manufacturer; + private String serialNumber; + + /** Builder class for constructing SerialPortDescriptor instances. */ + public static class Builder> { + protected String portId; + protected String portName; + protected String description; + protected Short productId; + protected Short vendorId; + protected String productName; + protected String manufacturer; + protected String serialNumber; + + @SuppressWarnings("unchecked") + public T self() { + return (T) this; + } + + public T portId(String portId) { + this.portId = portId; + return self(); + } + + public T portName(String portName) { + this.portName = portName; + return self(); + } + + public T description(String description) { + this.description = description; + return self(); + } + + public T productId(Short productId) { + this.productId = productId; + return self(); + } + + public T vendorId(Short vendorId) { + this.vendorId = vendorId; + return self(); + } + + public T productName(String productName) { + this.productName = productName; + return self(); + } + + public T manufacturer(String manufacturer) { + this.manufacturer = manufacturer; + return self(); + } + + public T serialNumber(String serialNumber) { + this.serialNumber = serialNumber; + return self(); + } + + /** + * Validates the builder's fields. + * + * @throws IllegalArgumentException if any required field is invalid + */ + protected void validate() { + ValueChecker.validateString(this.portId, "portId", true, false); + ValueChecker.validateString(this.portName, "portName", true, false); + ValueChecker.validateString(this.description, "description", true, false); + ValueChecker.validateNumber(this.productId, "productId", true); + ValueChecker.validateNumber(this.vendorId, "vendorId", true); + ValueChecker.validateString(this.productName, "productName", true, false); + ValueChecker.validateString(this.manufacturer, "manufacturer", true, false); + ValueChecker.validateString(this.serialNumber, "serialNumber", true, false); + } + + /** + * Builds the SerialPortDescriptor instance with the provided parameters. + * + * @return the constructed SerialPortDescriptor instance + * @throws IllegalArgumentException if any required field is invalid + */ + public SerialPortDescriptor build() { + this.validate(); + + return new SerialPortDescriptor(this); + } + } + + /** + * Constructs a SerialPortDescriptor with all parameters explicitly set. + * + *

    This constructor creates a descriptor with all serial port and USB device properties + * specified. + * + * @param builder the builder instance containing all the parameters + */ + protected SerialPortDescriptor(Builder builder) { + this.portId = builder.portId; + this.portName = builder.portName; + this.description = builder.description; + this.productId = builder.productId; + this.vendorId = builder.vendorId; + this.productName = builder.productName; + this.manufacturer = builder.manufacturer; + this.serialNumber = builder.serialNumber; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof SerialPortDescriptor)) { + return false; + } + SerialPortDescriptor other = (SerialPortDescriptor) obj; + return Objects.equals(this.portId, other.portId) + && Objects.equals(this.portName, other.portName) + && Objects.equals(this.description, other.description) + && Objects.equals(this.productId, other.productId) + && Objects.equals(this.vendorId, other.vendorId) + && Objects.equals(this.productName, other.productName) + && Objects.equals(this.manufacturer, other.manufacturer) + && Objects.equals(this.serialNumber, other.serialNumber); + } + + @Override + public int hashCode() { + return Objects.hash( + this.portId, + this.portName, + this.description, + this.productId, + this.vendorId, + this.productName, + this.manufacturer, + this.serialNumber); + } + + /** + * Returns the port unique identifier. + * + * @return the port unique identifier + */ + public String portId() { + return this.portId; + } + + /** + * Returns the port name used to open the serial port. + * + * @return the port name used to open the serial port + */ + public String portName() { + return this.portName; + } + + /** + * Returns the port description. + * + * @return the port description + */ + public String description() { + return this.description; + } + + /** + * Returns the USB device product identifier (PID). + * + * @return the USB device product identifier in range [0-65535] + */ + public Short productId() { + return this.productId; + } + + /** + * Returns the USB device vendor identifier (VID). + * + * @return the USB device vendor identifier in range [0-65535] + */ + public Short vendorId() { + return this.vendorId; + } + + /** + * Returns the USB device product name. + * + * @return the USB device product name + */ + public String productName() { + return this.productName; + } + + /** + * Returns the USB device manufacturer. + * + * @return the USB device manufacturer + */ + public String manufacturer() { + return this.manufacturer; + } + + /** + * Returns the USB device serial number. + * + * @return the USB device serial number + */ + public String serialNumber() { + return this.serialNumber; + } + + /** + * Checks if this serial port is a native port (not USB). + * + *

    A port is considered native if it has a port name but no port ID. This typically indicates a + * built-in serial port (e.g., COM1) rather than a USB-to-serial adapter. + * + * @return true if this is a native serial port, false if it's a USB device + */ + public boolean isNative() { + return this.portName != null && this.portId == null; + } +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java b/src/main/java/tic/io/serialport/SerialPortFinder.java similarity index 83% rename from src/main/java/enedis/lab/io/serialport/SerialPortFinder.java rename to src/main/java/tic/io/serialport/SerialPortFinder.java index e94cb91..b7c10d1 100644 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java +++ b/src/main/java/tic/io/serialport/SerialPortFinder.java @@ -5,11 +5,11 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.io.serialport; +package tic.io.serialport; -import enedis.lab.io.PortFinder; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; +import java.util.ArrayList; +import java.util.List; +import tic.io.PortFinder; /** * Interface for discovering and searching serial ports on the system. @@ -38,7 +38,7 @@ public interface SerialPortFinder extends PortFinder { */ public default SerialPortDescriptor findByPortId(String portId) { return this.findAll().stream() - .filter(k -> (k.getPortId() != null) ? k.getPortId().equals(portId) : portId == null) + .filter(k -> (k.portId() != null) ? k.portId().equals(portId) : portId == null) .findFirst() .orElse(null); } @@ -55,8 +55,7 @@ public default SerialPortDescriptor findByPortId(String portId) { */ public default SerialPortDescriptor findByPortName(String portName) { return this.findAll().stream() - .filter( - k -> (k.getPortName() != null) ? k.getPortName().equals(portName) : portName == null) + .filter(k -> (k.portName() != null) ? k.portName().equals(portName) : portName == null) .findFirst() .orElse(null); } @@ -73,7 +72,7 @@ public default SerialPortDescriptor findByPortName(String portName) { */ public default SerialPortDescriptor findNative(String portName) { return this.findAll().stream() - .filter(k -> k.isNative() && k.getPortName().equals(portName)) + .filter(k -> k.isNative() && k.portName().equals(portName)) .findFirst() .orElse(null); } @@ -115,19 +114,18 @@ public default SerialPortDescriptor findByPortIdOrPortName(String portId, String * @param vendorId the USB device vendor identifier (VID) to match * @return a list of matching serial port descriptors (empty list if no matches found) */ - public default DataList findByProductIdAndVendorId( - Number productId, Number vendorId) { - DataList descriptorList = new DataArrayList(); + public default List findByProductIdAndVendorId( + Short productId, Short vendorId) { + List descriptorList = new ArrayList<>(); for (SerialPortDescriptor descriptor : this.findAll()) { - if (descriptor.getProductId() == null && productId != null) { + if (descriptor.productId() == null && productId != null) { continue; } - if (descriptor.getVendorId() == null && vendorId != null) { + if (descriptor.vendorId() == null && vendorId != null) { continue; } - if (descriptor.getProductId().equals(productId) - && descriptor.getVendorId().equals(vendorId)) { + if (descriptor.productId().equals(productId) && descriptor.vendorId().equals(vendorId)) { descriptorList.add(descriptor); } } diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java b/src/main/java/tic/io/serialport/SerialPortFinderBase.java similarity index 66% rename from src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java rename to src/main/java/tic/io/serialport/SerialPortFinderBase.java index f9d934a..079e757 100644 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java +++ b/src/main/java/tic/io/serialport/SerialPortFinderBase.java @@ -5,9 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.io.serialport; +package tic.io.serialport; -import enedis.lab.types.DataList; +import java.util.List; import org.apache.commons.lang3.SystemUtils; /** @@ -32,21 +32,6 @@ * @see SerialPortDescriptor */ public class SerialPortFinderBase implements SerialPortFinder { - private static SerialPortFinder instance; - - /** - * Main method that outputs all serial port descriptors in JSON format. - * - *

    This utility method can be used to quickly inspect available serial ports on the system. The - * output is formatted JSON with an indentation of 2 spaces. - * - * @param args command-line arguments (not used) - */ - public static void main(String[] args) { - DataList descriptors = getInstance().findAll(); - - System.out.println(descriptors.toString(2)); - } /** * Returns the singleton instance of the serial port finder. @@ -60,27 +45,26 @@ public static void main(String[] args) { *

      *
    • Windows - returns Windows-specific finder *
    • Linux - returns Linux-specific finder + *
    • Mac OS X - returns Mac OS X-specific finder *
    * * @return the singleton SerialPortFinder instance for the current OS * @throws RuntimeException if the current operating system is not supported */ public static SerialPortFinder getInstance() { - if (instance == null) { - if (SystemUtils.IS_OS_WINDOWS) { - instance = SerialPortFinderForWindows.getInstance(); - } else if (SystemUtils.IS_OS_LINUX) { - instance = SerialPortFinderForLinux.getInstance(); - } else { - throw new RuntimeException( - "Operating system " - + SystemUtils.OS_NAME - + " is not handled by " - + SerialPortFinderBase.class.getName()); - } + if (SystemUtils.IS_OS_WINDOWS) { + return SerialPortFinderForWindows.getInstance(); + } else if (SystemUtils.IS_OS_LINUX) { + return SerialPortFinderForLinux.getInstance(); + } else if (SystemUtils.IS_OS_MAC_OSX) { + return SerialPortFinderForMacOsX.getInstance(); + } else { + throw new RuntimeException( + "Operating system " + + SystemUtils.OS_NAME + + " is not handled by " + + SerialPortFinderBase.class.getName()); } - - return instance; } /** @@ -99,7 +83,7 @@ private SerialPortFinderBase() {} * @return a list of all serial port descriptors available on the system */ @Override - public DataList findAll() { - return instance.findAll(); + public List findAll() { + return getInstance().findAll(); } } diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java b/src/main/java/tic/io/serialport/SerialPortFinderForLinux.java similarity index 84% rename from src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java rename to src/main/java/tic/io/serialport/SerialPortFinderForLinux.java index 60783fa..c7bd2c3 100644 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java +++ b/src/main/java/tic/io/serialport/SerialPortFinderForLinux.java @@ -5,16 +5,13 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.io.serialport; +package tic.io.serialport; import com.sun.jna.platform.linux.Udev; import com.sun.jna.platform.linux.Udev.UdevContext; import com.sun.jna.platform.linux.Udev.UdevDevice; import com.sun.jna.platform.linux.Udev.UdevEnumerate; import com.sun.jna.platform.linux.Udev.UdevListEntry; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; @@ -91,9 +88,8 @@ private SerialPortFinderForLinux() {} * @return a list of all serial port descriptors found on the system */ @Override - public DataList findAll() { - DataList serialPortDescriptorList = - new DataArrayList(); + public List findAll() { + List serialPortDescriptorList = new ArrayList<>(); serialPortDescriptorList = availablePortsByUdev(); if (serialPortDescriptorList.isEmpty()) { @@ -118,14 +114,13 @@ public DataList findAll() { * * @return a list of serial port descriptors discovered via udev */ - private static DataList availablePortsByUdev() { + private static List availablePortsByUdev() { UdevContext udev = Udev.INSTANCE.udev_new(); UdevEnumerate enumerate = Udev.INSTANCE.udev_enumerate_new(udev); Udev.INSTANCE.udev_enumerate_add_match_subsystem(enumerate, "tty"); Udev.INSTANCE.udev_enumerate_scan_devices(enumerate); - DataList serialPortDescriptorList = - new DataArrayList(); + List serialPortDescriptorList = new ArrayList<>(); for (UdevListEntry dev_list_entry = Udev.INSTANCE.udev_enumerate_get_list_entry(enumerate); dev_list_entry != null; @@ -153,22 +148,23 @@ private static DataList availablePortsByUdev() { String portId = devicePortId(device); String manufacturer = deviceManufacturer(device); String serialNumber = deviceSerialNumber(device); - Number vendorIdentifier = deviceVendorIdentifier(device); - Number productIdentifier = deviceProductIdentifier(device); + Short vendorIdentifier = deviceVendorIdentifier(device); + Short productIdentifier = deviceProductIdentifier(device); String productName = deviceProductName(device); try { serialPortDescriptor = - new SerialPortDescriptor( - portId, - location, - description, - productIdentifier, - vendorIdentifier, - productName, - manufacturer, - serialNumber); - } catch (DataDictionaryException e) { - e.printStackTrace(); + new SerialPortDescriptor.Builder<>() + .portId(portId) + .portName(location) + .description(description) + .productId(productIdentifier) + .vendorId(vendorIdentifier) + .productName(productName) + .manufacturer(manufacturer) + .serialNumber(serialNumber) + .build(); + } catch (IllegalArgumentException e) { + e.printStackTrace(System.err); Udev.INSTANCE.udev_device_unref(device); continue; } @@ -182,9 +178,18 @@ private static DataList availablePortsByUdev() { } try { serialPortDescriptor = - new SerialPortDescriptor(null, location, null, null, null, null, null, null); - } catch (DataDictionaryException e) { - e.printStackTrace(); + new SerialPortDescriptor.Builder<>() + .portId(null) + .portName(location) + .description(null) + .productId(null) + .vendorId(null) + .productName(null) + .manufacturer(null) + .serialNumber(null) + .build(); + } catch (IllegalArgumentException e) { + e.printStackTrace(System.err); Udev.INSTANCE.udev_device_unref(device); continue; } @@ -207,9 +212,8 @@ private static DataList availablePortsByUdev() { * * @return a list of serial port descriptors discovered via sysfs */ - private static DataList availablePortsBySysfs() { - DataList serialPortDescriptorList = - new DataArrayList(); + private static List availablePortsBySysfs() { + List serialPortDescriptorList = new ArrayList<>(); File ttySysClassDir = new File("/sys/class/tty"); if (!(ttySysClassDir.exists() && ttySysClassDir.canRead())) { @@ -254,8 +258,8 @@ public boolean accept(File pathname) { String description = null; String manufacturer = null; String serialNumber = null; - Number vendorIdentifier = null; - Number productIdentifier = null; + Short vendorIdentifier = null; + Short productIdentifier = null; String productName = null; do { if (description == null) { @@ -288,17 +292,18 @@ public boolean accept(File pathname) { } while (targetDir != null); try { serialPortDescriptor = - new SerialPortDescriptor( - null, - portName, - description, - productIdentifier, - vendorIdentifier, - productName, - manufacturer, - serialNumber); - } catch (DataDictionaryException e) { - e.printStackTrace(); + new SerialPortDescriptor.Builder<>() + .portId(null) + .portName(portName) + .description(description) + .productId(productIdentifier) + .vendorId(vendorIdentifier) + .productName(productName) + .manufacturer(manufacturer) + .serialNumber(serialNumber) + .build(); + } catch (IllegalArgumentException e) { + e.printStackTrace(System.err); continue; } serialPortDescriptorList.add(serialPortDescriptor); @@ -316,9 +321,8 @@ public boolean accept(File pathname) { * * @return a list of serial port descriptors discovered by device file filtering */ - private static DataList availablePortsByFiltersOfDevices() { - DataList serialPortDescriptorList = - new DataArrayList(); + private static List availablePortsByFiltersOfDevices() { + List serialPortDescriptorList = new ArrayList<>(); List deviceFilePaths = filteredDeviceFilePaths(); for (String deviceFilePath : deviceFilePaths) { @@ -326,9 +330,18 @@ private static DataList availablePortsByFiltersOfDevices() String portName = portNameFromSystemLocation(deviceFilePath); try { serialPortDescriptor = - new SerialPortDescriptor(null, portName, null, null, null, null, null, null); - } catch (DataDictionaryException e) { - e.printStackTrace(); + new SerialPortDescriptor.Builder<>() + .portId(null) + .portName(portName) + .description(null) + .productId(null) + .vendorId(null) + .productName(null) + .manufacturer(null) + .serialNumber(null) + .build(); + } catch (IllegalArgumentException e) { + e.printStackTrace(System.err); continue; } serialPortDescriptorList.add(serialPortDescriptor); @@ -465,11 +478,11 @@ private static String deviceManufacturer(File targetDir) { return deviceProperty(new File(targetDir, "manufacturer").getAbsolutePath()); } - private static Number deviceProductIdentifier(UdevDevice dev) { - Number identifierValue; + private static Short deviceProductIdentifier(UdevDevice dev) { + Short identifierValue; try { String indentifierText = deviceProperty(dev, "ID_MODEL_ID"); - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + identifierValue = (indentifierText != null) ? Short.parseShort(indentifierText, 16) : null; } catch (Exception e) { identifierValue = null; } @@ -484,30 +497,30 @@ private static String deviceProductName(File targetDir) { return deviceProperty(new File(targetDir, "product").getAbsolutePath()); } - private static Number deviceProductIdentifier(File targetDir) { + private static Short deviceProductIdentifier(File targetDir) { String indentifierText = deviceProperty(new File(targetDir, "idProduct").getAbsolutePath()); if (indentifierText == null) { indentifierText = deviceProperty(new File(targetDir, "device").getAbsolutePath()); } - Number identifierValue; + Short identifierValue; try { - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + identifierValue = (indentifierText != null) ? Short.parseShort(indentifierText, 16) : null; } catch (Exception e) { identifierValue = null; } return identifierValue; } - private static Number deviceVendorIdentifier(File targetDir) { + private static Short deviceVendorIdentifier(File targetDir) { String indentifierText = deviceProperty(new File(targetDir, "idVendor").getAbsolutePath()); if (indentifierText == null) { indentifierText = deviceProperty(new File(targetDir, "vendor").getAbsolutePath()); } - Number identifierValue; + Short identifierValue; try { - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + identifierValue = (indentifierText != null) ? Short.parseShort(indentifierText, 16) : null; } catch (Exception e) { identifierValue = null; } @@ -518,11 +531,11 @@ private static String deviceSerialNumber(File targetDir) { return deviceProperty(new File(targetDir, "serial").getAbsolutePath()); } - private static Number deviceVendorIdentifier(UdevDevice dev) { - Number identifierValue; + private static Short deviceVendorIdentifier(UdevDevice dev) { + Short identifierValue; try { String indentifierText = deviceProperty(dev, "ID_VENDOR_ID"); - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; + identifierValue = (indentifierText != null) ? Short.parseShort(indentifierText, 16) : null; } catch (Exception e) { identifierValue = null; } diff --git a/src/main/java/tic/io/serialport/SerialPortFinderForMacOsX.java b/src/main/java/tic/io/serialport/SerialPortFinderForMacOsX.java new file mode 100644 index 0000000..eb1a621 --- /dev/null +++ b/src/main/java/tic/io/serialport/SerialPortFinderForMacOsX.java @@ -0,0 +1,370 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.serialport; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; +import com.sun.jna.platform.mac.CoreFoundation; +import com.sun.jna.platform.mac.CoreFoundation.CFAllocatorRef; +import com.sun.jna.platform.mac.CoreFoundation.CFIndex; +import com.sun.jna.platform.mac.CoreFoundation.CFMutableDictionaryRef; +import com.sun.jna.platform.mac.CoreFoundation.CFNumberRef; +import com.sun.jna.platform.mac.CoreFoundation.CFStringRef; +import com.sun.jna.platform.mac.CoreFoundation.CFTypeID; +import com.sun.jna.platform.mac.CoreFoundation.CFTypeRef; +import com.sun.jna.platform.mac.IOKit; +import com.sun.jna.platform.mac.IOKit.IOIterator; +import com.sun.jna.platform.mac.IOKit.IORegistryEntry; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.ptr.PointerByReference; +import com.sun.jna.ptr.ShortByReference; +import java.util.ArrayList; +import java.util.List; + +/** Class used to find all serial port descriptor for MAC OS X */ +public class SerialPortFinderForMacOsX implements SerialPortFinder { + + private static final String kIOSerialBSDServiceValue = "IOSerialBSDClient"; + private static final String kIOSerialBSDTypeKey = "IOSerialBSDClientType"; + private static final String kIOSerialBSDAllTypes = "IOSerialStream"; + private static final String kIOCalloutDeviceKey = "IOCalloutDevice"; + private static final String kIOServicePlane = "IOService"; + private static final String kIODialinDeviceKey = "IODialinDevice"; + private static final String kIOPropertyProductNameKey = "Product Name"; + private static final String kUSBProductString = "USB Product Name"; + private static final String kUSBVendorString = "USB Vendor Name"; + private static final String kUSBSerialNumberString = "USB Serial Number"; + private static final String kUSBVendorID = "idVendor"; + private static final String kUSBProductID = "idProduct"; + private static final CFAllocatorRef kCFAllocatorDefault = + CoreFoundation.INSTANCE.CFAllocatorGetDefault(); + // Encodages CFString + private static final int kCFStringEncodingUTF8 = 0x08000100; + private static final int kCFNumberSInt16Type = 2; + private static final int kCFNumberSInt32Type = 3; + + /** + * Get instance + * + * @return Unique instance + */ + public static SerialPortFinderForMacOsX getInstance() { + if (instance == null) { + instance = new SerialPortFinderForMacOsX(); + } + + return instance; + } + + private static SerialPortFinderForMacOsX instance; + + private SerialPortFinderForMacOsX() {} + + @Override + public List findAll() { + List serialPortDescriptorList = new ArrayList(); + + CFMutableDictionaryRef serialPortDictionary = + IOKit.INSTANCE.IOServiceMatching(kIOSerialBSDServiceValue); + if (serialPortDictionary == null) { + return serialPortDescriptorList; + } + + CFStringRef key = CFStringRef.createCFString(kIOSerialBSDTypeKey); + CFStringRef value = CFStringRef.createCFString(kIOSerialBSDAllTypes); + + serialPortDictionary.setValue(key, value); + + IntByReference kIOMasterPortDefault = new IntByReference(); + int errorCode = IOKit.INSTANCE.IOMasterPort(0, kIOMasterPortDefault); + if (errorCode != 0) { + return serialPortDescriptorList; + } + PointerByReference serialPortIteratorRef = new PointerByReference(); + errorCode = + IOKit.INSTANCE.IOServiceGetMatchingServices( + kIOMasterPortDefault.getValue(), serialPortDictionary, serialPortIteratorRef); + if (errorCode != 0) { + return serialPortDescriptorList; + } + IOIterator serialPortIterator = new IOIterator(serialPortIteratorRef.getValue()); + + for (; ; ) { + IORegistryEntry serialPortService = serialPortIterator.next(); + if (serialPortService == null) { + break; + } + String calloutDevice = null; + String dialinDevice = null; + Integer locationId = null; + String deviceDescription = null; + String manufacturer = null; + String serialNumber = null; + Short vendorIdentifier = null; + Short productIdentifier = null; + + for (; ; ) { + if (calloutDevice == null) { + calloutDevice = calloutDeviceSystemLocation(serialPortService); + } + if (dialinDevice == null) { + dialinDevice = dialinDeviceSystemLocation(serialPortService); + } + if (locationId == null) { + locationId = deviceLocationId(serialPortService); + } + if (deviceDescription == null) { + deviceDescription = deviceDescription(serialPortService); + } + if (manufacturer == null) { + manufacturer = deviceManufacturer(serialPortService); + } + if (serialNumber == null) { + serialNumber = deviceSerialNumber(serialPortService); + } + if (vendorIdentifier == null) { + vendorIdentifier = deviceVendorIdentifier(serialPortService); + } + if (productIdentifier == null) { + productIdentifier = deviceProductIdentifier(serialPortService); + } + if (isCompleteInfo( + calloutDevice, + dialinDevice, + manufacturer, + deviceDescription, + serialNumber, + vendorIdentifier, + productIdentifier)) { + serialPortService.release(); + break; + } + serialPortService = serialPortService.getParentEntry(kIOServicePlane); + if (serialPortService == null) { + break; + } + } + String portId = (locationId != null) ? locationId.toString() : null; + SerialPortDescriptor calloutCandidate = + new SerialPortDescriptor.Builder<>() + .portId(portId) + .portName(calloutDevice) + .description(deviceDescription) + .productId(productIdentifier) + .vendorId(vendorIdentifier) + .productName(deviceDescription) + .manufacturer(manufacturer) + .serialNumber(serialNumber) + .build(); + serialPortDescriptorList.add(calloutCandidate); + + SerialPortDescriptor dialinCandidate = + new SerialPortDescriptor.Builder<>() + .portId(portId) + .portName(dialinDevice) + .description(deviceDescription) + .productId(productIdentifier) + .vendorId(vendorIdentifier) + .productName(deviceDescription) + .manufacturer(manufacturer) + .serialNumber(serialNumber) + .build(); + serialPortDescriptorList.add(dialinCandidate); + } + serialPortIterator.release(); + + return serialPortDescriptorList; + } + + private static boolean isCompleteInfo( + String calloutDevice, + String dialinDevice, + String manufacturer, + String description, + String serialNumber, + Short vendorIdentifier, + Short productIdentifier) { + return calloutDevice != null + && dialinDevice != null + && manufacturer != null + && description != null + && serialNumber != null + && vendorIdentifier != null + && vendorIdentifier != null; + } + + private static String calloutDeviceSystemLocation(IORegistryEntry ioRegistryEntry) { + return searchStringProperty(ioRegistryEntry, kIOCalloutDeviceKey); + } + + private static String dialinDeviceSystemLocation(IORegistryEntry ioRegistryEntry) { + return searchStringProperty(ioRegistryEntry, kIODialinDeviceKey); + } + + private static Integer deviceLocationId(IORegistryEntry ioRegistryEntry) { + return searchIntProperty(ioRegistryEntry, "locationID"); + } + + private static String deviceDescription(IORegistryEntry ioRegistryEntry) { + String result = searchStringProperty(ioRegistryEntry, kIOPropertyProductNameKey); + if (result == null || result.isEmpty()) { + result = searchStringProperty(ioRegistryEntry, kUSBProductString); + } + if (result == null || result.isEmpty()) { + result = searchStringProperty(ioRegistryEntry, "BTName"); + } + return result; + } + + private static String deviceManufacturer(IORegistryEntry ioRegistryEntry) { + return searchStringProperty(ioRegistryEntry, kUSBVendorString); + } + + private static String deviceSerialNumber(IORegistryEntry ioRegistryEntry) { + return searchStringProperty(ioRegistryEntry, kUSBSerialNumberString); + } + + private static Short deviceVendorIdentifier(IORegistryEntry ioRegistryEntry) { + return searchShortIntProperty(ioRegistryEntry, kUSBVendorID); + } + + private static Short deviceProductIdentifier(IORegistryEntry ioRegistryEntry) { + return searchShortIntProperty(ioRegistryEntry, kUSBProductID); + } + + private static String searchStringProperty(IORegistryEntry ioRegistryEntry, String propertyKey) { + CoreFoundation.CFTypeRef container = searchProperty(ioRegistryEntry, propertyKey); + String result = convertCFTypeRefToString(container); + if (container != null) { + container.release(); + } + + return result; + } + + private static Short searchShortIntProperty(IORegistryEntry ioRegistryEntry, String propertyKey) { + CoreFoundation.CFTypeRef container = searchProperty(ioRegistryEntry, propertyKey); + Short result = convertCFTypeRefToShort(container); + if (container != null) { + container.release(); + } + + return result; + } + + private static Integer searchIntProperty(IORegistryEntry ioRegistryEntry, String propertyKey) { + CoreFoundation.CFTypeRef container = searchProperty(ioRegistryEntry, propertyKey); + Integer result = convertCFTypeRefToInteger(container); + if (container != null) { + container.release(); + } + + return result; + } + + private static CoreFoundation.CFTypeRef searchProperty( + IORegistryEntry ioRegistryEntry, String propertyKey) { + + return IOKit.INSTANCE.IORegistryEntrySearchCFProperty( + ioRegistryEntry, + kIOServicePlane, + CFStringRef.createCFString(propertyKey), + kCFAllocatorDefault, + 0); + } + + private static String convertCFTypeRefToString(CFTypeRef cfTypeRef) { + if (cfTypeRef == null) { + return null; + } + + CFTypeID typeID = CoreFoundation.INSTANCE.CFGetTypeID(cfTypeRef); + CFTypeID stringTypeID = CoreFoundation.INSTANCE.CFStringGetTypeID(); + + if (!typeID.equals(stringTypeID)) { + return null; + } + + CFStringRef cfStringRef = new CFStringRef(cfTypeRef.getPointer()); + + CFIndex length = CoreFoundation.INSTANCE.CFStringGetLength(cfStringRef); + if (length.longValue() == 0) { + return ""; + } + + CFIndex maxSize = + CoreFoundation.INSTANCE.CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8); + + int bufferSize = (int) maxSize.longValue() + 1; + Pointer buffer = new Memory(bufferSize); + + CFIndex bufferLength = new CFIndex(bufferSize); + + byte success = + CoreFoundation.INSTANCE.CFStringGetCString( + cfStringRef, buffer, bufferLength, kCFStringEncodingUTF8); + + if (success == 0) { + return null; + } + + return buffer.getString(0, "UTF-8"); + } + + private static Integer convertCFTypeRefToInteger(CFTypeRef cfTypeRef) { + if (cfTypeRef == null) { + return null; + } + + CFTypeID typeID = CoreFoundation.INSTANCE.CFGetTypeID(cfTypeRef); + CFTypeID numberTypeID = CoreFoundation.INSTANCE.CFNumberGetTypeID(); + + if (!typeID.equals(numberTypeID)) { + return null; + } + + CFNumberRef cfNumberRef = new CFNumberRef(cfTypeRef.getPointer()); + + CFIndex numberType = new CFIndex(kCFNumberSInt32Type); + + IntByReference value = new IntByReference(); + byte success = CoreFoundation.INSTANCE.CFNumberGetValue(cfNumberRef, numberType, value); + + if (success == 0) { + return null; + } + + return value.getValue(); + } + + private static Short convertCFTypeRefToShort(CFTypeRef cfTypeRef) { + if (cfTypeRef == null) { + return null; + } + + CFTypeID typeID = CoreFoundation.INSTANCE.CFGetTypeID(cfTypeRef); + CFTypeID numberTypeID = CoreFoundation.INSTANCE.CFNumberGetTypeID(); + + if (!typeID.equals(numberTypeID)) { + return null; + } + + CFNumberRef cfNumberRef = new CFNumberRef(cfTypeRef.getPointer()); + + CFIndex numberType = new CFIndex(kCFNumberSInt16Type); + + ShortByReference value = new ShortByReference(); + byte success = CoreFoundation.INSTANCE.CFNumberGetValue(cfNumberRef, numberType, value); + + if (success == 0) { + return null; + } + + return value.getValue(); + } +} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java b/src/main/java/tic/io/serialport/SerialPortFinderForWindows.java similarity index 88% rename from src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java rename to src/main/java/tic/io/serialport/SerialPortFinderForWindows.java index 5cbdfe0..a963307 100644 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java +++ b/src/main/java/tic/io/serialport/SerialPortFinderForWindows.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.io.serialport; +package tic.io.serialport; import static com.sun.jna.platform.win32.SetupApi.DICS_FLAG_GLOBAL; import static com.sun.jna.platform.win32.SetupApi.DIGCF_DEVICEINTERFACE; @@ -35,9 +35,6 @@ import com.sun.jna.platform.win32.WinNT.HANDLE; import com.sun.jna.platform.win32.WinReg.HKEY; import com.sun.jna.ptr.IntByReference; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; import java.util.ArrayList; import java.util.List; @@ -115,7 +112,7 @@ private SerialPortFinderForWindows() {} * @return a list of all serial port descriptors found on the system */ @Override - public DataList findAll() { + public List findAll() { final class SetupToken { public GUID guid; public int flags; @@ -135,8 +132,7 @@ public SetupToken(GUID guid, int flags) { new SetupToken(GUID_DEVINTERFACE_MODEM, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE) }; - DataList serialPortDescriptorList = - new DataArrayList(); + List serialPortDescriptorList = new ArrayList<>(); for (int i = 0; i < setupTokens.length; ++i) { HANDLE deviceInfoSet = @@ -162,20 +158,21 @@ public SetupToken(GUID guid, int flags) { String manufacturer = deviceManufacturer(deviceInfoSet, deviceInfoData); String instanceIdentifier = deviceInstanceIdentifier(deviceInfoData.DevInst); String serialNumber = deviceSerialNumber(instanceIdentifier, deviceInfoData.DevInst); - Number vendorIdentifier = deviceVendorIdentifier(instanceIdentifier); - Number productIdentifier = deviceProductIdentifier(instanceIdentifier); + Short vendorIdentifier = deviceVendorIdentifier(instanceIdentifier); + Short productIdentifier = deviceProductIdentifier(instanceIdentifier); try { serialPortDescriptor = - new SerialPortDescriptor( - address, - portName, - description, - productIdentifier, - vendorIdentifier, - null, - manufacturer, - serialNumber); - } catch (DataDictionaryException e) { + new SerialPortDescriptor.Builder<>() + .portId(address) + .portName(portName) + .description(description) + .productId(productIdentifier) + .vendorId(vendorIdentifier) + .productName(null) + .manufacturer(manufacturer) + .serialNumber(serialNumber) + .build(); + } catch (IllegalArgumentException e) { e.printStackTrace(System.err); continue; } @@ -189,9 +186,18 @@ public SetupToken(GUID guid, int flags) { if (!anyOfPorts(serialPortDescriptorList, portName)) { try { SerialPortDescriptor serialPortDescriptor = - new SerialPortDescriptor(null, portName, null, null, null, null, null, null); + new SerialPortDescriptor.Builder<>() + .portId(null) + .portName(portName) + .description(null) + .productId(null) + .vendorId(null) + .productName(null) + .manufacturer(null) + .serialNumber(null) + .build(); serialPortDescriptorList.add(serialPortDescriptor); - } catch (DataDictionaryException e) { + } catch (IllegalArgumentException e) { e.printStackTrace(System.err); } } @@ -207,19 +213,19 @@ public SetupToken(GUID guid, int flags) { * @param portName the port name to search for * @return true if the port name is found in the list, false otherwise */ - private static boolean anyOfPorts(DataList descriptors, String portName) { + private static boolean anyOfPorts(List descriptors, String portName) { if (descriptors == null) { return false; } for (SerialPortDescriptor descriptor : descriptors) { - if (descriptor.getPortName() == null) { + if (descriptor.portName() == null) { if (portName != null) { continue; } else { return true; } } - if (descriptor.getPortName().equals(portName)) { + if (descriptor.portName().equals(portName)) { return true; } } @@ -265,10 +271,10 @@ private static String deviceAddress(HANDLE deviceInfoSet, SP_DEVINFO_DATA device return (address != null) ? address.toString() : null; } - private static Number deviceVendorIdentifier(String instanceIdentifier) { + private static Short deviceVendorIdentifier(String instanceIdentifier) { final int vendorIdentifierSize = 4; - Number result = parseDeviceIdentifier(instanceIdentifier, "VID_", vendorIdentifierSize); + Short result = parseDeviceIdentifier(instanceIdentifier, "VID_", vendorIdentifierSize); if (result == null) { result = parseDeviceIdentifier(instanceIdentifier, "VEN_", vendorIdentifierSize); } @@ -276,10 +282,10 @@ private static Number deviceVendorIdentifier(String instanceIdentifier) { return result; } - private static Number deviceProductIdentifier(String instanceIdentifier) { + private static Short deviceProductIdentifier(String instanceIdentifier) { final int productIdentifierSize = 4; - Number result = parseDeviceIdentifier(instanceIdentifier, "PID_", productIdentifierSize); + Short result = parseDeviceIdentifier(instanceIdentifier, "PID_", productIdentifierSize); if (result == null) { result = parseDeviceIdentifier(instanceIdentifier, "DEV_", productIdentifierSize); } @@ -365,7 +371,7 @@ private static String parseDeviceSerialNumber(String instanceIdentifier) { return instanceIdentifier.substring(firstbound + 1, lastbound); } - private static Number parseDeviceIdentifier( + private static Short parseDeviceIdentifier( String instanceIdentifier, String identifierPrefix, int identifierSize) { int index = instanceIdentifier.indexOf(identifierPrefix); @@ -375,9 +381,9 @@ private static Number parseDeviceIdentifier( String indentifierText = instanceIdentifier.substring( index + identifierPrefix.length(), index + identifierPrefix.length() + identifierSize); - Number identifierValue; + Short identifierValue; try { - identifierValue = Integer.parseInt(indentifierText, 16); + identifierValue = Short.parseShort(indentifierText, 16); } catch (Exception e) { identifierValue = null; } diff --git a/src/main/java/tic/io/serialport/SerialPortJsonEncoder.java b/src/main/java/tic/io/serialport/SerialPortJsonEncoder.java new file mode 100644 index 0000000..e9d5f5e --- /dev/null +++ b/src/main/java/tic/io/serialport/SerialPortJsonEncoder.java @@ -0,0 +1,61 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.serialport; + +import java.util.Collections; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONObject; + +/** Utility class to convert serial port descriptors to their JSON representation. */ +public final class SerialPortJsonEncoder { + private static final int DEFAULT_INDENT = 2; + + private SerialPortJsonEncoder() {} + + /** + * Encodes the provided serial port descriptors list into a JSON string using the default + * indentation factor (2 spaces). + * + * @param descriptors descriptors to encode; {@code null} is treated as an empty list + * @return the JSON string + */ + public static String encode(List descriptors) { + return encode(descriptors, DEFAULT_INDENT); + } + + /** + * Encodes the provided serial port descriptors list into a JSON string. + * + * @param descriptors descriptors to encode; {@code null} is treated as an empty list + * @param indentFactor indentation factor forwarded to {@link JSONArray#toString(int)}. If the + * value is negative the compact form is returned + * @return the JSON string + */ + public static String encode(List descriptors, int indentFactor) { + List safeDescriptors = + descriptors == null ? Collections.emptyList() : descriptors; + + JSONArray array = new JSONArray(); + safeDescriptors.forEach( + descriptor -> { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("portId", descriptor.portId()); + jsonObject.put("portName", descriptor.portName()); + jsonObject.put("description", descriptor.description()); + jsonObject.put("productId", descriptor.productId()); + jsonObject.put("vendorId", descriptor.vendorId()); + jsonObject.put("productName", descriptor.productName()); + jsonObject.put("manufacturer", descriptor.manufacturer()); + jsonObject.put("serialNumber", descriptor.serialNumber()); + array.put(jsonObject); + }); + + return indentFactor < 0 ? array.toString() : array.toString(indentFactor); + } +} diff --git a/src/main/java/tic/io/usb/UsbPortDescriptor.java b/src/main/java/tic/io/usb/UsbPortDescriptor.java new file mode 100644 index 0000000..ee0681a --- /dev/null +++ b/src/main/java/tic/io/usb/UsbPortDescriptor.java @@ -0,0 +1,541 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.usb; + +import java.util.Objects; +import tic.util.ValueChecker; + +/** + * A structure representing the standard USB device descriptor with additional string descriptors + * (manufacturer, product, serial number) if provided. This USB device descriptor is documented in + * section 9.6.1 of the USB 3.0 specification. All multiple-byte fields are represented in + * host-endian format. + */ +public class UsbPortDescriptor { + + private short bcdDevice; + private short bcdUSB; + private byte bDescriptorType; + private byte bDeviceClass; + private byte bDeviceProtocol; + private byte bDeviceSubClass; + private byte bLength; + private byte bMaxPacketSize0; + private byte bNumConfigurations; + private short idProduct; + private short idVendor; + private byte iManufacturer; + private byte iProduct; + private byte iSerialNumber; + private String manufacturer; + private String product; + private String serialNumber; + + /** Builder class for constructing UsbPortDescriptor instances. */ + public static class Builder { + private short bcdDevice; + private short bcdUSB; + private byte bDescriptorType; + private byte bDeviceClass; + private byte bDeviceProtocol; + private byte bDeviceSubClass; + private byte bLength; + private byte bMaxPacketSize0; + private byte bNumConfigurations; + private short idProduct; + private short idVendor; + private byte iManufacturer; + private byte iProduct; + private byte iSerialNumber; + private String manufacturer; + private String product; + private String serialNumber; + + /** + * Sets the bcdDevice field. + * + * @param bcdDevice the binary-coded decimal device release number + * @return the Builder instance + */ + public Builder bcdDevice(short bcdDevice) { + this.bcdDevice = bcdDevice; + return this; + } + + /** + * Sets the bcdUSB field. + * + * @param bcdUSB the binary-coded decimal USB specification release number + * @return the Builder instance + */ + public Builder bcdUSB(short bcdUSB) { + this.bcdUSB = bcdUSB; + return this; + } + + /** + * Sets the bDescriptorType field. + * + * @param bDescriptorType the USB descriptor type + * @return the Builder instance + */ + public Builder bDescriptorType(byte bDescriptorType) { + this.bDescriptorType = bDescriptorType; + return this; + } + + /** + * Sets the bDeviceClass field. + * + * @param bDeviceClass the USB device class + * @return the Builder instance + */ + public Builder bDeviceClass(byte bDeviceClass) { + this.bDeviceClass = bDeviceClass; + return this; + } + + /** + * Sets the bDeviceProtocol field. + * + * @param bDeviceProtocol the USB device protocol + * @return the Builder instance + */ + public Builder bDeviceProtocol(byte bDeviceProtocol) { + this.bDeviceProtocol = bDeviceProtocol; + return this; + } + + /** + * Sets the bDeviceSubClass field. + * + * @param bDeviceSubClass the USB device subclass + * @return the Builder instance + */ + public Builder bDeviceSubClass(byte bDeviceSubClass) { + this.bDeviceSubClass = bDeviceSubClass; + return this; + } + + /** + * Sets the bLength field. + * + * @param bLength the length of the USB device descriptor + * @return the Builder instance + */ + public Builder bLength(byte bLength) { + this.bLength = bLength; + return this; + } + + /** + * Sets the bMaxPacketSize0 field. + * + * @param bMaxPacketSize0 the maximum packet size for endpoint 0 + * @return the Builder instance + */ + public Builder bMaxPacketSize0(byte bMaxPacketSize0) { + this.bMaxPacketSize0 = bMaxPacketSize0; + return this; + } + + /** + * Sets the bNumConfigurations field. + * + * @param bNumConfigurations the number of possible configurations + * @return the Builder instance + */ + public Builder bNumConfigurations(byte bNumConfigurations) { + this.bNumConfigurations = bNumConfigurations; + return this; + } + + /** + * Sets the idProduct field. + * + * @param idProduct the USB product ID + * @return the Builder instance + */ + public Builder idProduct(short idProduct) { + this.idProduct = idProduct; + return this; + } + + /** + * Sets the idVendor field. + * + * @param idVendor the USB vendor ID + * @return the Builder instance + */ + public Builder idVendor(short idVendor) { + this.idVendor = idVendor; + return this; + } + + /** + * Sets the iManufacturer field. + * + * @param iManufacturer the index of the manufacturer string descriptor + * @return the Builder instance + */ + public Builder iManufacturer(byte iManufacturer) { + this.iManufacturer = iManufacturer; + return this; + } + + /** + * Sets the iProduct field. + * + * @param iProduct the index of the product string descriptor + * @return the Builder instance + */ + public Builder iProduct(byte iProduct) { + this.iProduct = iProduct; + return this; + } + + /** + * Sets the iSerialNumber field. + * + * @param iSerialNumber the index of the serial number string descriptor + * @return the Builder instance + */ + public Builder iSerialNumber(byte iSerialNumber) { + this.iSerialNumber = iSerialNumber; + return this; + } + + /** + * Sets the manufacturer field. + * + * @param manufacturer the manufacturer string + * @return the Builder instance + */ + public Builder manufacturer(String manufacturer) { + this.manufacturer = manufacturer; + return this; + } + + /** + * Sets the product field. + * + * @param product the product string + * @return the Builder instance + */ + public Builder product(String product) { + this.product = product; + return this; + } + + /** + * Sets the serialNumber field. + * + * @param serialNumber the serial number string + * @return the Builder instance + */ + public Builder serialNumber(String serialNumber) { + this.serialNumber = serialNumber; + return this; + } + + /** + * Builds the USBPortDescriptor instance. + * + * @return the constructed USBPortDescriptor instance + * @throws IllegalArgumentException if any required field is invalid + */ + public UsbPortDescriptor build() { + ValueChecker.validateString(this.manufacturer, "manufacturer", true, false); + ValueChecker.validateString(this.product, "product", true, false); + ValueChecker.validateString(this.serialNumber, "serialNumber", true, false); + + return new UsbPortDescriptor( + this.bcdDevice, + this.bcdUSB, + this.bDescriptorType, + this.bDeviceClass, + this.bDeviceProtocol, + this.bDeviceSubClass, + this.bLength, + this.bMaxPacketSize0, + this.bNumConfigurations, + this.idProduct, + this.idVendor, + this.iManufacturer, + this.iProduct, + this.iSerialNumber, + this.manufacturer, + this.product, + this.serialNumber); + } + } + + /** + * Constructor setting parameters to specific values + * + * @param bcdDevice the USB device release number in binary-coded decimal + * @param bcdUSB the USB specification release number in binary-coded decimal + * @param bDescriptorType the type of descriptor + * @param bDeviceClass the class of device + * @param bDeviceProtocol the protocol of device + * @param bDeviceSubClass the subclass of device + * @param bLength the length of the descriptor + * @param bMaxPacketSize0 the maximum packet size for endpoint 0 + * @param bNumConfigurations the number of possible configurations + * @param idProduct the product ID + * @param idVendor the vendor ID + * @param iManufacturer the index of the manufacturer string descriptor + * @param iProduct the index of the product string descriptor + * @param iSerialNumber the index of the serial number string descriptor + * @param manufacturer the manufacturer + * @param product the product + * @param serialNumber the serial number + */ + private UsbPortDescriptor( + short bcdDevice, + short bcdUSB, + byte bDescriptorType, + byte bDeviceClass, + byte bDeviceProtocol, + byte bDeviceSubClass, + byte bLength, + byte bMaxPacketSize0, + byte bNumConfigurations, + short idProduct, + short idVendor, + byte iManufacturer, + byte iProduct, + byte iSerialNumber, + String manufacturer, + String product, + String serialNumber) { + this.bcdDevice = bcdDevice; + this.bcdUSB = bcdUSB; + this.bDescriptorType = bDescriptorType; + this.bDeviceClass = bDeviceClass; + this.bDeviceProtocol = bDeviceProtocol; + this.bDeviceSubClass = bDeviceSubClass; + this.bLength = bLength; + this.bMaxPacketSize0 = bMaxPacketSize0; + this.bNumConfigurations = bNumConfigurations; + this.idProduct = idProduct; + this.idVendor = idVendor; + this.iManufacturer = iManufacturer; + this.iProduct = iProduct; + this.iSerialNumber = iSerialNumber; + this.manufacturer = manufacturer; + this.product = product; + this.serialNumber = serialNumber; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof UsbPortDescriptor)) { + return false; + } + UsbPortDescriptor other = (UsbPortDescriptor) obj; + return this.bcdDevice == other.bcdDevice + && this.bcdUSB == other.bcdUSB + && this.bDescriptorType == other.bDescriptorType + && this.bDeviceClass == other.bDeviceClass + && this.bDeviceProtocol == other.bDeviceProtocol + && this.bDeviceSubClass == other.bDeviceSubClass + && this.bLength == other.bLength + && this.bMaxPacketSize0 == other.bMaxPacketSize0 + && this.bNumConfigurations == other.bNumConfigurations + && this.idProduct == other.idProduct + && this.idVendor == other.idVendor + && this.iManufacturer == other.iManufacturer + && this.iProduct == other.iProduct + && this.iSerialNumber == other.iSerialNumber + && Objects.equals(this.manufacturer, other.manufacturer) + && Objects.equals(this.product, other.product) + && Objects.equals(this.serialNumber, other.serialNumber); + } + + @Override + public int hashCode() { + return Objects.hash( + this.bcdDevice, + this.bcdUSB, + this.bDescriptorType, + this.bDeviceClass, + this.bDeviceProtocol, + this.bDeviceSubClass, + this.bLength, + this.bMaxPacketSize0, + this.bNumConfigurations, + this.idProduct, + this.idVendor, + this.iManufacturer, + this.iProduct, + this.iSerialNumber, + this.manufacturer, + this.product, + this.serialNumber); + } + + /** + * Returns the USB device release number in binary-coded decimal. + * + * @return The USB device release number. + */ + public short bcdDevice() { + return this.bcdDevice; + } + + /** + * Returns the USB specification release number in binary-coded decimal. A value of 0x0200 + * indicates USB 2.0, 0x0110 indicates USB 1.1, etc. + * + * @return The USB specification release number. + */ + public short bcdUSB() { + return this.bcdUSB; + } + + /** + * Returns the USB descriptor type. + * + * @return The USB descriptor type. + */ + public byte bDescriptorType() { + return this.bDescriptorType; + } + + /** + * Returns the USB-IF class code for the device. + * + * @return The USB-IF class code. + */ + public byte bDeviceClass() { + return this.bDeviceClass; + } + + /** + * Returns the USB-IF protocol code for the device, qualified by the bDeviceClass and + * bDeviceSubClass values + * + * @return The USB-IF protocol code. + */ + public byte bDeviceProtocol() { + return this.bDeviceProtocol; + } + + /** + * Returns the USB-IF subclass code for the device, qualified by the bDeviceClass value. + * + * @return The USB-IF subclass code. + */ + public byte bDeviceSubClass() { + return this.bDeviceSubClass; + } + + /** + * Returns the size of the USB device descriptor (in bytes) without + * manufacturer/product/serialNumber fields. + * + * @return The size of this descriptor (in bytes). + */ + public byte bLength() { + return this.bLength; + } + + /** + * Returns the maximum packet size for endpoint 0. + * + * @return The maximum packet site for endpoint 0. + */ + public byte bMaxPacketSize0() { + return this.bMaxPacketSize0; + } + + /** + * Returns the number of possible configurations. + * + * @return The number of possible configurations. + */ + public byte bNumConfigurations() { + return this.bNumConfigurations; + } + + /** + * Returns the USB-IF product ID. + * + * @return The product ID. + */ + public short idProduct() { + return this.idProduct; + } + + /** + * Returns the USB-IF vendor ID. + * + * @return The vendor ID. + */ + public short idVendor() { + return this.idVendor; + } + + /** + * Returns the index of the string descriptor describing manufacturer. + * + * @return The manufacturer string descriptor index. + */ + public byte iManufacturer() { + return this.iManufacturer; + } + + /** + * Returns the index of the string descriptor describing product. + * + * @return The product string descriptor index. + */ + public byte iProduct() { + return this.iProduct; + } + + /** + * Returns the index of the string descriptor containing device serial number. + * + * @return The serial number string descriptor index. + */ + public byte iSerialNumber() { + return this.iSerialNumber; + } + + /** + * Returns the USB device manufacturer. + * + * @return The USB device manufacturer if exists or null otherwise. + */ + public String manufacturer() { + return this.manufacturer; + } + + /** + * Returns the USB device product. + * + * @return The USB device product if exists or null otherwise. + */ + public String product() { + return this.product; + } + + /** + * Returns the USB device serial number. + * + * @return The USB device serial number if exists or null otherwise. + */ + public String serialNumber() { + return this.serialNumber; + } +} diff --git a/src/main/java/tic/io/usb/UsbPortFinder.java b/src/main/java/tic/io/usb/UsbPortFinder.java new file mode 100644 index 0000000..c2d10e6 --- /dev/null +++ b/src/main/java/tic/io/usb/UsbPortFinder.java @@ -0,0 +1,55 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.usb; + +import java.util.List; +import java.util.stream.Collectors; +import tic.io.PortFinder; + +/** Interface used to find all USB port descriptor */ +public interface UsbPortFinder extends PortFinder { + /** + * Find USB device with the given product id + * + * @param idProduct the USB product identifier + * @return the USB descriptor data list of all USB device connected with the given product id + */ + public default List findByProductId(short idProduct) { + return this.findAll().stream() + .filter(d -> d.idProduct() == idProduct) + .collect(Collectors.toList()); + } + + /** + * Find USB device with the given vendor id + * + * @param idVendor the USB vendor identifier + * @return the USB descriptor data list of all USB device connected with the given vendor id + */ + public default List findByVendorId(short idVendor) { + return this.findAll().stream() + .filter(d -> d.idVendor() == idVendor) + .collect(Collectors.toList()); + } + + /** + * Find USB device with the given product id and vendor id + * + * @param idProduct the USB product identifier + * @param idVendor the USB vendor identifier + * @return the USB descriptor data list of all USB device connected with the given product id and + * vendor id + */ + public default List findByProductIdAndVendorId( + short idProduct, short idVendor) { + return this.findAll().stream() + .filter(d -> d.idProduct() == idProduct) + .filter(d -> d.idVendor() == idVendor) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java b/src/main/java/tic/io/usb/UsbPortFinderBase.java similarity index 54% rename from src/main/java/enedis/lab/io/usb/USBPortFinderBase.java rename to src/main/java/tic/io/usb/UsbPortFinderBase.java index b534761..b1b6352 100644 --- a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java +++ b/src/main/java/tic/io/usb/UsbPortFinderBase.java @@ -5,11 +5,10 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.io.usb; +package tic.io.usb; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; +import java.util.ArrayList; +import java.util.List; import org.usb4java.Context; import org.usb4java.Device; import org.usb4java.DeviceDescriptor; @@ -18,38 +17,28 @@ import org.usb4java.LibUsb; /** Class used to find all USB port descriptor */ -public class USBPortFinderBase implements USBPortFinder { - /** - * Program writing the USB port descriptor list (JSON format) on the output stream - * - * @param args not used - */ - public static void main(String[] args) { - DataList descriptors = getInstance().findAll(); - - System.out.println(descriptors.toString(2)); - } +public class UsbPortFinderBase implements UsbPortFinder { /** * Get instance * * @return Unique instance */ - public static USBPortFinderBase getInstance() { + public static UsbPortFinder getInstance() { if (instance == null) { - instance = new USBPortFinderBase(); + instance = new UsbPortFinderBase(); } return instance; } - private static USBPortFinderBase instance; + private static UsbPortFinder instance; - private USBPortFinderBase() {} + private UsbPortFinderBase() {} @Override - public DataList findAll() { - DataList usbPortList = new DataArrayList(); + public List findAll() { + List usbPortList = new ArrayList<>(); Context context = new Context(); DeviceList deviceList = new DeviceList(); @@ -92,29 +81,28 @@ public DataList findAll() { } try { - // @formatter:off - USBPortDescriptor usbPort = - new USBPortDescriptor( - descriptor.bcdDevice() & 0xFFFF, - descriptor.bcdUSB() & 0xFFFF, - descriptor.bDescriptorType() & 0xFF, - descriptor.bDeviceClass() & 0xFF, - descriptor.bDeviceProtocol() & 0xFF, - descriptor.bDeviceSubClass() & 0xFF, - descriptor.bLength() & 0xFF, - descriptor.bMaxPacketSize0() & 0xFF, - descriptor.bNumConfigurations() & 0xFF, - descriptor.idProduct() & 0xFFFF, - descriptor.idVendor() & 0xFFFF, - descriptor.iManufacturer() & 0xFF, - descriptor.iProduct() & 0xFF, - descriptor.iSerialNumber() & 0xFF, - manufacturer, - productName, - serialNumber); - // @formatter:on + UsbPortDescriptor usbPort = + new UsbPortDescriptor.Builder() + .bcdDevice(descriptor.bcdDevice()) + .bcdUSB(descriptor.bcdUSB()) + .bDescriptorType(descriptor.bDescriptorType()) + .bDeviceClass(descriptor.bDeviceClass()) + .bDeviceProtocol(descriptor.bDeviceProtocol()) + .bDeviceSubClass(descriptor.bDeviceSubClass()) + .bLength(descriptor.bLength()) + .bMaxPacketSize0(descriptor.bMaxPacketSize0()) + .bNumConfigurations(descriptor.bNumConfigurations()) + .idProduct(descriptor.idProduct()) + .idVendor(descriptor.idVendor()) + .iManufacturer(descriptor.iManufacturer()) + .iProduct(descriptor.iProduct()) + .iSerialNumber(descriptor.iSerialNumber()) + .manufacturer(manufacturer) + .product(productName) + .serialNumber(serialNumber) + .build(); usbPortList.add(usbPort); - } catch (DataDictionaryException e) { + } catch (IllegalArgumentException e) { } } diff --git a/src/main/java/tic/io/usb/UsbPortJsonEncoder.java b/src/main/java/tic/io/usb/UsbPortJsonEncoder.java new file mode 100644 index 0000000..8a03ad6 --- /dev/null +++ b/src/main/java/tic/io/usb/UsbPortJsonEncoder.java @@ -0,0 +1,70 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.usb; + +import java.util.Collections; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONObject; + +/** Utility class to convert USB port descriptors to their JSON representation. */ +public final class UsbPortJsonEncoder { + private static final int DEFAULT_INDENT = 2; + + private UsbPortJsonEncoder() {} + + /** + * Encodes the provided USB descriptors list into a JSON string using the default indentation + * factor (2 spaces). + * + * @param descriptors descriptors to encode; {@code null} is treated as an empty list + * @return the JSON string + */ + public static String encode(List descriptors) { + return encode(descriptors, DEFAULT_INDENT); + } + + /** + * Encodes the provided USB descriptors list into a JSON string. + * + * @param descriptors descriptors to encode; {@code null} is treated as an empty list + * @param indentFactor indentation factor forwarded to {@link JSONArray#toString(int)}. If the + * value is negative the compact form is returned + * @return the JSON string + */ + public static String encode(List descriptors, int indentFactor) { + List safeDescriptors = + descriptors == null ? Collections.emptyList() : descriptors; + + JSONArray array = new JSONArray(); + safeDescriptors.forEach( + descriptor -> { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("bcdDevice", descriptor.bcdDevice()); + jsonObject.put("bcdUSB", descriptor.bcdUSB()); + jsonObject.put("bDescriptorType", descriptor.bDescriptorType()); + jsonObject.put("bDeviceClass", descriptor.bDeviceClass()); + jsonObject.put("bDeviceProtocol", descriptor.bDeviceProtocol()); + jsonObject.put("bDeviceSubClass", descriptor.bDeviceSubClass()); + jsonObject.put("bLength", descriptor.bLength()); + jsonObject.put("bMaxPacketSize0", descriptor.bMaxPacketSize0()); + jsonObject.put("bNumConfigurations", descriptor.bNumConfigurations()); + jsonObject.put("idProduct", descriptor.idProduct()); + jsonObject.put("idVendor", descriptor.idVendor()); + jsonObject.put("iManufacturer", descriptor.iManufacturer()); + jsonObject.put("iProduct", descriptor.iProduct()); + jsonObject.put("iSerialNumber", descriptor.iSerialNumber()); + jsonObject.put("manufacturer", descriptor.manufacturer()); + jsonObject.put("product", descriptor.product()); + jsonObject.put("serialNumber", descriptor.serialNumber()); + array.put(jsonObject); + }); + + return indentFactor < 0 ? array.toString() : array.toString(indentFactor); + } +} diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java b/src/main/java/tic/service/TIC2WebSocketApplication.java similarity index 94% rename from src/main/java/enedis/tic/service/TIC2WebSocketApplication.java rename to src/main/java/tic/service/TIC2WebSocketApplication.java index df0dfe8..2795fc6 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java +++ b/src/main/java/tic/service/TIC2WebSocketApplication.java @@ -5,17 +5,17 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service; - -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.tic.core.TICCore; -import enedis.tic.core.TICCoreBase; -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.client.TIC2WebSocketClientPoolBase; -import enedis.tic.service.config.TIC2WebSocketConfiguration; -import enedis.tic.service.netty.TIC2WebSocketServer; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandlerBase; +package tic.service; + +import tic.core.TICCore; +import tic.core.TICCoreBase; +import tic.service.client.TIC2WebSocketClientPool; +import tic.service.client.TIC2WebSocketClientPoolBase; +import tic.service.config.TIC2WebSocketConfiguration; +import tic.service.config.TIC2WebSocketConfigurationLoader; +import tic.service.netty.TIC2WebSocketServer; +import tic.service.requesthandler.TIC2WebSocketRequestHandler; +import tic.service.requesthandler.TIC2WebSocketRequestHandlerBase; import java.io.File; import java.io.InputStream; import java.util.Properties; @@ -213,7 +213,7 @@ public int init() { this.server = new TIC2WebSocketServer( "localhost", - this.configuration.getServerPort().intValue(), + this.configuration.getServerPort(), this.clientPool, this.requestHandler); @@ -360,9 +360,7 @@ private int loadConfiguration() { } try { this.logger.info("Loading configuration file " + configPath); - this.configuration = - (TIC2WebSocketConfiguration) - DataDictionaryBase.fromFile(configPath, TIC2WebSocketConfiguration.class); + this.configuration = TIC2WebSocketConfigurationLoader.load(configPath.getAbsolutePath()); } catch (Exception exception) { this.logger.error("Loading configuration file " + configPath + " failed", exception); return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java b/src/main/java/tic/service/TIC2WebSocketApplicationErrorCode.java similarity index 98% rename from src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java rename to src/main/java/tic/service/TIC2WebSocketApplicationErrorCode.java index 882f4c1..e933a52 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java +++ b/src/main/java/tic/service/TIC2WebSocketApplicationErrorCode.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service; +package tic.service; /** * Enumeration of application error codes for TIC2WebSocket. diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java b/src/main/java/tic/service/TIC2WebSocketCommandLine.java similarity index 99% rename from src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java rename to src/main/java/tic/service/TIC2WebSocketCommandLine.java index 160b4b2..90c6c90 100644 --- a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java +++ b/src/main/java/tic/service/TIC2WebSocketCommandLine.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service; +package tic.service; import org.apache.logging.log4j.Level; import picocli.CommandLine.Command; diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java b/src/main/java/tic/service/client/TIC2WebSocketClient.java similarity index 69% rename from src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java rename to src/main/java/tic/service/client/TIC2WebSocketClient.java index 6af100c..fde36b4 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java +++ b/src/main/java/tic/service/client/TIC2WebSocketClient.java @@ -5,20 +5,17 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.client; +package tic.service.client; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Event; -import enedis.tic.core.TICCoreError; -import enedis.tic.core.TICCoreFrame; -import enedis.tic.core.TICCoreSubscriber; -import enedis.tic.service.endpoint.EventSender; -import enedis.tic.service.message.EventOnError; -import enedis.tic.service.message.EventOnTICData; import io.netty.channel.Channel; import java.time.LocalDateTime; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import tic.core.TICCoreError; +import tic.core.TICCoreFrame; +import tic.core.TICCoreSubscriber; +import tic.service.endpoint.EventSender; +import tic.service.message.EventOnError; +import tic.service.message.EventOnTICData; +import tic.util.message.Event; /** * WebSocket client for TIC2WebSocket service. @@ -40,8 +37,6 @@ * @see Channel */ public class TIC2WebSocketClient implements TICCoreSubscriber { - /** Logger for client events and errors. */ - private final Logger logger; /** Associated Netty WebSocket channel for communication. */ private final Channel channel; @@ -59,7 +54,6 @@ public class TIC2WebSocketClient implements TICCoreSubscriber { */ public TIC2WebSocketClient(Channel channel, EventSender eventSender) { super(); - this.logger = LogManager.getLogger(this.getClass()); this.channel = channel; this.eventSender = eventSender; } @@ -74,12 +68,8 @@ public TIC2WebSocketClient(Channel channel, EventSender eventSender) { */ @Override public void onData(TICCoreFrame frame) { - try { - Event event = new EventOnTICData(LocalDateTime.now(), frame); - this.eventSender.sendEvent(this.channel, event); - } catch (DataDictionaryException e) { - this.logger.error(e.getMessage(), e); - } + Event event = new EventOnTICData(LocalDateTime.now(), frame); + this.eventSender.sendEvent(this.channel, event); } /** @@ -92,12 +82,8 @@ public void onData(TICCoreFrame frame) { */ @Override public void onError(TICCoreError error) { - try { - Event event = new EventOnError(LocalDateTime.now(), error); - this.eventSender.sendEvent(this.channel, event); - } catch (DataDictionaryException e) { - this.logger.error(e.getMessage(), e); - } + Event event = new EventOnError(LocalDateTime.now(), error); + this.eventSender.sendEvent(this.channel, event); } /** diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java b/src/main/java/tic/service/client/TIC2WebSocketClientPool.java similarity index 96% rename from src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java rename to src/main/java/tic/service/client/TIC2WebSocketClientPool.java index 6d681db..c81d8c3 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java +++ b/src/main/java/tic/service/client/TIC2WebSocketClientPool.java @@ -5,9 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.client; +package tic.service.client; -import enedis.tic.service.endpoint.EventSender; +import tic.service.endpoint.EventSender; import io.netty.channel.Channel; import java.util.Optional; diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java b/src/main/java/tic/service/client/TIC2WebSocketClientPoolBase.java similarity index 98% rename from src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java rename to src/main/java/tic/service/client/TIC2WebSocketClientPoolBase.java index ee30943..ca8e304 100644 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java +++ b/src/main/java/tic/service/client/TIC2WebSocketClientPoolBase.java @@ -5,9 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.client; +package tic.service.client; -import enedis.tic.service.endpoint.EventSender; +import tic.service.endpoint.EventSender; import io.netty.channel.Channel; import java.util.Optional; import java.util.Set; diff --git a/src/main/java/tic/service/config/TIC2WebSocketConfiguration.java b/src/main/java/tic/service/config/TIC2WebSocketConfiguration.java new file mode 100644 index 0000000..f05f262 --- /dev/null +++ b/src/main/java/tic/service/config/TIC2WebSocketConfiguration.java @@ -0,0 +1,141 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import tic.frame.TICMode; + +/** + * Configuration class for TIC2WebSocket service. + * + *

    This class manages configuration parameters for the TIC2WebSocket server, including server + * port, TIC mode, and the list of TIC port names. It provides validation and conversion logic for + * each parameter, ensuring correct types and values. + * + *

    Key features include: + * + *

      + *
    • Validation of server port range and TIC port names + *
    • Support for multiple construction methods (map, DataDictionary, explicit values) + *
    • Integration with the configuration base and key descriptor system + *
    + * + * @author Enedis Smarties team + * @see ConfigurationBase + * @see TICMode + */ +public class TIC2WebSocketConfiguration { + + public static final String KEY_SERVER_PORT = "serverPort"; + public static final String KEY_TIC_MODE = "ticMode"; + public static final String KEY_TIC_PORT_NAMES = "ticPortNames"; + + public static final int SERVER_PORT_MIN = 1; + public static final int SERVER_PORT_MAX = 65535; + + public static final TICMode DEFAULT_TIC_MODE = TICMode.AUTO; + + private int serverPort; + private TICMode ticMode; + private List ticPortNames; + + /** + * Constructs a configuration with a mandatory server port. + * + * @param serverPort the server port number + */ + public TIC2WebSocketConfiguration(int serverPort) { + this(serverPort, null, null); + } + + /** + * Constructs a configuration with a mandatory server port, and optional TIC mode and port names. + * + * @param serverPort the server port number + * @param ticMode the TIC mode (optional, defaults to {@link #DEFAULT_TIC_MODE} when null) + * @param ticPortNames list of TIC native port names (optional, null/empty means not set) + */ + public TIC2WebSocketConfiguration(int serverPort, TICMode ticMode, List ticPortNames) { + this.setServerPort(serverPort); + this.setTicMode(ticMode); + this.setTicPortNames(ticPortNames); + } + + public int getServerPort() { + return this.serverPort; + } + + public TICMode getTicMode() { + return this.ticMode; + } + + /** + * Returns the list of configured TIC port names. + * + * @return an unmodifiable list, or null if not set + */ + public List getTicPortNames() { + return this.ticPortNames; + } + + private void setServerPort(int serverPort) { + checkServerPort(serverPort); + this.serverPort = serverPort; + } + + private void setTicMode(TICMode ticMode) { + this.ticMode = (ticMode == null) ? DEFAULT_TIC_MODE : ticMode; + } + + private void setTicPortNames(List ticPortNames) { + this.ticPortNames = normalizeAndCheckPortNames(ticPortNames); + } + + private static void checkServerPort(int serverPort) { + if (serverPort < SERVER_PORT_MIN || serverPort > SERVER_PORT_MAX) { + throw new IllegalArgumentException( + "Server port must be between " + SERVER_PORT_MIN + " and " + SERVER_PORT_MAX); + } + } + + private static List normalizeAndCheckPortNames(List ticPortNames) { + if (ticPortNames == null || ticPortNames.isEmpty()) { + return null; + } + + List normalized = new ArrayList<>(ticPortNames.size()); + Set seen = new HashSet<>(); + + for (int i = 0; i < ticPortNames.size(); i++) { + String portName = ticPortNames.get(i); + if (portName == null) { + throw new IllegalArgumentException( + "Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + " cannot be null"); + } + + String trimmed = portName.trim(); + if (trimmed.isEmpty()) { + throw new IllegalArgumentException( + "Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + " cannot be empty"); + } + + if (!seen.add(trimmed)) { + throw new IllegalArgumentException( + "Key " + KEY_TIC_PORT_NAMES + ": duplicate value '" + trimmed + "'"); + } + + normalized.add(trimmed); + } + + return Collections.unmodifiableList(normalized); + } +} diff --git a/src/main/java/tic/service/config/TIC2WebSocketConfigurationCodec.java b/src/main/java/tic/service/config/TIC2WebSocketConfigurationCodec.java new file mode 100644 index 0000000..934c2e8 --- /dev/null +++ b/src/main/java/tic/service/config/TIC2WebSocketConfigurationCodec.java @@ -0,0 +1,123 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.config; + +import java.util.ArrayList; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; +import tic.frame.TICMode; +import tic.util.codec.JsonObjectCodec; +import tic.util.codec.JsonStringCodec; + +public class TIC2WebSocketConfigurationCodec + implements JsonStringCodec, + JsonObjectCodec { + + private static TIC2WebSocketConfigurationCodec instance = null; + + public static TIC2WebSocketConfigurationCodec getInstance() { + if (instance == null) { + instance = new TIC2WebSocketConfigurationCodec(); + } + return instance; + } + + private TIC2WebSocketConfigurationCodec() {} + + @Override + public String encodeToJsonString(TIC2WebSocketConfiguration object, int indentFactor) + throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'encodeToJsonString'"); + } + + @Override + public TIC2WebSocketConfiguration decodeFromJsonString(String jsonString, int indentFactor) + throws Exception { + JSONObject rootObject = new JSONObject(new JSONTokener(jsonString)); + return decodeFromJsonObject(rootObject); + } + + @Override + public Object encodeToJsonObject(TIC2WebSocketConfiguration object) throws Exception { + throw new UnsupportedOperationException("Unimplemented method 'encodeToJsonObject'"); + } + + @Override + public TIC2WebSocketConfiguration decodeFromJsonObject(Object jsonObject) throws Exception { + + JSONObject root = (JSONObject) jsonObject; + + int serverPort = parseServerPort(root); + TICMode ticMode = parseTicMode(root); + List ticPortNames = parseTicPortNames(root); + + return new TIC2WebSocketConfiguration(serverPort, ticMode, ticPortNames); + } + + private int parseServerPort(JSONObject root) { + if (!root.has(TIC2WebSocketConfiguration.KEY_SERVER_PORT)) { + throw new IllegalArgumentException( + "Key '" + TIC2WebSocketConfiguration.KEY_SERVER_PORT + "' is required"); + } + + int serverPort = root.getInt(TIC2WebSocketConfiguration.KEY_SERVER_PORT); + if (serverPort < TIC2WebSocketConfiguration.SERVER_PORT_MIN + || serverPort > TIC2WebSocketConfiguration.SERVER_PORT_MAX) { + throw new IllegalArgumentException( + "Key '" + + TIC2WebSocketConfiguration.KEY_SERVER_PORT + + "' must be between " + + TIC2WebSocketConfiguration.SERVER_PORT_MIN + + " and " + + TIC2WebSocketConfiguration.SERVER_PORT_MAX); + } + + return serverPort; + } + + private static TICMode parseTicMode(JSONObject root) { + String modeValue = + root.optString( + TIC2WebSocketConfiguration.KEY_TIC_MODE, + TIC2WebSocketConfiguration.DEFAULT_TIC_MODE.name()); + return TICMode.valueOf(modeValue.toUpperCase()); + } + + private static List parseTicPortNames(JSONObject root) { + JSONArray array = root.optJSONArray(TIC2WebSocketConfiguration.KEY_TIC_PORT_NAMES); + if (array == null || array.length() == 0) { + return null; + } + + List ticPortNames = new ArrayList<>(array.length()); + for (int i = 0; i < array.length(); i++) { + String portName = array.getString(i); + if (portName == null) { + throw new IllegalArgumentException( + "Key '" + + TIC2WebSocketConfiguration.KEY_TIC_PORT_NAMES + + "': value at index " + + i + + " cannot be null"); + } + if (portName.isEmpty()) { + throw new IllegalArgumentException( + "Key '" + + TIC2WebSocketConfiguration.KEY_TIC_PORT_NAMES + + "': value at index " + + i + + " cannot be empty"); + } + ticPortNames.add(portName); + } + + return ticPortNames; + } +} diff --git a/src/main/java/tic/service/config/TIC2WebSocketConfigurationLoader.java b/src/main/java/tic/service/config/TIC2WebSocketConfigurationLoader.java new file mode 100644 index 0000000..51f29c6 --- /dev/null +++ b/src/main/java/tic/service/config/TIC2WebSocketConfigurationLoader.java @@ -0,0 +1,39 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.config; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public final class TIC2WebSocketConfigurationLoader { + + private TIC2WebSocketConfigurationLoader() {} + + /** + * Load TIC2WebSocket application configuration from a JSON file. + * + * @param filepath the path to the configuration file + * @return the loaded configuration + * @throws IllegalStateException if there is an error loading or parsing the configuration + */ + public static TIC2WebSocketConfiguration load(String filepath) { + try { + String json = readFileAsString(filepath); + return TIC2WebSocketConfigurationCodec.getInstance().decodeFromJsonString(json); + } catch (Exception exception) { + throw new IllegalStateException("Unable to load TIC2WebSocket configuration", exception); + } + } + + private static String readFileAsString(String filepath) throws IOException { + Path path = Paths.get(filepath); + return new String(Files.readAllBytes(path)); + } +} diff --git a/src/main/java/enedis/tic/service/endpoint/EventSender.java b/src/main/java/tic/service/endpoint/EventSender.java similarity index 94% rename from src/main/java/enedis/tic/service/endpoint/EventSender.java rename to src/main/java/tic/service/endpoint/EventSender.java index 2fd05c8..3a8d079 100644 --- a/src/main/java/enedis/tic/service/endpoint/EventSender.java +++ b/src/main/java/tic/service/endpoint/EventSender.java @@ -5,9 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.endpoint; +package tic.service.endpoint; -import enedis.lab.util.message.Event; +import tic.util.message.Event; import io.netty.channel.Channel; /** diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java b/src/main/java/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java similarity index 98% rename from src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java rename to src/main/java/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java index 3933706..4fc9abe 100644 --- a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java +++ b/src/main/java/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.endpoint; +package tic.service.endpoint; /** * Error codes for TIC2WebSocket endpoint operations. diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java b/src/main/java/tic/service/endpoint/TIC2WebSocketEndPointException.java similarity index 98% rename from src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java rename to src/main/java/tic/service/endpoint/TIC2WebSocketEndPointException.java index 5a68bb6..3867512 100644 --- a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java +++ b/src/main/java/tic/service/endpoint/TIC2WebSocketEndPointException.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.endpoint; +package tic.service.endpoint; /** * Exception class for TIC2WebSocket endpoint errors. diff --git a/src/main/java/tic/service/message/EventOnError.java b/src/main/java/tic/service/message/EventOnError.java new file mode 100644 index 0000000..ed51c1f --- /dev/null +++ b/src/main/java/tic/service/message/EventOnError.java @@ -0,0 +1,68 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import java.time.LocalDateTime; +import tic.core.TICCoreError; +import tic.util.message.Event; + +/** + * Event message representing an error in the TIC2WebSocket protocol. + * + *

    This class encapsulates error information using a {@link TICCoreError} object and provides + * constructors for various initialization scenarios. It integrates with the event messaging system + * for error notification and handling. + * + *

    Key features include: + * + *

      + *
    • Encapsulates error data for protocol events + *
    • Supports construction from map, DataDictionary, or explicit values + *
    • Validates and manages error data using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Event + * @see TICCoreError + */ +public class EventOnError extends Event { + /** Message name for error events. */ + public static final String NAME = "OnError"; + + private TICCoreError data; + + /** + * Constructs an error event with explicit date/time and error data. + * + * @param dateTime the event timestamp + * @param data the error data + */ + public EventOnError(LocalDateTime dateTime, TICCoreError data) { + super(NAME, dateTime); + this.setData(data); + } + + /** + * Returns the error data associated with this event. + * + * @return the error data + */ + public TICCoreError getData() { + return this.data; + } + + /** + * Sets the error data for this event. + * + * @param data the error data + * @throws DataDictionaryException if validation fails + */ + public void setData(TICCoreError data) { + this.data = data; + } +} diff --git a/src/main/java/tic/service/message/EventOnTICData.java b/src/main/java/tic/service/message/EventOnTICData.java new file mode 100644 index 0000000..dc41f53 --- /dev/null +++ b/src/main/java/tic/service/message/EventOnTICData.java @@ -0,0 +1,68 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import java.time.LocalDateTime; +import tic.core.TICCoreFrame; +import tic.util.message.Event; + +/** + * Event message representing TIC data in the TIC2WebSocket protocol. + * + *

    This class encapsulates TIC data using a {@link TICCoreFrame} object and provides constructors + * for various initialization scenarios. It integrates with the event messaging system for data + * notification and handling. + * + *

    Key features include: + * + *

      + *
    • Encapsulates TIC data for protocol events + *
    • Supports construction from map, DataDictionary, or explicit values + *
    • Validates and manages TIC data using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Event + * @see TICCoreFrame + */ +public class EventOnTICData extends Event { + /** Message name for TIC data events. */ + public static final String NAME = "OnTICData"; + + private TICCoreFrame data; + + /** + * Constructs a TIC data event with explicit date/time and TIC data. + * + * @param dateTime the event timestamp + * @param data the TIC data + */ + public EventOnTICData(LocalDateTime dateTime, TICCoreFrame data) { + super(NAME, dateTime); + this.setData(data); + } + + /** + * Returns the TIC data associated with this event. + * + * @return the TIC data + */ + public TICCoreFrame getData() { + return this.data; + } + + /** + * Sets the TIC data for this event. + * + * @param data the TIC data + * @throws DataDictionaryException if validation fails + */ + public void setData(TICCoreFrame data) { + this.data = data; + } +} diff --git a/src/main/java/tic/service/message/RequestGetAvailableTICs.java b/src/main/java/tic/service/message/RequestGetAvailableTICs.java new file mode 100644 index 0000000..ab04e90 --- /dev/null +++ b/src/main/java/tic/service/message/RequestGetAvailableTICs.java @@ -0,0 +1,37 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import tic.util.message.Request; + +/** + * Request message for retrieving available TICs. + * + *

    This class represents a request to obtain the list of available TICs. It provides constructors + * for various initialization scenarios and integrates with the request messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates request for available TICs + *
    • Supports construction from map, DataDictionary, or default values + *
    • Validates and manages request parameters + *
    + * + * @author Enedis Smarties team + * @see Request + */ +public class RequestGetAvailableTICs extends Request { + /** Message name for this request. */ + public static final String NAME = "GetAvailableTICs"; + + /** Constructs a request for available TICs with default values. */ + public RequestGetAvailableTICs() { + super(NAME); + } +} diff --git a/src/main/java/tic/service/message/RequestGetModemsInfo.java b/src/main/java/tic/service/message/RequestGetModemsInfo.java new file mode 100644 index 0000000..252d318 --- /dev/null +++ b/src/main/java/tic/service/message/RequestGetModemsInfo.java @@ -0,0 +1,37 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import tic.util.message.Request; + +/** + * Request message for retrieving modem information. + * + *

    This class represents a request to obtain information about available modems. It provides + * constructors for various initialization scenarios and integrates with the request messaging + * system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates request for modem information + *
    • Validates and manages request parameters + *
    + * + * @author Enedis Smarties team + * @see Request + */ +public class RequestGetModemsInfo extends Request { + /** Message name for this request. */ + public static final String NAME = "GetModemsInfo"; + + /** Constructs a request for modem information with default values. */ + public RequestGetModemsInfo() { + super(NAME); + } +} diff --git a/src/main/java/tic/service/message/RequestReadTIC.java b/src/main/java/tic/service/message/RequestReadTIC.java new file mode 100644 index 0000000..07ea78e --- /dev/null +++ b/src/main/java/tic/service/message/RequestReadTIC.java @@ -0,0 +1,65 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import tic.core.TICIdentifier; +import tic.util.message.Request; + +/** + * Request message for reading TIC data in the TIC2WebSocket protocol. + * + *

    This class represents a request to read TIC data for a specific identifier. It provides + * constructors for various initialization scenarios and integrates with the request messaging + * system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates request for reading TIC data + *
    • Supports construction from map, DataDictionary, or explicit identifier + *
    • Validates and manages request parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Request + * @see TICIdentifier + */ +public class RequestReadTIC extends Request { + /** Message name for this request. */ + public static final String NAME = "ReadTIC"; + + private TICIdentifier data; + + /** + * Constructs a request for reading TIC data with a specific identifier. + * + * @param data the TIC identifier + */ + public RequestReadTIC(TICIdentifier data) { + super(NAME); + this.setData(data); + } + + /** + * Returns the TIC identifier data associated with this request. + * + * @return the TIC identifier + */ + public TICIdentifier getData() { + return this.data; + } + + /** + * Sets the TIC identifier data for this request. + * + * @param data the TIC identifier + */ + public void setData(TICIdentifier data) { + this.data = data; + } +} diff --git a/src/main/java/tic/service/message/RequestSubscribeTIC.java b/src/main/java/tic/service/message/RequestSubscribeTIC.java new file mode 100644 index 0000000..fe14db9 --- /dev/null +++ b/src/main/java/tic/service/message/RequestSubscribeTIC.java @@ -0,0 +1,66 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import tic.util.message.Request; +import tic.core.TICIdentifier; +import java.util.List; + +/** + * Request message for subscribing to TIC data. + * + *

    This class represents a request to subscribe to TIC data for one or more identifiers. It + * provides constructors for various initialization scenarios and integrates with the request + * messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates request for TIC data subscription + *
    • Supports construction from map, DataDictionary, or explicit identifier list + *
    • Validates and manages request parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Request + * @see TICIdentifier + */ +public class RequestSubscribeTIC extends Request { + /** Message name for this request. */ + public static final String NAME = "SubscribeTIC"; + + private List data; + + /** + * Constructs a request for subscribing to TIC data with a specific list of identifiers. + * + * @param data the list of TIC identifiers + */ + public RequestSubscribeTIC(List data) { + super(NAME); + this.setData(data); + } + + /** + * Returns the list of TIC identifier data associated with this request. + * + * @return the list of TIC identifiers + */ + public List getData() { + return this.data; + } + + /** + * Sets the list of TIC identifier data for this request. + * + * @param data the list of TIC identifiers + */ + public void setData(List data) { + this.data = data; + } +} diff --git a/src/main/java/tic/service/message/RequestUnsubscribeTIC.java b/src/main/java/tic/service/message/RequestUnsubscribeTIC.java new file mode 100644 index 0000000..947f54e --- /dev/null +++ b/src/main/java/tic/service/message/RequestUnsubscribeTIC.java @@ -0,0 +1,66 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import tic.util.message.Request; +import tic.core.TICIdentifier; +import java.util.List; + +/** + * Request message for unsubscribing from TIC data in the TIC2WebSocket protocol. + * + *

    This class represents a request to unsubscribe from TIC data for one or more identifiers. It + * provides constructors for various initialization scenarios and integrates with the request + * messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates request for TIC data unsubscription + *
    • Supports construction from map, DataDictionary, or explicit identifier list + *
    • Validates and manages request parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Request + * @see TICIdentifier + */ +public class RequestUnsubscribeTIC extends Request { + /** Message name for this request. */ + public static final String NAME = "UnsubscribeTIC"; + + private List data; + + /** + * Constructs a request for unsubscribing from TIC data with a specific list of identifiers. + * + * @param data the list of TIC identifiers + */ + public RequestUnsubscribeTIC(List data) { + super(NAME); + this.setData(data); + } + + /** + * Returns the list of TIC identifier data associated with this request. + * + * @return the list of TIC identifiers + */ + public List getData() { + return this.data; + } + + /** + * Sets the list of TIC identifier data for this request. + * + * @param data the list of TIC identifiers + */ + public void setData(List data) { + this.data = data; + } +} diff --git a/src/main/java/tic/service/message/ResponseError.java b/src/main/java/tic/service/message/ResponseError.java new file mode 100644 index 0000000..33a03b6 --- /dev/null +++ b/src/main/java/tic/service/message/ResponseError.java @@ -0,0 +1,26 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import tic.util.message.Response; +import java.time.LocalDateTime; + +public class ResponseError extends Response { + /** + * Constructs a generic response with explicit parameters. + * + * @param name the message name + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + */ + public ResponseError(String name, + LocalDateTime dateTime, Number errorCode, String errorMessage) { + super(name, dateTime, errorCode, errorMessage); + } +} diff --git a/src/main/java/tic/service/message/ResponseGetAvailableTICs.java b/src/main/java/tic/service/message/ResponseGetAvailableTICs.java new file mode 100644 index 0000000..22e92f1 --- /dev/null +++ b/src/main/java/tic/service/message/ResponseGetAvailableTICs.java @@ -0,0 +1,74 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import java.time.LocalDateTime; +import java.util.List; +import tic.core.TICIdentifier; +import tic.util.message.Response; +import tic.util.message.ResponseWithData; + +/** + * Response message for available TIC identifiers. + * + *

    This class represents a response containing the list of available TIC identifiers. It provides + * constructors for various initialization scenarios and integrates with the response messaging + * system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates response for available TIC identifiers + *
    • Supports construction from map, DataDictionary, or explicit parameter list + *
    • Validates and manages response parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Response + * @see TICIdentifier + */ +public class ResponseGetAvailableTICs extends ResponseWithData> { + /** Message name for this response. */ + public static final String NAME = "GetAvailableTICs"; + + private List ticIdentifiers; + + /** + * Constructs a response for available TIC identifiers with explicit parameters. + * + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + * @param data the list of available TIC identifiers + */ + public ResponseGetAvailableTICs( + LocalDateTime dateTime, Number errorCode, String errorMessage, List data) { + super(NAME, dateTime, errorCode, errorMessage, data); + this.ticIdentifiers = data; + } + + /** + * Returns the list of available TIC identifier data associated with this response. + * + * @return the list of TIC identifiers + */ + @Override + public List getData() { + return this.ticIdentifiers; + } + + /** + * Sets the list of available TIC identifier data for this response. + * + * @param data the list of TIC identifiers + */ + @Override + public void setData(List data) { + this.ticIdentifiers = data; + } +} diff --git a/src/main/java/tic/service/message/ResponseGetModemsInfo.java b/src/main/java/tic/service/message/ResponseGetModemsInfo.java new file mode 100644 index 0000000..e29de54 --- /dev/null +++ b/src/main/java/tic/service/message/ResponseGetModemsInfo.java @@ -0,0 +1,73 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import java.time.LocalDateTime; +import java.util.List; +import tic.io.modem.ModemDescriptor; +import tic.util.message.ResponseWithData; + +/** + * Response message for modem information. + * + *

    This class represents a response containing the list of modem port descriptors. It provides + * constructors for various initialization scenarios and integrates with the response messaging + * system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates response for modem information + *
    • Supports construction from map, DataDictionary, or explicit parameter list + *
    • Validates and manages response parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Response + * @see ModemDescriptor + */ +public class ResponseGetModemsInfo extends ResponseWithData> { + /** Message name for this response. */ + public static final String NAME = "GetModemsInfo"; + + private List modemDescriptors; + + /** + * Constructs a response for modem information with explicit parameters. + * + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + * @param data the list of modem port descriptors + */ + public ResponseGetModemsInfo( + LocalDateTime dateTime, Number errorCode, String errorMessage, List data) { + super(NAME, dateTime, errorCode, errorMessage, data); + this.modemDescriptors = data; + } + + /** + * Returns the list of modem port descriptor data associated with this response. + * + * @return the list of modem descriptors + */ + @Override + public List getData() { + return this.modemDescriptors; + } + + /** + * Sets the list of modem port descriptor data for this response. + * + * @param data the list of modem descriptors + */ + @Override + public void setData(List data) { + this.modemDescriptors = data; + } +} \ No newline at end of file diff --git a/src/main/java/tic/service/message/ResponseReadTIC.java b/src/main/java/tic/service/message/ResponseReadTIC.java new file mode 100644 index 0000000..b189682 --- /dev/null +++ b/src/main/java/tic/service/message/ResponseReadTIC.java @@ -0,0 +1,61 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import tic.util.message.Response; +import tic.util.message.ResponseWithData; +import tic.core.TICCoreFrame; +import java.time.LocalDateTime; + +/** + * Response message for TIC frame data. + * + *

    This class represents a response containing a TIC frame. It provides constructors for various + * initialization scenarios and integrates with the response messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates response for TIC frame data + *
    • Supports construction from map, DataDictionary, or explicit parameter list + *
    • Validates and manages response parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Response + * @see TICCoreFrame + */ +public class ResponseReadTIC extends ResponseWithData { + /** Message name for this response. */ + public static final String NAME = "ReadTIC"; + private TICCoreFrame ticCoreFrame; + + /** + * Constructs a response for TIC frame data with explicit parameters. + * + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + * @param data the TIC frame data + */ + public ResponseReadTIC( + LocalDateTime dateTime, Number errorCode, String errorMessage, TICCoreFrame data) { + super(NAME, dateTime, errorCode, errorMessage, data); + this.ticCoreFrame = data; +} + + @Override + public TICCoreFrame getData() { + return this.ticCoreFrame; + } + + @Override + public void setData(TICCoreFrame data) { + this.ticCoreFrame = data; + } +} \ No newline at end of file diff --git a/src/main/java/tic/service/message/ResponseSubscribeTIC.java b/src/main/java/tic/service/message/ResponseSubscribeTIC.java new file mode 100644 index 0000000..5d5342d --- /dev/null +++ b/src/main/java/tic/service/message/ResponseSubscribeTIC.java @@ -0,0 +1,44 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import tic.util.message.Response; +import java.time.LocalDateTime; + +/** + * Response message for TIC subscription. + * + *

    This class represents a response to a TIC subscription request. It provides constructors for + * various initialization scenarios and integrates with the response messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates response for TIC subscription + *
    • Supports construction from map, DataDictionary, or explicit parameter list + *
    • Validates and manages response parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Response + */ +public class ResponseSubscribeTIC extends Response { + /** Message name for this response. */ + public static final String NAME = "SubscribeTIC"; + + /** + * Constructs a response for TIC subscription with explicit parameters. + * + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + */ + public ResponseSubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) { + super(NAME, dateTime, errorCode, errorMessage); + } +} diff --git a/src/main/java/tic/service/message/ResponseUnsubscribeTIC.java b/src/main/java/tic/service/message/ResponseUnsubscribeTIC.java new file mode 100644 index 0000000..a34eedf --- /dev/null +++ b/src/main/java/tic/service/message/ResponseUnsubscribeTIC.java @@ -0,0 +1,44 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.message; + +import tic.util.message.Response; +import java.time.LocalDateTime; + +/** + * Response message for TIC unsubscription. + * + *

    This class represents a response to a TIC unsubscription request. It provides constructors for + * various initialization scenarios and integrates with the response messaging system. + * + *

    Key features include: + * + *

      + *
    • Encapsulates response for TIC unsubscription + *
    • Supports construction from map, DataDictionary, or explicit parameter list + *
    • Validates and manages response parameters using key descriptors + *
    + * + * @author Enedis Smarties team + * @see Response + */ +public class ResponseUnsubscribeTIC extends Response { + /** Message name for this response. */ + public static final String NAME = "UnsubscribeTIC"; + + /** + * Constructs a response for TIC unsubscription with explicit parameters. + * + * @param dateTime the response date and time + * @param errorCode the error code, if any + * @param errorMessage the error message, if any + */ + public ResponseUnsubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) { + super(NAME, dateTime, errorCode, errorMessage); + } +} diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java b/src/main/java/tic/service/netty/TIC2WebSocketChannelInitializer.java similarity index 94% rename from src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java rename to src/main/java/tic/service/netty/TIC2WebSocketChannelInitializer.java index 150ca83..d86d6fe 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java +++ b/src/main/java/tic/service/netty/TIC2WebSocketChannelInitializer.java @@ -5,10 +5,10 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.netty; +package tic.service.netty; -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; +import tic.service.client.TIC2WebSocketClientPool; +import tic.service.requesthandler.TIC2WebSocketRequestHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java b/src/main/java/tic/service/netty/TIC2WebSocketHandler.java similarity index 69% rename from src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java rename to src/main/java/tic/service/netty/TIC2WebSocketHandler.java index dc622eb..0584b4a 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java +++ b/src/main/java/tic/service/netty/TIC2WebSocketHandler.java @@ -5,45 +5,48 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.netty; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Event; -import enedis.lab.util.message.Message; -import enedis.lab.util.message.Request; -import enedis.lab.util.message.Response; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import enedis.tic.core.TICIdentifier; -import enedis.tic.service.client.TIC2WebSocketClient; -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.endpoint.EventSender; -import enedis.tic.service.endpoint.TIC2WebSocketEndPointErrorCode; -import enedis.tic.service.message.RequestUnsubscribeTIC; -import enedis.tic.service.message.factory.TIC2WebSocketMessageFactory; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; +package tic.service.netty; + import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import tic.core.TICIdentifier; +import tic.service.client.TIC2WebSocketClient; +import tic.service.client.TIC2WebSocketClientPool; +import tic.service.endpoint.EventSender; +import tic.service.endpoint.TIC2WebSocketEndPointErrorCode; +import tic.service.message.RequestUnsubscribeTIC; +import tic.service.message.ResponseError; +import tic.service.requesthandler.TIC2WebSocketRequestHandler; +import tic.util.message.Event; +import tic.util.message.Message; +import tic.util.message.Request; +import tic.util.message.Response; +import tic.util.message.codec.MessageJsonCodec; +import tic.util.message.exception.MessageException; +import tic.util.message.exception.MessageInvalidContentException; +import tic.util.message.exception.MessageInvalidFormatException; +import tic.util.message.exception.MessageInvalidTypeException; +import tic.util.message.exception.MessageKeyNameDoesntExistException; +import tic.util.message.exception.MessageKeyTypeDoesntExistException; +import tic.util.message.exception.UnsupportedMessageException; /** * Netty WebSocket handler for TIC2WebSocket. * - *

    This handler manages WebSocket connections for TIC2WebSocket, handling incoming WebSocket frames, - * client lifecycle events, and message dispatching. It integrates with the client pool and request handler - * to process requests and send responses or events over the WebSocket channel. + *

    This handler manages WebSocket connections for TIC2WebSocket, handling incoming WebSocket + * frames, client lifecycle events, and message dispatching. It integrates with the client pool and + * request handler to process requests and send responses or events over the WebSocket channel. * *

    Responsibilities include: + * *

      *
    • Managing client connections and their lifecycle *
    • Parsing and validating incoming WebSocket messages @@ -52,7 +55,8 @@ *
    • Logging and error handling for channel operations *
    * - *

    This class is intended to be used as a Netty handler in the server pipeline for real-time TIC data exchange. + *

    This class is intended to be used as a Netty handler in the server pipeline for real-time TIC + * data exchange. * * @author Enedis Smarties team * @see TIC2WebSocketClientPool @@ -66,10 +70,11 @@ public class TIC2WebSocketHandler extends SimpleChannelInboundHandler) null); this.handleRequest(clientPool.getClient(channelId).get(), request); - logger.info("Remove client with channel id : " + channelId); + logger.debug("Remove client with channel id : " + channelId); clientPool.remove(channelId); - } catch (DataDictionaryException e) { + } catch (Exception e) { logger.error("Error during channel close", e); } } else { @@ -175,8 +179,8 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E /** * Handles incoming WebSocket frames. * - *

    Processes text frames as TIC2WebSocket messages, validates and dispatches requests, - * and sends responses. Unsupported frame types are logged as warnings. + *

    Processes text frames as TIC2WebSocket messages, validates and dispatches requests, and + * sends responses. Unsupported frame types are logged as warnings. * * @param ctx the channel handler context * @param frame the received WebSocket frame @@ -190,7 +194,7 @@ protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) thr Channel channel = ctx.channel(); String channelId = channel.id().asLongText(); - logger.info("Message on channel " + channelId); + logger.debug("Message on channel " + channelId); TIC2WebSocketClient client = this.getClient(channel); @@ -245,30 +249,35 @@ private Optional getMessage(Channel channel, String text) { String errorMessage = ""; try { - message = this.factory.getMessage(text); - } catch (MessageInvalidFormatException e) { - errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_FORMAT; - errorMessage = e.getMessage(); - } catch (MessageInvalidTypeException e) { - errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_INVALID; - errorMessage = e.getMessage(); - } catch (MessageKeyNameDoesntExistException e) { - errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_NAME_MISSING; - errorMessage = e.getMessage(); - } catch (MessageKeyTypeDoesntExistException e) { - errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_MISSING; - errorMessage = e.getMessage(); - } catch (MessageInvalidContentException e) { - errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_CONTENT; - errorMessage = e.getMessage(); - } catch (UnsupportedMessageException e) { - errorCode = TIC2WebSocketEndPointErrorCode.UNSUPPORTED_MESSAGE; - errorMessage = e.getMessage(); + message = messageJsonCodec.decodeFromJsonString(text); + } catch (MessageException e) { + if (e instanceof MessageInvalidFormatException) { + errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_FORMAT; + errorMessage = e.getMessage(); + } else if (e instanceof MessageInvalidTypeException) { + errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_INVALID; + errorMessage = e.getMessage(); + } else if (e instanceof MessageKeyNameDoesntExistException) { + errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_NAME_MISSING; + errorMessage = e.getMessage(); + } else if (e instanceof MessageKeyTypeDoesntExistException) { + errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_MISSING; + errorMessage = e.getMessage(); + } else if (e instanceof MessageInvalidContentException) { + errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_CONTENT; + errorMessage = e.getMessage(); + } else if (e instanceof UnsupportedMessageException) { + errorCode = TIC2WebSocketEndPointErrorCode.UNSUPPORTED_MESSAGE; + errorMessage = e.getMessage(); + } + } catch (Exception e) { + errorCode = TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR; + errorMessage = "Internal error: " + e.getMessage(); } if (errorCode != TIC2WebSocketEndPointErrorCode.NO_ERROR) { logger.error("Error parsing message: " + errorMessage); - // TODO: Send error event + this.sendErrorMessage(channel, errorCode, errorMessage); return Optional.empty(); } @@ -287,7 +296,8 @@ private Optional getMessage(Channel channel, String text) { private Optional getRequest(Channel channel, Message message) { if (!(message instanceof Request)) { logger.error("Message is not a request"); - // TODO: Send error event + this.sendErrorMessage( + channel, TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_INVALID, "Message is not a request"); return Optional.empty(); } @@ -305,6 +315,13 @@ private Response handleRequest(TIC2WebSocketClient client, Request request) { return requestHandler.handle(request, client); } + private void sendErrorMessage( + Channel channel, TIC2WebSocketEndPointErrorCode errorCode, String errorMessage) { + ResponseError responseError = + new ResponseError("ErrorResponse", LocalDateTime.now(), errorCode.value(), errorMessage); + this.sendMessage(channel, responseError); + } + /** * Sends a message to the specified channel as a JSON WebSocket frame. * @@ -315,7 +332,7 @@ private Response handleRequest(TIC2WebSocketClient client, Request request) { */ private void sendMessage(Channel channel, Message message) { try { - String json = message.toJSON().toString(); + String json = messageJsonCodec.encodeToJsonString(message); TextWebSocketFrame frame = new TextWebSocketFrame(json); channel.writeAndFlush(frame); diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java b/src/main/java/tic/service/netty/TIC2WebSocketServer.java similarity index 96% rename from src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java rename to src/main/java/tic/service/netty/TIC2WebSocketServer.java index c6410c1..4788cb8 100644 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java +++ b/src/main/java/tic/service/netty/TIC2WebSocketServer.java @@ -5,10 +5,10 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.netty; +package tic.service.netty; -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; +import tic.service.client.TIC2WebSocketClientPool; +import tic.service.requesthandler.TIC2WebSocketRequestHandler; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java b/src/main/java/tic/service/requesthandler/TIC2WebSocketRequestHandler.java similarity index 88% rename from src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java rename to src/main/java/tic/service/requesthandler/TIC2WebSocketRequestHandler.java index c69bbd8..bfb4939 100644 --- a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java +++ b/src/main/java/tic/service/requesthandler/TIC2WebSocketRequestHandler.java @@ -5,11 +5,11 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.requesthandler; +package tic.service.requesthandler; -import enedis.lab.util.message.Request; -import enedis.lab.util.message.Response; -import enedis.tic.service.client.TIC2WebSocketClient; +import tic.util.message.Request; +import tic.util.message.Response; +import tic.service.client.TIC2WebSocketClient; /** * Interface for handling TIC2WebSocket requests. diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java b/src/main/java/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java similarity index 71% rename from src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java rename to src/main/java/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java index 461e8f1..70f176d 100644 --- a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java +++ b/src/main/java/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java @@ -5,45 +5,46 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.service.requesthandler; - -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Request; -import enedis.lab.util.message.Response; -import enedis.lab.util.message.ResponseBase; -import enedis.tic.core.TICCore; -import enedis.tic.core.TICCoreErrorCode; -import enedis.tic.core.TICCoreException; -import enedis.tic.core.TICCoreFrame; -import enedis.tic.core.TICIdentifier; -import enedis.tic.service.client.TIC2WebSocketClient; -import enedis.tic.service.endpoint.TIC2WebSocketEndPointErrorCode; -import enedis.tic.service.message.RequestGetAvailableTICs; -import enedis.tic.service.message.RequestGetModemsInfo; -import enedis.tic.service.message.RequestReadTIC; -import enedis.tic.service.message.RequestSubscribeTIC; -import enedis.tic.service.message.RequestUnsubscribeTIC; -import enedis.tic.service.message.ResponseGetAvailableTICs; -import enedis.tic.service.message.ResponseGetModemsInfo; -import enedis.tic.service.message.ResponseReadTIC; -import enedis.tic.service.message.ResponseSubscribeTIC; -import enedis.tic.service.message.ResponseUnsubscribeTIC; +package tic.service.requesthandler; + import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import tic.core.TICCore; +import tic.core.TICCoreErrorCode; +import tic.core.TICCoreException; +import tic.core.TICCoreFrame; +import tic.core.TICIdentifier; +import tic.io.modem.ModemDescriptor; +import tic.service.client.TIC2WebSocketClient; +import tic.service.endpoint.TIC2WebSocketEndPointErrorCode; +import tic.service.message.RequestGetAvailableTICs; +import tic.service.message.RequestGetModemsInfo; +import tic.service.message.RequestReadTIC; +import tic.service.message.RequestSubscribeTIC; +import tic.service.message.RequestUnsubscribeTIC; +import tic.service.message.ResponseError; +import tic.service.message.ResponseGetAvailableTICs; +import tic.service.message.ResponseGetModemsInfo; +import tic.service.message.ResponseReadTIC; +import tic.service.message.ResponseSubscribeTIC; +import tic.service.message.ResponseUnsubscribeTIC; +import tic.util.message.Request; +import tic.util.message.Response; /** * Base implementation of the TIC2WebSocket request handler. * - *

    This class provides the default logic for handling TIC2WebSocket requests, including subscription, - * unsubscription, reading TIC frames, and retrieving available TICs and modem information. It integrates - * with the TICCore to perform operations and generate appropriate responses for each request type. + *

    This class provides the default logic for handling TIC2WebSocket requests, including + * subscription, unsubscription, reading TIC frames, and retrieving available TICs and modem + * information. It integrates with the TICCore to perform operations and generate appropriate + * responses for each request type. * *

    Responsibilities include: + * *

      *
    • Dispatching and processing supported TIC2WebSocket requests *
    • Managing client subscriptions and unsubscriptions @@ -60,6 +61,7 @@ public class TIC2WebSocketRequestHandlerBase implements TIC2WebSocketRequestHandler { /** Logger for request handling and errors. */ private Logger logger; + /** TICCore instance for data operations. */ private TICCore ticCore; @@ -88,21 +90,22 @@ public TIC2WebSocketRequestHandlerBase(TICCore ticCore) { public Response handle(Request request, TIC2WebSocketClient client) { Response response = null; + logger.info("Handling request: " + request.getName()); switch (request.getName()) { case RequestGetAvailableTICs.NAME: - response = this.handleGetAvailableTICsRequest((RequestGetAvailableTICs) request); + response = this.handleGetAvailableTICsRequest(request); break; case RequestGetModemsInfo.NAME: - response = this.handleGetModemsInfoRequest((RequestGetModemsInfo) request); + response = this.handleGetModemsInfoRequest(request); break; case RequestReadTIC.NAME: - response = this.handleReadTICRequest((RequestReadTIC) request); + response = this.handleReadTICRequest(request); break; case RequestSubscribeTIC.NAME: - response = this.handleSubscribeTICRequest((RequestSubscribeTIC) request, client); + response = this.handleSubscribeTICRequest(request, client); break; case RequestUnsubscribeTIC.NAME: - response = this.handleUnsubscribeTICRequest((RequestUnsubscribeTIC) request, client); + response = this.handleUnsubscribeTICRequest(request, client); break; default: this.logger.error("Request " + request.getName() + " not supported"); @@ -123,10 +126,11 @@ public Response handle(Request request, TIC2WebSocketClient client) { * @param request the request to process * @return the response containing available TIC identifiers or an error */ - private Response handleGetAvailableTICsRequest(RequestGetAvailableTICs request) { + private Response handleGetAvailableTICsRequest(Request request) { List ticIdentifiers = this.ticCore.getAvailableTICs(); Response response = null; + try { response = new ResponseGetAvailableTICs( @@ -134,7 +138,7 @@ private Response handleGetAvailableTICsRequest(RequestGetAvailableTICs request) TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, ticIdentifiers); - } catch (DataDictionaryException e) { + } catch (Exception e) { this.logger.error(e.getMessage(), e); response = this.createErrorResponse( @@ -150,8 +154,8 @@ private Response handleGetAvailableTICsRequest(RequestGetAvailableTICs request) * @param request the request to process * @return the response containing modem information or an error */ - private Response handleGetModemsInfoRequest(RequestGetModemsInfo request) { - List modemsInfo = this.ticCore.getModemsInfo(); + private Response handleGetModemsInfoRequest(Request request) { + List modemsInfo = this.ticCore.getModemsInfo(); Response response = null; try { @@ -161,7 +165,7 @@ private Response handleGetModemsInfoRequest(RequestGetModemsInfo request) { TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, modemsInfo); - } catch (DataDictionaryException e) { + } catch (Exception e) { this.logger.error(e.getMessage(), e); response = this.createErrorResponse( @@ -177,18 +181,22 @@ private Response handleGetModemsInfoRequest(RequestGetModemsInfo request) { * @param request the request to process * @return the response containing the TIC frame or an error */ - private Response handleReadTICRequest(RequestReadTIC request) { + private Response handleReadTICRequest(Request request) { Response response = null; try { - TICCoreFrame frame = this.ticCore.readNextFrame(request.getData()); + if (!(request instanceof RequestReadTIC)) { + return this.createErrorResponse( + request.getName(), + TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, + "Invalid request type for " + request.getName()); + } + + TICIdentifier identifier = ((RequestReadTIC) request).getData(); + + TICCoreFrame frame = this.ticCore.readNextFrame(identifier); response = new ResponseReadTIC( LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, frame); - } catch (DataDictionaryException e) { - this.logger.error(e.getMessage(), e); - response = - this.createErrorResponse( - request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); } catch (TICCoreException e) { if (e.getErrorCode() == TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode()) { response = @@ -217,10 +225,19 @@ private Response handleReadTICRequest(RequestReadTIC request) { * @param client the client to subscribe * @return the response indicating subscription success or failure */ - private Response handleSubscribeTICRequest( - RequestSubscribeTIC request, TIC2WebSocketClient client) { + private Response handleSubscribeTICRequest(Request request, TIC2WebSocketClient client) { Response response = null; - Optional> ticIdentifiers = Optional.ofNullable(request.getData()); + + if (!(request instanceof RequestSubscribeTIC)) { + return this.createErrorResponse( + request.getName(), + TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, + "Invalid request type for " + request.getName()); + } + + List requestedIdentifiers = ((RequestSubscribeTIC) request).getData(); + + Optional> ticIdentifiers = Optional.ofNullable(requestedIdentifiers); if (ticIdentifiers.isPresent()) { List newSubscriptions = @@ -233,7 +250,7 @@ private Response handleSubscribeTICRequest( response = new ResponseSubscribeTIC( LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } catch (DataDictionaryException e) { + } catch (Exception e) { this.logger.error(e.getMessage(), e); } } catch (TICCoreException e) { @@ -251,7 +268,7 @@ private Response handleSubscribeTICRequest( response = new ResponseSubscribeTIC( LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } catch (DataDictionaryException e) { + } catch (Exception e) { this.logger.error(e.getMessage(), e); } } @@ -262,14 +279,15 @@ private Response handleSubscribeTICRequest( /** * Determines new TIC subscriptions requested by the client. * - *

      Compares current subscriptions with requested identifiers and returns only new subscriptions. + *

      Compares current subscriptions with requested identifiers and returns only new + * subscriptions. * * @param currentSubscriptions the client's current subscriptions * @param askedTicIdentifiers the requested TIC identifiers * @return a list of new TIC identifiers to subscribe */ private List getNewSubcriptions( - List currentSubscriptions, List askedTicIdentifiers) { + List currentSubscriptions, List askedTicIdentifiers) { List newSubscriptions = new ArrayList(); for (TICIdentifier askedTicIdentifier : askedTicIdentifiers) { @@ -294,10 +312,19 @@ private List getNewSubcriptions( * @param client the client to unsubscribe * @return the response indicating unsubscription success or failure */ - private Response handleUnsubscribeTICRequest( - RequestUnsubscribeTIC request, TIC2WebSocketClient client) { + private Response handleUnsubscribeTICRequest(Request request, TIC2WebSocketClient client) { Response response = null; - Optional> ticIdentifiers = Optional.ofNullable(request.getData()); + + if (!(request instanceof RequestUnsubscribeTIC)) { + return this.createErrorResponse( + request.getName(), + TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, + "Invalid request type for " + request.getName()); + } + + List requestedIdentifiers = ((RequestUnsubscribeTIC) request).getData(); + + Optional> ticIdentifiers = Optional.ofNullable(requestedIdentifiers); if (ticIdentifiers.isPresent()) { for (TICIdentifier identifier : ticIdentifiers.get()) { @@ -307,7 +334,7 @@ private Response handleUnsubscribeTICRequest( response = new ResponseUnsubscribeTIC( LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } catch (DataDictionaryException e) { + } catch (Exception e) { this.logger.error(e.getMessage(), e); } } catch (TICCoreException e) { @@ -324,7 +351,7 @@ private Response handleUnsubscribeTICRequest( response = new ResponseUnsubscribeTIC( LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } catch (DataDictionaryException e) { + } catch (Exception e) { this.logger.error(e.getMessage(), e); } } @@ -341,10 +368,10 @@ private Response handleUnsubscribeTICRequest( * @return the error response */ private Response createErrorResponse( - String name, TIC2WebSocketEndPointErrorCode code, String message) { + String name, TIC2WebSocketEndPointErrorCode code, String message) { try { - return new ResponseBase(name, LocalDateTime.now(), code.value(), message, null); - } catch (DataDictionaryException e) { + return new ResponseError(name, LocalDateTime.now(), code.value(), message); + } catch (Exception e) { this.logger.error(e.getMessage(), e); return null; } diff --git a/src/main/java/tic/stream/TICStream.java b/src/main/java/tic/stream/TICStream.java new file mode 100644 index 0000000..a676cfe --- /dev/null +++ b/src/main/java/tic/stream/TICStream.java @@ -0,0 +1,259 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream; + +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tic.frame.TICFrame; +import tic.frame.TICMode; +import tic.frame.codec.TICFrameCodec; +import tic.frame.codec.TICFrameSummarizedCodec; +import tic.io.serialport.SerialPortDescriptor; +import tic.io.serialport.SerialPortFinder; +import tic.io.serialport.SerialPortFinderBase; +import tic.stream.configuration.TICStreamConfiguration; +import tic.stream.configuration.TICStreamConfigurationLoader; +import tic.stream.identifier.TICStreamIdentifier; +import tic.stream.identifier.TICStreamIdentifierType; +import tic.util.task.Task; +import tic.util.task.TaskBase; +import tic.util.task.TaskPeriodicWithSubscribers; + +public class TICStream extends TaskPeriodicWithSubscribers { + + private static final Logger logger = LogManager.getLogger(TICStream.class); + + private final TICStreamConfiguration configuration; + private final SerialPortFinder portFinder; + + private final int timeoutMillis; + private TICStreamModeDetector streamModeDetector; + private TICMode currentMode; + private TICStreamReader streamReader; + private TICFrame lastFrame; + + public static void main(String[] args) { + if (args.length != 1) { + System.err.println("Usage: java tic.stream.TICStream "); + System.exit(1); + } + + String configPath = args[0]; + + try { + TICStreamConfiguration configuration = TICStreamConfigurationLoader.load(configPath); + TICStream stream = new TICStream(configuration); + CountDownLatch shutdownLatch = new CountDownLatch(1); + + stream.subscribe( + new TICStreamListener() { + @Override + public void onFrame(TICFrame frame) { + + try { + logger.debug( + "TIC stream frame:\n" + + TICFrameSummarizedCodec.getInstance().encodeToJsonString(frame) + + "\n"); + } catch (Exception e) { + logger.error("Failed to encode TIC frame to JSON: " + e.getMessage()); + } + } + + @Override + public void onError(String error) { + logger.error("TIC stream error: " + error); + } + }); + + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + stream.stop(); + shutdownLatch.countDown(); + })); + + logger.info("Starting TIC stream with configuration " + configPath); + logger.info("Press CTRL-C to stop."); + stream.start(); + shutdownLatch.await(); + } catch (Exception exception) { + logger.error("Failed to start TIC stream: " + exception.getMessage()); + System.exit(1); + } + } + + public TICStream(TICStreamConfiguration configuration) { + this(configuration, SerialPortFinderBase.getInstance()); + } + + public TICStream(TICStreamConfiguration configuration, SerialPortFinder portFinder) { + this.configuration = Objects.requireNonNull(configuration, "configuration must not be null"); + this.portFinder = Objects.requireNonNull(portFinder, "portFinder must not be null"); + this.timeoutMillis = configuration.getTimeout() * 1000; + this.initializeStreamReader(); + this.initializeStreamModeDetector(); + } + + private void initializeStreamReader() { + String portName = this.resolvePortName(); + int baudrate = this.resolveBaudrate(this.configuration.getTicMode()); + if (this.streamReader != null) { + try { + this.streamReader.close(); + } catch (Exception e) { + logger.warn("Failed to close existing TIC stream reader: {}", e.getMessage()); + } + } + this.streamReader = new TICStreamReader(portName, baudrate, timeoutMillis); + + if (this.streamModeDetector == null) { + this.initializeStreamModeDetector(); + } else { + this.streamModeDetector.setStreamReader(this.streamReader); + } + } + + private void initializeStreamModeDetector() { + if (this.streamReader == null) { + throw new IllegalStateException("TIC stream reader is not initialized"); + } + this.streamModeDetector = new TICStreamModeDetector(this.currentMode, this.streamReader); + } + + // Synchronous + public TICFrame getNextFrame() { + byte[] ticFrameAsByte = this.streamReader.read(); + if (ticFrameAsByte == null) { + return null; + } + TICFrame ticFrame = TICFrameCodec.decode(ticFrameAsByte); + return ticFrame; + } + + // Asynchronous + public TICFrame getLastFrame() { + return this.lastFrame; + } + + @Override + protected void process() { + if (this.streamReader == null) { + throw new IllegalStateException("TIC stream reader is not initialized"); + } + + if (this.currentMode == null) { + TICMode newMode = this.streamModeDetector.autoDetectMode(); + if (newMode != null && newMode != this.currentMode) { + logger.info("TIC mode detected: {}", newMode); + } + if (newMode == null) { + this.onReadTimeout(); + return; + } + this.currentMode = newMode; + this.initializeStreamReader(); + } + + try { + TICFrame ticFrame = this.getNextFrame(); + + if (ticFrame == null) { + this.onReadTimeout(); + } else { + this.notifyOnDataRead(ticFrame); + } + this.lastFrame = ticFrame; + } catch (Exception e) { + this.notifyOnErrorDetected("TIC read failed: " + e.getMessage()); + } + } + + private void notifyOnDataRead(TICFrame ticFrame) { + Collection subscribers = this.getSubscribers(); + for (TICStreamListener subscriber : subscribers) { + Task task = + new TaskBase() { + @Override + public void process() { + subscriber.onFrame(ticFrame); + } + }; + task.start(); + } + } + + private void notifyOnErrorDetected(String message) { + Collection subscribers = this.getSubscribers(); + for (TICStreamListener subscriber : subscribers) { + Task task = + new TaskBase() { + @Override + public void process() { + subscriber.onError(message); + } + }; + task.start(); + } + } + + protected void onReadTimeout() { + this.notifyOnErrorDetected("TIC read timeout"); + try { + this.streamReader.reset(); + } catch (Exception e) { + } + + this.currentMode = null; + } + + private int resolveBaudrate(TICMode mode) { + if (mode == TICMode.HISTORIC) { + return 1200; + } + return 9600; // Default for STANDARD and AUTO + } + + private String resolvePortName() { + TICStreamIdentifier identifier = this.configuration.getIdentifier(); + if (identifier == null) { + throw new IllegalStateException("No TIC stream identifier configured"); + } + + SerialPortDescriptor descriptor = null; + if (identifier.getType() == TICStreamIdentifierType.PORT_ID) { + descriptor = this.portFinder.findByPortId(identifier.getPortId()); + if (descriptor == null) { + throw new IllegalStateException( + "Serial port with id " + identifier.getPortId() + " not found"); + } + } else if (identifier.getType() == TICStreamIdentifierType.PORT_NAME) { + String configuredName = identifier.getPortName(); + if (configuredName == null || configuredName.isEmpty()) { + throw new IllegalStateException("Serial port name is not configured"); + } + descriptor = this.portFinder.findByPortName(configuredName); + if (descriptor == null) { + logger.warn( + "Serial port {} not found via discovery, attempting to open directly", configuredName); + return configuredName; + } + } else { + throw new IllegalStateException("Unsupported identifier type " + identifier.getType()); + } + + if (descriptor.portName() == null || descriptor.portName().isEmpty()) { + throw new IllegalStateException("Resolved descriptor lacks a port name"); + } + return descriptor.portName(); + } +} diff --git a/src/main/java/tic/stream/TICStreamListener.java b/src/main/java/tic/stream/TICStreamListener.java new file mode 100644 index 0000000..caf59c3 --- /dev/null +++ b/src/main/java/tic/stream/TICStreamListener.java @@ -0,0 +1,28 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream; + +import tic.frame.TICFrame; +import tic.util.task.Subscriber; + +public interface TICStreamListener extends Subscriber { + /** + * Notify when a new TIC frame is received + * + * @param frame the frame received + */ + public void onFrame(TICFrame frame); + + /** + * Notify when an error occurs during TIC stream reading + * + * @param error the error detected + */ + public void onError(String error); + +} diff --git a/src/main/java/tic/stream/TICStreamModeDetector.java b/src/main/java/tic/stream/TICStreamModeDetector.java new file mode 100644 index 0000000..0f72a0e --- /dev/null +++ b/src/main/java/tic/stream/TICStreamModeDetector.java @@ -0,0 +1,105 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream; + +import java.util.Arrays; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tic.frame.TICMode; +import tic.frame.delimiter.TICStartPattern; + +public class TICStreamModeDetector { + private static final Logger logger = LogManager.getLogger(TICStreamModeDetector.class); + private TICMode selectedMode; + private TICMode currentMode; + private TICStreamReader streamReader; + + public TICStreamModeDetector(TICMode ticMode, TICStreamReader streamReader) { + this.selectedMode = ticMode; + this.streamReader = streamReader; + } + + /** + * Updates the stream reader reference so auto-detection keeps using the active reader after a + * reconnect/reset. + */ + public void setStreamReader(TICStreamReader streamReader) { + this.streamReader = streamReader; + } + + /** + * Gets the current TIC mode being used by the channel. Returns the detected mode if available, + * otherwise returns the selected mode from configuration. + * + * @return the current TIC mode (HISTORIC, STANDARD, or AUTO) + */ + public TICMode getMode() { + return (this.getCurrentMode() != null) ? this.getCurrentMode() : this.getSelectedMode(); + } + + /** + * Gets the TIC mode selected in the channel configuration. + * + * @return the selected TIC mode from configuration + */ + public TICMode getSelectedMode() { + return this.selectedMode; + } + + /** + * Gets the currently detected TIC mode during operation. This may differ from the selected mode + * if auto-detection is enabled. + * + * @return the currently detected TIC mode, or null if not yet detected + */ + public TICMode getCurrentMode() { + return this.currentMode; + } + + public TICMode autoDetectMode() { + if (this.selectedMode == TICMode.STANDARD || this.selectedMode == TICMode.HISTORIC) { + this.currentMode = this.selectedMode; + return this.currentMode; + } + logger.debug("Auto detecting TIC Mode"); + try { + byte[] ticFrame = this.streamReader.read(); + + for (TICMode ticMode : TICMode.values()) { + if (ticMode != TICMode.AUTO) { + if (this.checkMode(ticMode, ticFrame)) { + logger.debug("TIC Mode " + ticMode + " detected"); + this.currentMode = ticMode; + return this.currentMode; + } + } + } + + logger.debug("TIC Mode not detected"); + return null; + } catch (Exception e) { + logger.error("Error during TIC mode detection: " + e.getMessage(), e); + } + return null; + } + + private boolean checkMode(TICMode ticMode, byte[] ticFrame) { + if (ticFrame == null || ticFrame.length <= TICStartPattern.length()) { + return false; + } + + byte[] expectedStart = this.getExpectedStartBytes(ticMode); + byte[] ticFrameStart = Arrays.copyOf(ticFrame, TICStartPattern.length()); + + return Arrays.equals(ticFrameStart, expectedStart); + } + + private byte[] getExpectedStartBytes(TICMode ticMode) { + return TICStartPattern.getValueFromMode(ticMode); + } +} diff --git a/src/main/java/tic/stream/TICStreamReader.java b/src/main/java/tic/stream/TICStreamReader.java new file mode 100644 index 0000000..0827f65 --- /dev/null +++ b/src/main/java/tic/stream/TICStreamReader.java @@ -0,0 +1,243 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream; + +import jssc.SerialPort; +import jssc.SerialPortException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tic.frame.TICFrame; +import tic.frame.codec.TICFrameCodec; +import tic.frame.codec.TICFrameSummarizedCodec; +import tic.frame.delimiter.TICFrameDelimiter; +import tic.util.time.Time; + +public class TICStreamReader { + + private static final Logger logger = LogManager.getLogger(TICStreamReader.class); + private String portName; + private SerialPort serialPort; + private int timeoutMillis; + private int baudrate; + private int pollingPeriod; + + /** Polling period in milliseconds for data reception */ + private static final int RECEIVE_DATA_POLLING_PERIOD = 100; + + private static final int DATA_BITS = 7; + private static final int STOP_BITS = SerialPort.STOPBITS_1; + private static final int PARITY = SerialPort.PARITY_EVEN; + + public TICStreamReader(String portName, int baudrate, int timeoutMillis) { + this.portName = portName; + this.baudrate = baudrate; + this.pollingPeriod = RECEIVE_DATA_POLLING_PERIOD; + this.timeoutMillis = timeoutMillis; + } + + /** + * Small CLI helper that opens a TIC stream with inline configuration and prints the first + * received frame to the console. + * + *

      Usage: {@code java tic.stream.TICStreamReader } + */ + public static void main(String[] args) { + if (args.length < 2) { + System.err.println("Usage: java tic.stream.TICStreamReader "); + System.exit(1); + } + + String portName = args[0]; + + int baudrate; + try { + baudrate = Integer.parseInt(args[1]); + } catch (NumberFormatException exception) { + System.err.println("Invalid baudrate value: " + args[1]); + System.exit(1); + return; + } + int timeout = 10000; + + TICStreamReader streamReader = new TICStreamReader(portName, baudrate, timeout); + + System.out.println( + "Listening TIC stream on PORT_NAME=" + portName + " at baudrate=" + baudrate); + try { + byte[] frameBuffer = streamReader.read(); + if (frameBuffer == null) { + System.err.println("No TIC frame received within timeout (" + timeout + "s)"); + System.exit(3); + } else { + TICFrame frame = TICFrameCodec.decode(frameBuffer); + String jsonFrame = TICFrameSummarizedCodec.getInstance().encodeToJsonString(frame); + System.out.println("TIC frame read:\n" + jsonFrame); + System.exit(0); + } + } catch (Exception exception) { + System.err.println("Error while reading TIC frame: " + exception.getMessage()); + exception.printStackTrace(System.err); + System.exit(2); + } finally { + try { + streamReader.close(); + } catch (Exception closeException) { + System.err.println("Warning: failed to close stream: " + closeException.getMessage()); + } + } + } + + public byte[] read() { + long beginTime = System.currentTimeMillis(); + long elapsedTime = 0; + boolean startFrameFound = false; + StringBuffer buffer = new StringBuffer(); + + while (hasRemainingTime(elapsedTime)) { + Character nextByte = readNextByteIfAvailable(); + if (nextByte != null) { + if (!startFrameFound) { + if (isStartDelimiter(nextByte)) { + startFrameFound = true; + buffer.append(nextByte); + } + } else { + buffer.append(nextByte); + if (isEndDelimiter(nextByte)) { + return buffer.toString().getBytes(); + } + } + } + elapsedTime = System.currentTimeMillis() - beginTime; + } + + return null; + } + + private boolean hasRemainingTime(long elapsedTime) { + return this.timeoutMillis == 0 || elapsedTime < this.timeoutMillis; + } + + private Character readNextByteIfAvailable() { + try { + if (this.available() <= 0) { + Time.sleep(this.pollingPeriod); + return null; + } + } catch (Exception e) { + return null; + } + + byte[] chunk = this.read(1); + if (chunk.length == 0) { + return null; + } + + return (char) chunk[0]; + } + + private boolean isStartDelimiter(char value) { + return value == TICFrameDelimiter.BEGIN.getValue(); + } + + private boolean isEndDelimiter(char value) { + return value == TICFrameDelimiter.END.getValue(); + } + + /** Reads the specified number of bytes from the serial port input buffer. */ + private synchronized byte[] read(int byteCount) { + if (byteCount <= 0) { + throw new IllegalArgumentException("byteCount must be strictly positive"); + } + this.open(); + try { + byte[] data = this.serialPort.readBytes(byteCount); + return (data != null) ? data : new byte[0]; + } catch (SerialPortException exception) { + throw new IllegalStateException("Cannot read serial port", exception); + } + } + + /** Returns the number of bytes available to read from the serial port input buffer. */ + private synchronized int available() { + this.open(); + try { + return this.serialPort.getInputBufferBytesCount(); + } catch (SerialPortException exception) { + throw new IllegalStateException("Cannot query input buffer size", exception); + } + } + + /** Flushes the serial port input and output buffers. */ + private synchronized void flush() { + this.open(); + try { + int mask = + SerialPort.PURGE_RXCLEAR + | SerialPort.PURGE_RXABORT + | SerialPort.PURGE_TXCLEAR + | SerialPort.PURGE_TXABORT; + this.serialPort.purgePort(mask); + } catch (SerialPortException exception) { + throw new IllegalStateException("Cannot flush serial port", exception); + } + } + + /** Opens the serial port if needed and returns the handler. */ + private synchronized void open() { + if (this.isOpened()) { + return; + } + + if (this.serialPort == null) { + this.serialPort = new SerialPort(this.portName); + } + + try { + this.serialPort.openPort(); + this.serialPort.setParams(this.baudrate, DATA_BITS, STOP_BITS, PARITY); + this.serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); + + logger.info("Opened TIC serial port {}", this.serialPort.getPortName()); + } catch (SerialPortException exception) { + throw new IllegalStateException( + "Failed to open serial port " + this.serialPort.getPortName(), exception); + } + } + + /** Closes the serial port if opened. */ + public synchronized void close() { + if (this.serialPort == null) { + return; + } + try { + if (this.serialPort.isOpened()) { + this.serialPort.closePort(); + logger.debug("Closed serial port {}", this.serialPort.getPortName()); + } + } catch (SerialPortException exception) { + throw new IllegalStateException("Cannot close serial port", exception); + } finally { + this.serialPort = null; + } + } + + protected void reset() { + this.close(); + this.open(); + try { + this.flush(); + } catch (Exception e) { + } + } + + /** Indicates whether the serial port is currently opened. */ + private synchronized boolean isOpened() { + return this.serialPort != null && this.serialPort.isOpened(); + } +} diff --git a/src/main/java/tic/stream/configuration/TICStreamConfiguration.java b/src/main/java/tic/stream/configuration/TICStreamConfiguration.java new file mode 100644 index 0000000..26e5c7e --- /dev/null +++ b/src/main/java/tic/stream/configuration/TICStreamConfiguration.java @@ -0,0 +1,97 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream.configuration; + +import tic.frame.TICMode; +import tic.stream.identifier.TICStreamIdentifier; + +/** + * Configuration class for TIC stream, including TIC mode, identifier, and timeout. + * + * @author Enedis Smarties team + */ +public class TICStreamConfiguration { + + public static final TICMode DEFAULT_TIC_MODE = TICMode.AUTO; + public static final int DEFAULT_TIMEOUT = 10; + + private TICMode ticMode; + private TICStreamIdentifier identifier; + private int timeout; + + /** + * Constructs a TICStreamConfiguration with the specified parameters. + * + * @param ticMode the TIC mode (STANDARD, HISTORIC, or AUTO) + */ + public TICStreamConfiguration(TICMode ticMode, TICStreamIdentifier identifier, int timeout) { + this.setTicMode(ticMode); + this.setIdentifier(identifier); + this.setTimeout(timeout); + } + + /** + * Returns the configured TIC mode for this stream. + * + * @return the TIC mode (STANDARD, HISTORIC, or AUTO) + */ + public TICMode getTicMode() { + return this.ticMode; + } + + /** + * Returns the TIC stream identifier. + * + * @return the TIC stream identifier + */ + public TICStreamIdentifier getIdentifier() { + return this.identifier; + } + + /** + * Returns the timeout value for the TIC stream. + * + * @return the timeout value in seconds + */ + public int getTimeout() { + return this.timeout; + } + + private void setTicMode(TICMode ticMode) { + checkTicMode(ticMode); + this.ticMode = ticMode; + } + + private void setIdentifier(TICStreamIdentifier identifier) { + checkIdentifier(identifier); + this.identifier = identifier; + } + + private void setTimeout(int timeout) { + checkTimeout(timeout); + this.timeout = timeout; + } + + private void checkTicMode(TICMode ticMode) { + if (ticMode == null) { + throw new IllegalArgumentException("TIC mode cannot be null"); + } + } + + private void checkIdentifier(TICStreamIdentifier identifier) { + if (identifier == null) { + throw new IllegalArgumentException("TIC stream identifier cannot be null"); + } + } + + private void checkTimeout(int timeout) { + if (timeout <= 0) { + throw new IllegalArgumentException("Timeout must be a positive integer"); + } + } +} diff --git a/src/main/java/tic/stream/configuration/TICStreamConfigurationLoader.java b/src/main/java/tic/stream/configuration/TICStreamConfigurationLoader.java new file mode 100644 index 0000000..fd7dbda --- /dev/null +++ b/src/main/java/tic/stream/configuration/TICStreamConfigurationLoader.java @@ -0,0 +1,82 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream.configuration; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import tic.frame.TICMode; +import tic.stream.identifier.SerialPortId; +import tic.stream.identifier.SerialPortName; +import tic.stream.identifier.TICStreamIdentifier; +import tic.stream.identifier.TICStreamIdentifierType; + +public final class TICStreamConfigurationLoader { + + private TICStreamConfigurationLoader() {} + + /** + * Load TIC stream configuration from a JSON file. + * + * @param filepath the path to the configuration file + * @return the loaded TIC stream configuration + * @throws IllegalStateException if there is an error loading or parsing the configuration + */ + public static TICStreamConfiguration load(String filepath) { + try { + String json = readFileAsString(filepath); + JSONObject rootObject = new JSONObject(new JSONTokener(json)); + + TICMode ticMode = parseTicMode(rootObject); + int timeout = parseTimeout(rootObject); + TICStreamIdentifier identifier = parseIdentifier(rootObject); + + return new TICStreamConfiguration(ticMode, identifier, timeout); + } catch (NullPointerException | IOException | JSONException exception) { + throw new IllegalStateException("Unable to load TIC stream configuration", exception); + } + } + + private static final String readFileAsString(String filepath) throws IOException { + Path path = Paths.get(filepath); + return new String(Files.readAllBytes(path)); + } + + private static TICMode parseTicMode(JSONObject root) { + String modeValue = root.optString("ticMode", TICStreamConfiguration.DEFAULT_TIC_MODE.name()); + return TICMode.valueOf(modeValue.toUpperCase()); + } + + private static int parseTimeout(JSONObject root) { + int timeout = root.optInt("timeout", TICStreamConfiguration.DEFAULT_TIMEOUT); + if (timeout <= 0) { + throw new IllegalArgumentException("Timeout must be a positive integer"); + } + return timeout; + } + + private static TICStreamIdentifier parseIdentifier(JSONObject root) { + JSONObject identifierObject = root.optJSONObject("identifier"); + if (identifierObject == null) { + throw new IllegalArgumentException("TIC stream identifier is required"); + } + String typeValue = identifierObject.optString("type"); + String value = identifierObject.optString("value"); + if (typeValue.equalsIgnoreCase(TICStreamIdentifierType.PORT_NAME.name())) { + return new TICStreamIdentifier(new SerialPortName(value)); + } else if (typeValue.equalsIgnoreCase(TICStreamIdentifierType.PORT_ID.name())) { + return new TICStreamIdentifier(new SerialPortId(value)); + } else { + throw new IllegalArgumentException("Invalid TIC stream identifier type: " + typeValue); + } + } +} diff --git a/src/main/java/tic/stream/identifier/SerialPortId.java b/src/main/java/tic/stream/identifier/SerialPortId.java new file mode 100644 index 0000000..0a2f4f3 --- /dev/null +++ b/src/main/java/tic/stream/identifier/SerialPortId.java @@ -0,0 +1,30 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream.identifier; + +/** + * Class representing a serial port identifier by its ID. + */ +public class SerialPortId { + private String portId; + + public SerialPortId(String portId) { + this.setPortId(portId); + } + + public String getPortId() { + return portId; + } + + private void setPortId(String portId) { + if (portId == null) { + throw new IllegalArgumentException("Port ID cannot be null"); + } + this.portId = portId; + } +} diff --git a/src/main/java/tic/stream/identifier/SerialPortName.java b/src/main/java/tic/stream/identifier/SerialPortName.java new file mode 100644 index 0000000..d6b4dad --- /dev/null +++ b/src/main/java/tic/stream/identifier/SerialPortName.java @@ -0,0 +1,30 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream.identifier; + +/** + * Class representing a serial port identified by its name. + */ +public class SerialPortName { + private String portName; + + public SerialPortName(String portName) { + this.setPortName(portName); + } + + public String getPortName() { + return portName; + } + + private void setPortName(String portName) { + if (portName == null) { + throw new IllegalArgumentException("Port name cannot be null"); + } + this.portName = portName; + } +} diff --git a/src/main/java/tic/stream/identifier/TICStreamIdentifier.java b/src/main/java/tic/stream/identifier/TICStreamIdentifier.java new file mode 100644 index 0000000..cb7a6f6 --- /dev/null +++ b/src/main/java/tic/stream/identifier/TICStreamIdentifier.java @@ -0,0 +1,57 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream.identifier; + +/** Class representing a TIC stream identifier, which can be either by name or by ID. */ +public class TICStreamIdentifier { + private SerialPortName portName; + private SerialPortId portId; + private TICStreamIdentifierType type; + + public TICStreamIdentifier(SerialPortName portName) { + this.type = TICStreamIdentifierType.PORT_NAME; + this.setPortName(portName); + } + + public TICStreamIdentifier(SerialPortId portId) { + this.type = TICStreamIdentifierType.PORT_ID; + this.setPortId(portId); + } + + public String getPortName() { + return this.portName.getPortName(); + } + + public String getPortId() { + return this.portId.getPortId(); + } + + public TICStreamIdentifierType getType() { + return this.type; + } + + private void setPortName(SerialPortName portName) { + if (this.type == TICStreamIdentifierType.PORT_ID) { + throw new IllegalStateException("Cannot set port name when identifier type is PORT_ID"); + } + if (portName == null) { + throw new IllegalArgumentException("Port name cannot be null"); + } + this.portName = portName; + } + + private void setPortId(SerialPortId portId) { + if (this.type == TICStreamIdentifierType.PORT_NAME) { + throw new IllegalStateException("Cannot set port ID when identifier type is PORT_NAME"); + } + if (portId == null) { + throw new IllegalArgumentException("Port ID cannot be null"); + } + this.portId = portId; + } +} diff --git a/src/main/java/tic/stream/identifier/TICStreamIdentifierType.java b/src/main/java/tic/stream/identifier/TICStreamIdentifierType.java new file mode 100644 index 0000000..a0ce232 --- /dev/null +++ b/src/main/java/tic/stream/identifier/TICStreamIdentifierType.java @@ -0,0 +1,22 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream.identifier; + +/** + * Enumeration representing the types of TIC stream identifiers. + * + *

      This enum defines the different ways to identify a serial port, such as by its name or ID. + * + * @author Enedis Smarties team + */ +public enum TICStreamIdentifierType { + /** Identifier type for the serial port name. */ + PORT_NAME, + /** Identifier type for the serial port ID. */ + PORT_ID; +} diff --git a/src/main/java/tic/util/ValueChecker.java b/src/main/java/tic/util/ValueChecker.java new file mode 100644 index 0000000..c0b0e0b --- /dev/null +++ b/src/main/java/tic/util/ValueChecker.java @@ -0,0 +1,69 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util; + +/** + * Utility class for validating values such as numbers and strings. + * + *

      This class provides methods to validate numerical ranges and string properties, throwing + * exceptions for invalid inputs. It is useful for ensuring data integrity in various contexts. + * + *

      Common use cases include: + * + *

        + *
      • Validating numerical inputs against specified ranges + *
      • Checking for null or empty strings + *
      • Ensuring data consistency before processing + *
      + * + * @author Enedis Smarties team + */ +public class ValueChecker { + + public static Number validateNumber(Number value, String key, boolean allowNull) { + if (value == null) { + if (!allowNull) { + throw new IllegalArgumentException("Value '" + key + "' cannot be null"); + } + return null; + } + return value; + } + + public static Number validateNumber( + Number value, String key, int min, int max, boolean allowNull) { + if (value == null) { + if (!allowNull) { + throw new IllegalArgumentException("Value '" + key + "' cannot be null"); + } + return null; + } + if (value.doubleValue() < min || value.doubleValue() > max) { + throw new IllegalArgumentException( + "Value '" + key + "' must be in range [" + min + "-" + max + "], but got: " + value); + } + return value; + } + + public static String validateString( + String value, String key, boolean allowNull, boolean allowEmpty) { + if (value == null && allowNull) { + return null; + } + + if (value == null && !allowNull) { + throw new IllegalArgumentException("Value '" + key + "' cannot be null"); + } + + if (!allowEmpty && value.isEmpty()) { + throw new IllegalArgumentException("Value '" + key + "' cannot be empty"); + } + + return value; + } +} diff --git a/src/main/java/tic/util/codec/Codec.java b/src/main/java/tic/util/codec/Codec.java new file mode 100644 index 0000000..048010f --- /dev/null +++ b/src/main/java/tic/util/codec/Codec.java @@ -0,0 +1,14 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.codec; + +public interface Codec { + T2 encode(T1 object) throws Exception; + + T1 decode(T2 object) throws Exception; +} diff --git a/src/main/java/tic/util/codec/JsonArrayCodec.java b/src/main/java/tic/util/codec/JsonArrayCodec.java new file mode 100644 index 0000000..fe6cd73 --- /dev/null +++ b/src/main/java/tic/util/codec/JsonArrayCodec.java @@ -0,0 +1,14 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.codec; + +public interface JsonArrayCodec { + Object encodeToJsonArray(T object) throws Exception; + + T decodeFromJsonArray(Object jsonArray) throws Exception; +} diff --git a/src/main/java/tic/util/codec/JsonObjectCodec.java b/src/main/java/tic/util/codec/JsonObjectCodec.java new file mode 100644 index 0000000..605f726 --- /dev/null +++ b/src/main/java/tic/util/codec/JsonObjectCodec.java @@ -0,0 +1,14 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.codec; + +public interface JsonObjectCodec { + Object encodeToJsonObject(T object) throws Exception; + + T decodeFromJsonObject(Object jsonObject) throws Exception; +} diff --git a/src/main/java/tic/util/codec/JsonStringCodec.java b/src/main/java/tic/util/codec/JsonStringCodec.java new file mode 100644 index 0000000..3f9c4be --- /dev/null +++ b/src/main/java/tic/util/codec/JsonStringCodec.java @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.codec; + +public interface JsonStringCodec { + int DEFAULT_INDENT = 2; + + default String encodeToJsonString(T object) throws Exception { + return encodeToJsonString(object, DEFAULT_INDENT); + } + + default T decodeFromJsonString(String jsonString) throws Exception { + return decodeFromJsonString(jsonString, DEFAULT_INDENT); + } + + String encodeToJsonString(T object, int indentFactor) throws Exception; + + T decodeFromJsonString(String jsonString, int indentFactor) throws Exception; +} diff --git a/src/main/java/tic/util/message/Event.java b/src/main/java/tic/util/message/Event.java new file mode 100644 index 0000000..87c5692 --- /dev/null +++ b/src/main/java/tic/util/message/Event.java @@ -0,0 +1,61 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message; + +import java.time.LocalDateTime; + +/** + * Abstract base class for event messages. + * + *

      This class represents event messages. It provides support for event-specific fields such as + * date/time and enforces the accepted message type. Subclasses can define additional event data and + * behavior. + * + *

      Common use cases include: + * + *

        + *
      • Representing system or application events + *
      • Handling event notifications in the message pipeline + *
      • Extending for custom event types + *
      + * + * @author Enedis Smarties team + * @see Message + */ +public abstract class Event extends Message { + private LocalDateTime dateTime; + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param dateTime + */ + public Event(String name, LocalDateTime dateTime) { + super(MessageType.EVENT, name); + this.setDateTime(dateTime); + } + + /** + * Get date time + * + * @return the date time + */ + public LocalDateTime getDateTime() { + return this.dateTime; + } + + /** + * Set date time + * + * @param dateTime + */ + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } +} diff --git a/src/main/java/tic/util/message/Message.java b/src/main/java/tic/util/message/Message.java new file mode 100644 index 0000000..3384b96 --- /dev/null +++ b/src/main/java/tic/util/message/Message.java @@ -0,0 +1,76 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message; + +/** + * Base class for all messages. + * + *

      This class provides the core structure for messages, including type and name fields. It can be + * extended for specific message types such as requests, responses, and events. + * + *

      Common use cases include: + * + *

        + *
      • Representing generic messages in the communication pipeline + *
      • Providing a base for request, response, and event messages + *
      • Supporting extensible message structures + *
      + * + * @author Enedis Smarties team + */ +public class Message { + private MessageType type; + private String name; + + /** + * Constructor setting parameters to specific values + * + * @param type + * @param name + */ + public Message(MessageType type, String name) { + this.setType(type); + this.setName(name); + } + + /** + * Get type + * + * @return the type + */ + public MessageType getType() { + return this.type; + } + + /** + * Get name + * + * @return the name + */ + public String getName() { + return this.name; + } + + /** + * Set type + * + * @param type + */ + public void setType(MessageType type) { + this.type = type; + } + + /** + * Set name + * + * @param name + */ + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/enedis/lab/util/message/MessageType.java b/src/main/java/tic/util/message/MessageType.java similarity index 95% rename from src/main/java/enedis/lab/util/message/MessageType.java rename to src/main/java/tic/util/message/MessageType.java index 5d92e22..d1f8831 100644 --- a/src/main/java/enedis/lab/util/message/MessageType.java +++ b/src/main/java/tic/util/message/MessageType.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.message; +package tic.util.message; /** * Enumeration of message types. diff --git a/src/main/java/tic/util/message/Request.java b/src/main/java/tic/util/message/Request.java new file mode 100644 index 0000000..ddfc154 --- /dev/null +++ b/src/main/java/tic/util/message/Request.java @@ -0,0 +1,37 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message; + +/** + * Abstract base class for request messages. + * + *

      This class represents request messages. It enforces the accepted message type and provides + * support for request-specific fields. Subclasses can define additional request data and behavior. + * + *

      Common use cases include: + * + *

        + *
      • Representing client requests for data or actions + *
      • Handling request validation and processing + *
      • Extending for custom request types + *
      + * + * @author Enedis Smarties team + * @see Message + */ +public abstract class Request extends Message { + + /** + * Constructor setting parameters to specific values + * + * @param name + */ + public Request(String name) { + super(MessageType.REQUEST, name); + } +} diff --git a/src/main/java/tic/util/message/Response.java b/src/main/java/tic/util/message/Response.java new file mode 100644 index 0000000..9abd34b --- /dev/null +++ b/src/main/java/tic/util/message/Response.java @@ -0,0 +1,105 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message; + +import java.time.LocalDateTime; + +/** + * Abstract base class for response messages. + * + *

      This class represents response messages. It provides support for response-specific fields such + * as date/time, error code, and error message, and enforces the accepted message type. Subclasses + * can define additional response data and behavior. + * + *

      Common use cases include: + * + *

        + *
      • Representing server responses to client requests + *
      • Handling error reporting and status information + *
      • Extending for custom response types + *
      + * + * @author Enedis Smarties team + * @see Message + */ +public abstract class Response extends Message { + private LocalDateTime dateTime; + private Number errorCode; + private String errorMessage; + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param dateTime + * @param errorCode + * @param errorMessage + */ + public Response( + String name, LocalDateTime dateTime, Number errorCode, String errorMessage) { + super(MessageType.RESPONSE, name); + + this.setDateTime(dateTime); + this.setErrorCode(errorCode); + this.setErrorMessage(errorMessage); + } + + /** + * Get date time + * + * @return the date time + */ + public LocalDateTime getDateTime() { + return this.dateTime; + } + + /** + * Get error code + * + * @return the error code + */ + public Number getErrorCode() { + return this.errorCode; + } + + /** + * Get error message + * + * @return the error message + */ + public String getErrorMessage() { + return this.errorMessage; + } + + /** + * Set date time + * + * @param dateTime + */ + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + /** + * Set error code + * + * @param errorCode + */ + public void setErrorCode(Number errorCode) { + this.errorCode = errorCode; + } + + /** + * Set error message + * + * @param errorMessage + */ + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/src/main/java/tic/util/message/ResponseWithData.java b/src/main/java/tic/util/message/ResponseWithData.java new file mode 100644 index 0000000..b393331 --- /dev/null +++ b/src/main/java/tic/util/message/ResponseWithData.java @@ -0,0 +1,42 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message; + +import java.time.LocalDateTime; + +public abstract class ResponseWithData extends Response { + + /** + * Constructor setting parameters to specific values + * + * @param name + * @param dateTime + * @param errorCode + * @param errorMessage + * @param data + */ + public ResponseWithData( + String name, LocalDateTime dateTime, Number errorCode, String errorMessage, T data) { + super(name, dateTime, errorCode, errorMessage); + this.setData(data); + } + + /** + * Get data + * + * @return the data + */ + public abstract T getData(); + + /** + * Set data + * + * @param data + */ + public abstract void setData(T data); +} diff --git a/src/main/java/tic/util/message/codec/EventJsonEncoder.java b/src/main/java/tic/util/message/codec/EventJsonEncoder.java new file mode 100644 index 0000000..b6cb118 --- /dev/null +++ b/src/main/java/tic/util/message/codec/EventJsonEncoder.java @@ -0,0 +1,63 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message.codec; + +import org.json.JSONObject; +import tic.core.TICCoreError; +import tic.core.TICCoreFrame; +import tic.core.codec.TICIdentifierCodec; +import tic.frame.codec.TICFrameSummarizedCodec; +import tic.service.message.EventOnError; +import tic.service.message.EventOnTICData; +import tic.util.message.Event; + +public class EventJsonEncoder { + private static TICIdentifierCodec ticIdentifierCodec = TICIdentifierCodec.getInstance(); + private static TICFrameSummarizedCodec ticFrameSummarizedCodec = TICFrameSummarizedCodec.getInstance(); + + private EventJsonEncoder() {} + + public static JSONObject encode(Event message, JSONObject jsonMessage) { + jsonMessage.put("datetime", message.getDateTime().toString()); + + switch (message.getName()) { + case EventOnError.NAME: + return encodeEventOnError((EventOnError) message, jsonMessage); + case EventOnTICData.NAME: + return encodeEventOnTICData((EventOnTICData) message, jsonMessage); + default: + return jsonMessage; + } + } + + private static JSONObject encodeEventOnError(EventOnError message, JSONObject jsonMessage) { + TICCoreError error = message.getData(); + jsonMessage.put("identifier", ticIdentifierCodec.encodeToJsonObject(error.getIdentifier())); + jsonMessage.put("errorCode", error.getErrorCode()); + jsonMessage.put("errorMessage", error.getErrorMessage()); + try { + jsonMessage.put("frame", ticFrameSummarizedCodec.encodeToJsonObject(error.getFrame())); + } catch (Exception e) { + jsonMessage.put("frame", JSONObject.NULL); + } + return jsonMessage; + } + + private static JSONObject encodeEventOnTICData(EventOnTICData message, JSONObject jsonMessage) { + TICCoreFrame frame = message.getData(); + jsonMessage.put("identifier", ticIdentifierCodec.encodeToJsonObject(frame.getIdentifier())); + jsonMessage.put("mode", frame.getMode().toString()); + jsonMessage.put("captureDateTime", frame.getCaptureDateTime().toString()); + try { + jsonMessage.put("frame", ticFrameSummarizedCodec.encodeToJsonObject(frame.getFrame())); + } catch (Exception e) { + jsonMessage.put("frame", JSONObject.NULL); + } + return jsonMessage; + } +} diff --git a/src/main/java/tic/util/message/codec/MessageJsonCodec.java b/src/main/java/tic/util/message/codec/MessageJsonCodec.java new file mode 100644 index 0000000..13e3781 --- /dev/null +++ b/src/main/java/tic/util/message/codec/MessageJsonCodec.java @@ -0,0 +1,100 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message.codec; + +import org.json.JSONObject; +import tic.util.codec.JsonObjectCodec; +import tic.util.codec.JsonStringCodec; +import tic.util.message.Event; +import tic.util.message.Message; +import tic.util.message.Request; +import tic.util.message.Response; +import tic.util.message.exception.MessageException; +import tic.util.message.exception.MessageInvalidFormatException; +import tic.util.message.exception.UnsupportedMessageException; + +/** + * Codec for encoding and decoding Message objects to and from JSON format. + * + *

      This codec supports serialization of various Message types, including Event, Request, and + * Response, into JSON objects and strings. It also handles deserialization from JSON back into the + * appropriate Message subclass based on the message name and type. + * + *

      Usage examples: + * + *

        + *
      • Encoding a Message to a JSON string for transmission or storage + *
      • Decoding a JSON string received from a network or file into a Message object + *
      • Handling different Message types with specific encoding/decoding logic + *
      + * + * @author Enedis Smarties team + */ +public class MessageJsonCodec implements JsonObjectCodec, JsonStringCodec { + + private static MessageJsonCodec instance = null; + + public static MessageJsonCodec getInstance() { + if (instance == null) { + return new MessageJsonCodec(); + } + return instance; + } + + private MessageJsonCodec() {} + + @Override + public String encodeToJsonString(Message object, int indentFactor) throws Exception { + JSONObject jsonObject = encodeToJsonObject(object); + return jsonObject.toString(indentFactor); + } + + @Override + public Message decodeFromJsonString(String jsonString, int indentFactor) throws Exception { + if (jsonString == null || jsonString.isEmpty()) { + throw new MessageInvalidFormatException("Input JSON string is null or empty"); + } + + JSONObject jsonObject = null; + + try { + jsonObject = new JSONObject(jsonString); + } catch (Exception e) { + throw new MessageInvalidFormatException("Input is not a valid JSON string", e); + } + + return decodeFromJsonObject(jsonObject); + } + + @Override + public JSONObject encodeToJsonObject(Message message) { + JSONObject jsonMessage = new JSONObject(); + jsonMessage.put("name", message.getName()); + jsonMessage.put("type", message.getType().toString()); + + switch (message.getType()) { + case EVENT: + return EventJsonEncoder.encode((Event) message, jsonMessage); + case REQUEST: + return RequestJsonEncoder.encode((Request) message, jsonMessage); + case RESPONSE: + return ResponseJsonEncoder.encode((Response) message, jsonMessage); + default: + return jsonMessage; + } + } + + @Override + public Message decodeFromJsonObject(Object object) throws MessageException { + try { + return RequestJsonDecoder.decode(object); + } catch (Exception e) { + throw new UnsupportedMessageException("Failed to decode Message from JSON object", e); + } + } +} diff --git a/src/main/java/tic/util/message/codec/RequestJsonDecoder.java b/src/main/java/tic/util/message/codec/RequestJsonDecoder.java new file mode 100644 index 0000000..9a5d3a2 --- /dev/null +++ b/src/main/java/tic/util/message/codec/RequestJsonDecoder.java @@ -0,0 +1,127 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message.codec; + +import java.util.Collections; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONObject; +import tic.core.TICIdentifier; +import tic.core.codec.TICIdentifierCodec; +import tic.service.message.RequestGetAvailableTICs; +import tic.service.message.RequestGetModemsInfo; +import tic.service.message.RequestReadTIC; +import tic.service.message.RequestSubscribeTIC; +import tic.service.message.RequestUnsubscribeTIC; +import tic.util.message.Message; +import tic.util.message.MessageType; +import tic.util.message.exception.MessageException; +import tic.util.message.exception.MessageInvalidContentException; +import tic.util.message.exception.MessageInvalidFormatException; +import tic.util.message.exception.MessageInvalidTypeException; +import tic.util.message.exception.MessageKeyNameDoesntExistException; + +public class RequestJsonDecoder { + + private RequestJsonDecoder() {} + + public static Message decode(Object object) throws Exception { + if (object == null || object instanceof JSONObject == false) { + throw new MessageInvalidFormatException("Input is not a valid JSON object"); + } + + JSONObject jsonObject = (JSONObject) object; + + if (!jsonObject.has("name") || !jsonObject.has("type")) { + throw new MessageInvalidContentException( + "JSON object missing required fields 'name' or 'type'"); + } + + String type = jsonObject.getString("type"); + if (!type.equals(MessageType.REQUEST.name())) { + throw new MessageInvalidTypeException("Invalid message type: " + type); + } + + Message message; + + String name = jsonObject.getString("name"); + switch (name) { + case RequestGetAvailableTICs.NAME: + message = decodeGetAvailableTICs(jsonObject); + break; + case RequestGetModemsInfo.NAME: + message = decodeGetModemsInfo(jsonObject); + break; + case RequestReadTIC.NAME: + message = decodeReadTIC(jsonObject); + break; + case RequestSubscribeTIC.NAME: + message = decodeSubscribeTIC(jsonObject); + break; + case RequestUnsubscribeTIC.NAME: + message = decodeUnsubscribeTIC(jsonObject); + break; + default: + throw new MessageKeyNameDoesntExistException("Unsupported request message name: " + name); + } + return message; + } + + private static Message decodeGetAvailableTICs(JSONObject jsonObject) { + return new RequestGetAvailableTICs(); + } + + private static Message decodeGetModemsInfo(JSONObject jsonObject) { + return new RequestGetModemsInfo(); + } + + private static Message decodeReadTIC(JSONObject jsonObject) throws MessageException { + if (!jsonObject.has("data")) { + throw new MessageInvalidContentException( + "JSON object missing required field 'data' for ReadTIC request"); + } + JSONObject dataObject = jsonObject.getJSONObject("data"); + return new RequestReadTIC(TICIdentifierCodec.getInstance().decodeFromJsonObject(dataObject)); + } + + private static Message decodeSubscribeTIC(JSONObject jsonObject) throws MessageException { + return new RequestSubscribeTIC(decodeOptionalTICIdentifierList(jsonObject)); + } + + private static Message decodeUnsubscribeTIC(JSONObject jsonObject) throws MessageException { + return new RequestUnsubscribeTIC(decodeOptionalTICIdentifierList(jsonObject)); + } + + private static List decodeOptionalTICIdentifierList(JSONObject jsonObject) + throws MessageException { + if (jsonObject == null || !jsonObject.has("data") || jsonObject.isNull("data")) { + return null; + } + + Object dataValue = jsonObject.opt("data"); + if (dataValue == null || dataValue == JSONObject.NULL) { + return null; + } + + try { + if (dataValue instanceof JSONArray) { + return TICIdentifierCodec.getInstance().decodeFromJsonArray(dataValue); + } + if (dataValue instanceof JSONObject) { + TICIdentifier identifier = TICIdentifierCodec.getInstance().decodeFromJsonObject(dataValue); + return Collections.singletonList(identifier); + } + } catch (IllegalArgumentException e) { + throw new MessageInvalidFormatException("Invalid 'data' field: " + e.getMessage(), e); + } + + throw new MessageInvalidFormatException( + "Invalid 'data' field: expected an array of TICIdentifier or a single TICIdentifier" + + " object"); + } +} diff --git a/src/main/java/tic/util/message/codec/RequestJsonEncoder.java b/src/main/java/tic/util/message/codec/RequestJsonEncoder.java new file mode 100644 index 0000000..68b27df --- /dev/null +++ b/src/main/java/tic/util/message/codec/RequestJsonEncoder.java @@ -0,0 +1,60 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message.codec; + +import org.json.JSONObject; +import tic.core.TICIdentifier; +import tic.core.codec.TICIdentifierCodec; +import tic.service.message.RequestGetAvailableTICs; +import tic.service.message.RequestGetModemsInfo; +import tic.service.message.RequestReadTIC; +import tic.service.message.RequestSubscribeTIC; +import tic.service.message.RequestUnsubscribeTIC; +import tic.util.message.Request; + +public class RequestJsonEncoder { + + private static TICIdentifierCodec ticIdentifierCodec = TICIdentifierCodec.getInstance(); + + private RequestJsonEncoder() {} + + public static JSONObject encode(Request message, JSONObject jsonMessage) { + switch (message.getName()) { + case RequestGetAvailableTICs.NAME: + return jsonMessage; + case RequestGetModemsInfo.NAME: + return jsonMessage; + case RequestReadTIC.NAME: + return encodeReadTICRequest((RequestReadTIC) message, jsonMessage); + case RequestSubscribeTIC.NAME: + return encodeSubscribeTICRequest((RequestSubscribeTIC) message, jsonMessage); + case RequestUnsubscribeTIC.NAME: + return encodeUnsubscribeTICRequest((RequestUnsubscribeTIC) message, jsonMessage); + default: + return jsonMessage; + } + } + + private static JSONObject encodeReadTICRequest(RequestReadTIC message, JSONObject jsonMessage) { + TICIdentifier identifier = message.getData(); + jsonMessage.put("data", ticIdentifierCodec.encodeToJsonObject(identifier)); + return jsonMessage; + } + + private static JSONObject encodeSubscribeTICRequest( + RequestSubscribeTIC message, JSONObject jsonMessage) { + jsonMessage.put("data", ticIdentifierCodec.encodeToJsonArray(message.getData())); + return jsonMessage; + } + + private static JSONObject encodeUnsubscribeTICRequest( + RequestUnsubscribeTIC message, JSONObject jsonMessage) { + jsonMessage.put("data", ticIdentifierCodec.encodeToJsonArray(message.getData())); + return jsonMessage; + } +} diff --git a/src/main/java/tic/util/message/codec/ResponseJsonEncoder.java b/src/main/java/tic/util/message/codec/ResponseJsonEncoder.java new file mode 100644 index 0000000..0aa87ec --- /dev/null +++ b/src/main/java/tic/util/message/codec/ResponseJsonEncoder.java @@ -0,0 +1,82 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message.codec; + +import org.json.JSONObject; +import tic.core.codec.TICCoreFrameCodec; +import tic.core.codec.TICIdentifierCodec; +import tic.io.modem.ModemJsonCodec; +import tic.service.message.ResponseGetAvailableTICs; +import tic.service.message.ResponseGetModemsInfo; +import tic.service.message.ResponseReadTIC; +import tic.service.message.ResponseSubscribeTIC; +import tic.service.message.ResponseUnsubscribeTIC; +import tic.util.message.Response; + +public class ResponseJsonEncoder { + + private static TICIdentifierCodec ticIdentifierCodec = TICIdentifierCodec.getInstance(); + + private ResponseJsonEncoder() {} + + public static JSONObject encode(Response message, JSONObject jsonMessage) { + jsonMessage.put("datetime", message.getDateTime().toString()); + jsonMessage.put("errorCode", message.getErrorCode()); + jsonMessage.put("errorMessage", message.getErrorMessage()); + + switch (message.getName()) { + case ResponseGetAvailableTICs.NAME: + if (message instanceof ResponseGetAvailableTICs) { + return encodeGetAvailableTICsResponse((ResponseGetAvailableTICs) message, jsonMessage); + } + return jsonMessage; + case ResponseGetModemsInfo.NAME: + if (message instanceof ResponseGetModemsInfo) { + return encodeGetModemsInfoResponse((ResponseGetModemsInfo) message, jsonMessage); + } + return jsonMessage; + case ResponseReadTIC.NAME: + if (message instanceof ResponseReadTIC) { + return encodeReadTICResponse((ResponseReadTIC) message, jsonMessage); + } + return jsonMessage; + case ResponseSubscribeTIC.NAME: + return jsonMessage; + case ResponseUnsubscribeTIC.NAME: + return jsonMessage; + default: + return jsonMessage; + } + } + + private static JSONObject encodeGetAvailableTICsResponse( + ResponseGetAvailableTICs message, JSONObject jsonMessage) { + jsonMessage.put("data", ticIdentifierCodec.encodeToJsonArray(message.getData())); + return jsonMessage; + } + + private static JSONObject encodeGetModemsInfoResponse( + ResponseGetModemsInfo message, JSONObject jsonMessage) { + try { + jsonMessage.put("data", ModemJsonCodec.getInstance().encodeToJsonArray(message.getData())); + } catch (Exception e) { + jsonMessage.put("data", JSONObject.NULL); + } + return jsonMessage; + } + + private static JSONObject encodeReadTICResponse(ResponseReadTIC message, JSONObject jsonMessage) { + try { + jsonMessage.put( + "data", TICCoreFrameCodec.getInstance().encodeToJsonObject(message.getData())); + } catch (Exception e) { + jsonMessage.put("data", JSONObject.NULL); + } + return jsonMessage; + } +} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageException.java b/src/main/java/tic/util/message/exception/MessageException.java similarity index 98% rename from src/main/java/enedis/lab/util/message/exception/MessageException.java rename to src/main/java/tic/util/message/exception/MessageException.java index 084896b..4561d2c 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageException.java +++ b/src/main/java/tic/util/message/exception/MessageException.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.message.exception; +package tic.util.message.exception; /** * Exception for signaling errors during message processing in the TIC2WebSocket framework. diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java b/src/main/java/tic/util/message/exception/MessageInvalidContentException.java similarity index 98% rename from src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java rename to src/main/java/tic/util/message/exception/MessageInvalidContentException.java index c56d1d9..f957cca 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java +++ b/src/main/java/tic/util/message/exception/MessageInvalidContentException.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.message.exception; +package tic.util.message.exception; /** * Exception indicating that a message contains invalid or unexpected content. diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java b/src/main/java/tic/util/message/exception/MessageInvalidFormatException.java similarity index 98% rename from src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java rename to src/main/java/tic/util/message/exception/MessageInvalidFormatException.java index e5aaad1..c87bf0b 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java +++ b/src/main/java/tic/util/message/exception/MessageInvalidFormatException.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.message.exception; +package tic.util.message.exception; /** * Exception indicating that a message has an invalid format. diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java b/src/main/java/tic/util/message/exception/MessageInvalidTypeException.java similarity index 98% rename from src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java rename to src/main/java/tic/util/message/exception/MessageInvalidTypeException.java index 69edef4..cdae0d3 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java +++ b/src/main/java/tic/util/message/exception/MessageInvalidTypeException.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.message.exception; +package tic.util.message.exception; /** * Exception indicating that a message has an invalid or unsupported type. diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java b/src/main/java/tic/util/message/exception/MessageKeyNameDoesntExistException.java similarity index 98% rename from src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java rename to src/main/java/tic/util/message/exception/MessageKeyNameDoesntExistException.java index 266e736..f005023 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java +++ b/src/main/java/tic/util/message/exception/MessageKeyNameDoesntExistException.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.message.exception; +package tic.util.message.exception; /** * Exception indicating that a required message key name does not exist. diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java b/src/main/java/tic/util/message/exception/MessageKeyTypeDoesntExistException.java similarity index 98% rename from src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java rename to src/main/java/tic/util/message/exception/MessageKeyTypeDoesntExistException.java index 6e20c8a..104d014 100644 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java +++ b/src/main/java/tic/util/message/exception/MessageKeyTypeDoesntExistException.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.message.exception; +package tic.util.message.exception; /** * Exception indicating that a required message key type does not exist. diff --git a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java b/src/main/java/tic/util/message/exception/UnsupportedMessageException.java similarity index 98% rename from src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java rename to src/main/java/tic/util/message/exception/UnsupportedMessageException.java index f9330dc..1cfd985 100644 --- a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java +++ b/src/main/java/tic/util/message/exception/UnsupportedMessageException.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.message.exception; +package tic.util.message.exception; /** * Exception indicating that a message type or operation is unsupported. diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifier.java b/src/main/java/tic/util/task/FilteredNotifier.java similarity index 98% rename from src/main/java/enedis/lab/util/task/FilteredNotifier.java rename to src/main/java/tic/util/task/FilteredNotifier.java index 2124192..30422a7 100644 --- a/src/main/java/enedis/lab/util/task/FilteredNotifier.java +++ b/src/main/java/tic/util/task/FilteredNotifier.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.task; +package tic.util.task; import java.util.Collection; import java.util.function.Predicate; @@ -26,7 +26,6 @@ * * @param the filter type * @param the subscriber type - * @author Enedis Smarties team */ public interface FilteredNotifier extends Notifier { /** diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java b/src/main/java/tic/util/task/FilteredNotifierBase.java similarity index 99% rename from src/main/java/enedis/lab/util/task/FilteredNotifierBase.java rename to src/main/java/tic/util/task/FilteredNotifierBase.java index 1b99bbd..8c6670c 100644 --- a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java +++ b/src/main/java/tic/util/task/FilteredNotifierBase.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.task; +package tic.util.task; import java.util.Collection; import java.util.HashSet; @@ -33,7 +33,6 @@ * * @param the filter type * @param the subscriber type - * @author Enedis Smarties team */ public class FilteredNotifierBase implements FilteredNotifier { protected Map> subscribersFiltered; diff --git a/src/main/java/enedis/lab/util/task/Notifier.java b/src/main/java/tic/util/task/Notifier.java similarity index 95% rename from src/main/java/enedis/lab/util/task/Notifier.java rename to src/main/java/tic/util/task/Notifier.java index c522a80..21e3894 100644 --- a/src/main/java/enedis/lab/util/task/Notifier.java +++ b/src/main/java/tic/util/task/Notifier.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.task; +package tic.util.task; import java.util.Collection; @@ -25,7 +25,6 @@ *
    * * @param the subscriber type - * @author Enedis Smarties team */ public interface Notifier { /** diff --git a/src/main/java/enedis/lab/util/task/NotifierBase.java b/src/main/java/tic/util/task/NotifierBase.java similarity index 96% rename from src/main/java/enedis/lab/util/task/NotifierBase.java rename to src/main/java/tic/util/task/NotifierBase.java index f41a5de..1a761ed 100644 --- a/src/main/java/enedis/lab/util/task/NotifierBase.java +++ b/src/main/java/tic/util/task/NotifierBase.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.task; +package tic.util.task; import java.util.Collection; import java.util.Set; @@ -26,7 +26,6 @@ * * * @param the subscriber type - * @author Enedis Smarties team */ public class NotifierBase implements Notifier { protected Set subscribers; diff --git a/src/main/java/enedis/lab/util/task/Subscriber.java b/src/main/java/tic/util/task/Subscriber.java similarity index 64% rename from src/main/java/enedis/lab/util/task/Subscriber.java rename to src/main/java/tic/util/task/Subscriber.java index b2265eb..0aa0af2 100644 --- a/src/main/java/enedis/lab/util/task/Subscriber.java +++ b/src/main/java/tic/util/task/Subscriber.java @@ -5,22 +5,12 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.task; +package tic.util.task; /** * Interface representing a generic event subscriber. * *

    This interface is used as a contract for objects that can receive notifications or events from * a notifier. It is commonly used in observer and event-driven patterns. - * - *

    Common use cases include: - * - *

      - *
    • Receiving notifications from event sources - *
    • Implementing observer patterns - *
    • Extending for custom event handling logic - *
    - * - * @author Enedis Smarties team */ public interface Subscriber {} diff --git a/src/main/java/enedis/lab/util/task/Task.java b/src/main/java/tic/util/task/Task.java similarity index 74% rename from src/main/java/enedis/lab/util/task/Task.java rename to src/main/java/tic/util/task/Task.java index 6549092..fe592d7 100644 --- a/src/main/java/enedis/lab/util/task/Task.java +++ b/src/main/java/tic/util/task/Task.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.task; +package tic.util.task; /** * Interface representing a generic asynchronous task. @@ -13,16 +13,6 @@ *

    This interface defines the contract for executing tasks, typically in a concurrent or * event-driven environment. Implementations may represent background jobs, scheduled actions, or * event handlers. - * - *

    Common use cases include: - * - *

      - *
    • Running background operations - *
    • Scheduling periodic or delayed actions - *
    • Handling events or notifications - *
    - * - * @author Enedis Smarties team */ public interface Task { /** Start task */ diff --git a/src/main/java/enedis/lab/util/task/TaskBase.java b/src/main/java/tic/util/task/TaskBase.java similarity index 91% rename from src/main/java/enedis/lab/util/task/TaskBase.java rename to src/main/java/tic/util/task/TaskBase.java index 9ddfdf4..ca7417c 100644 --- a/src/main/java/enedis/lab/util/task/TaskBase.java +++ b/src/main/java/tic/util/task/TaskBase.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.task; +package tic.util.task; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.logging.log4j.LogManager; @@ -16,16 +16,6 @@ * *

    This class provides a foundation for implementing tasks that can be executed asynchronously, * typically in concurrent or event-driven environments. - * - *

    Common use cases include: - * - *

      - *
    • Running background operations - *
    • Scheduling periodic or delayed actions - *
    • Extending for custom task logic - *
    - * - * @author Enedis Smarties team */ public abstract class TaskBase implements Task, Runnable { private AtomicBoolean stopRequired; diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodic.java b/src/main/java/tic/util/task/TaskPeriodic.java similarity index 87% rename from src/main/java/enedis/lab/util/task/TaskPeriodic.java rename to src/main/java/tic/util/task/TaskPeriodic.java index 29e0f52..5431365 100644 --- a/src/main/java/enedis/lab/util/task/TaskPeriodic.java +++ b/src/main/java/tic/util/task/TaskPeriodic.java @@ -5,26 +5,16 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.task; +package tic.util.task; -import enedis.lab.util.time.Time; import java.util.concurrent.atomic.AtomicLong; +import tic.util.time.Time; /** * Abstract class representing a generic periodic asynchronous task. * *

    This class provides functionality for executing tasks at regular intervals, typically used for * background operations or scheduled actions. - * - *

    Common use cases include: - * - *

      - *
    • Running periodic background jobs - *
    • Scheduling recurring actions - *
    • Extending for custom periodic logic - *
    - * - * @author Enedis Smarties team */ public abstract class TaskPeriodic extends TaskBase { /** Default period (in milliseconds) used to execute process */ diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java b/src/main/java/tic/util/task/TaskPeriodicWithSubscribers.java similarity index 82% rename from src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java rename to src/main/java/tic/util/task/TaskPeriodicWithSubscribers.java index b1d5e50..0446c26 100644 --- a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java +++ b/src/main/java/tic/util/task/TaskPeriodicWithSubscribers.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.task; +package tic.util.task; import java.util.Collection; @@ -15,16 +15,7 @@ *

    This class provides functionality for executing tasks at regular intervals and notifying * registered subscribers of events or results. * - *

    Common use cases include: - * - *

      - *
    • Running periodic background jobs with notifications - *
    • Scheduling recurring actions and informing listeners - *
    • Extending for custom periodic and notification logic - *
    - * * @param the subscriber type - * @author Enedis Smarties team */ public abstract class TaskPeriodicWithSubscribers extends TaskPeriodic implements Notifier { diff --git a/src/main/java/enedis/lab/util/time/Time.java b/src/main/java/tic/util/time/Time.java similarity index 99% rename from src/main/java/enedis/lab/util/time/Time.java rename to src/main/java/tic/util/time/Time.java index d92b725..3a8e848 100644 --- a/src/main/java/enedis/lab/util/time/Time.java +++ b/src/main/java/tic/util/time/Time.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.util.time; +package tic.util.time; import java.text.DateFormat; import java.text.SimpleDateFormat; diff --git a/src/main/resources/config/TIC2WebSocketConfiguration.json.license b/src/main/resources/config/TIC2WebSocketConfiguration.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/main/resources/config/TIC2WebSocketConfiguration.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/main/resources/config/TICStreamConfiguration.json b/src/main/resources/config/TICStreamConfiguration.json new file mode 100644 index 0000000..6285b65 --- /dev/null +++ b/src/main/resources/config/TICStreamConfiguration.json @@ -0,0 +1,8 @@ +{ + "ticMode": "AUTO", + "timeout": 10, + "identifier": { + "type": "PORT_NAME", + "value": "COM1" + } +} diff --git a/src/main/resources/log4j2-debug.xml b/src/main/resources/log4j2-debug.xml index 8ad49b1..2a7217c 100644 --- a/src/main/resources/log4j2-debug.xml +++ b/src/main/resources/log4j2-debug.xml @@ -10,36 +10,36 @@ SPDX-License-Identifier: Apache-2.0 - ${sys:logDir:-var/log} - ${sys:logFile:-TIC2WebSocket} + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} - + - + - - - - - - + + + + + + - - - - + + + + - + - + \ No newline at end of file diff --git a/src/main/resources/log4j2-error.xml b/src/main/resources/log4j2-error.xml index 6b7a9c5..574f2f2 100644 --- a/src/main/resources/log4j2-error.xml +++ b/src/main/resources/log4j2-error.xml @@ -10,36 +10,36 @@ SPDX-License-Identifier: Apache-2.0 - ${sys:logDir:-var/log} - ${sys:logFile:-TIC2WebSocket} + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} - + - + - - - - - - + + + + + + - - - - + + + + - + - + \ No newline at end of file diff --git a/src/main/resources/log4j2-fatal.xml b/src/main/resources/log4j2-fatal.xml index 6a936cd..544e770 100644 --- a/src/main/resources/log4j2-fatal.xml +++ b/src/main/resources/log4j2-fatal.xml @@ -10,36 +10,36 @@ SPDX-License-Identifier: Apache-2.0 - ${sys:logDir:-var/log} - ${sys:logFile:-TIC2WebSocket} + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} - + - + - - - - - - + + + + + + - - - - + + + + - + - + \ No newline at end of file diff --git a/src/main/resources/log4j2-info.xml b/src/main/resources/log4j2-info.xml index 8a0f971..1fcf958 100644 --- a/src/main/resources/log4j2-info.xml +++ b/src/main/resources/log4j2-info.xml @@ -10,36 +10,36 @@ SPDX-License-Identifier: Apache-2.0 - ${sys:logDir:-var/log} - ${sys:logFile:-TIC2WebSocket} + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} - + - + - - - - - - + + + + + + - - - - + + + + - + - + \ No newline at end of file diff --git a/src/main/resources/log4j2-off.xml b/src/main/resources/log4j2-off.xml index 539ef56..54b76f7 100644 --- a/src/main/resources/log4j2-off.xml +++ b/src/main/resources/log4j2-off.xml @@ -10,36 +10,36 @@ SPDX-License-Identifier: Apache-2.0 - ${sys:logDir:-var/log} - ${sys:logFile:-TIC2WebSocket} + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} - + - + - - - - - - + + + + + + - - - - + + + + - + - + \ No newline at end of file diff --git a/src/main/resources/log4j2-trace.xml b/src/main/resources/log4j2-trace.xml index f0324fe..bcbfd20 100644 --- a/src/main/resources/log4j2-trace.xml +++ b/src/main/resources/log4j2-trace.xml @@ -10,36 +10,36 @@ SPDX-License-Identifier: Apache-2.0 - ${sys:logDir:-var/log} - ${sys:logFile:-TIC2WebSocket} + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} - + - + - - - - - - + + + + + + - - - - + + + + - + - + \ No newline at end of file diff --git a/src/main/resources/log4j2-warn.xml b/src/main/resources/log4j2-warn.xml index 8d42e66..7643d18 100644 --- a/src/main/resources/log4j2-warn.xml +++ b/src/main/resources/log4j2-warn.xml @@ -10,36 +10,36 @@ SPDX-License-Identifier: Apache-2.0 - ${sys:logDir:-var/log} - ${sys:logFile:-TIC2WebSocket} + ${sys:logDir:-var/log} + ${sys:logFile:-TIC2WebSocket} - + - + - - - - - - + + + + + + - - - - + + + + - + - + \ No newline at end of file diff --git a/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java b/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java deleted file mode 100644 index a74af07..0000000 --- a/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import enedis.lab.io.PortFinderMock; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; - -@SuppressWarnings("javadoc") -public class TICPortFinderMock extends PortFinderMock implements TICPortFinder { - public DataList nativeDescriptorList = new DataArrayList(); - - public TICPortFinderMock() { - super(); - } - - public TICPortFinderMock(TICPortDescriptor... descriptors) { - this.setDescriptors(DataArrayList.asList(descriptors)); - } - - @Override - public TICPortDescriptor findNative(String portName) { - for (TICPortDescriptor descriptor : this.nativeDescriptorList) { - if (descriptor.getPortName() == null && portName == null) { - return descriptor; - } - if (descriptor.getPortName().equals(portName)) { - return descriptor; - } - } - - return null; - } -} diff --git a/src/test/java/enedis/tic/core/TICIdentifierTest.java b/src/test/java/enedis/tic/core/TICIdentifierTest.java deleted file mode 100644 index a601433..0000000 --- a/src/test/java/enedis/tic/core/TICIdentifierTest.java +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.types.DataDictionaryException; -import org.junit.Assert; -import org.junit.Test; - -public class TICIdentifierTest { - @Test - public void test_constructor_full() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier("{1-1}", "COM3", "021976551632"); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - Assert.assertEquals("COM3", identifier.getPortName()); - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlyPortId() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - Assert.assertNull(identifier.getPortName()); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlyPortName() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, "COM3", null); - - Assert.assertNull(identifier.getPortId()); - Assert.assertEquals("COM3", identifier.getPortName()); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlySerialNumber() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertNull(identifier.getPortId()); - Assert.assertNull(identifier.getPortName()); - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - } - - @Test(expected = DataDictionaryException.class) - public void test_constructor_empty() throws DataDictionaryException { - new TICIdentifier(null, null, null); - } - - @Test - public void test_setPortId_null_notEmpty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, "021976551632"); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - identifier.setPortId(null); - Assert.assertNull(identifier.getPortId()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setPortId_null_empty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - identifier.setPortId(null); - } - - @Test - public void test_setPortName_null_notEmpty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); - - Assert.assertEquals("COM3", identifier.getPortName()); - identifier.setPortName(null); - Assert.assertNull(identifier.getPortName()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setPortName_null_empty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, "COM3", null); - - Assert.assertEquals("COM3", identifier.getPortName()); - identifier.setPortName(null); - } - - @Test - public void test_setSerialNumber_null_notEmpty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); - - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - identifier.setSerialNumber(null); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setSerialNumber_null_empty() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - identifier.setSerialNumber(null); - } - - @Test - public void test_matches_null() throws DataDictionaryException { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertFalse(identifier.matches(null)); - } - - @Test - public void test_matches_same_serialNumber() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM4", "021976551632"); - - Assert.assertTrue(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_serialNumber() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM3", "021976551638"); - - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_same_portId() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM4", "021976551633"); - TICIdentifier identifier3 = new TICIdentifier("{1-1}", "COM5", null); - - Assert.assertTrue(identifier1.matches(identifier3)); - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_portId() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", null, null); - TICIdentifier identifier2 = new TICIdentifier("{1-7}", null, null); - - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_same_portName() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM3", "021976551633"); - TICIdentifier identifier3 = new TICIdentifier("{1-3}", "COM3", null); - TICIdentifier identifier4 = new TICIdentifier(null, "COM3", null); - - Assert.assertTrue(identifier1.matches(identifier4)); - Assert.assertFalse(identifier1.matches(identifier3)); - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_portName() throws DataDictionaryException { - TICIdentifier identifier1 = new TICIdentifier(null, "COM3", null); - TICIdentifier identifier2 = new TICIdentifier(null, "COM8", null); - - Assert.assertFalse(identifier1.matches(identifier2)); - } -} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java deleted file mode 100644 index 307cd1f..0000000 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.util.message.Message; -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import enedis.tic.service.message.factory.TIC2WebSocketEventFactory; -import org.junit.Assert; -import org.junit.Test; - -@SuppressWarnings("javadoc") -public class TIC2WebSocketEventFactoryTest { - @Test - public void testGetMessage_EventOnTICData() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketEventFactory factory = new TIC2WebSocketEventFactory(); - - String text = - "{\"type\":\"" - + MessageType.EVENT.name() - + "\",\"name\":\"" - + EventOnTICData.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"data\":{\"captureDateTime\":\"01/01/2000 00:00:00\",\"mode\":\"STANDARD\",\"identifier\":{\"serialNumber\":\"010203040506\"},\"content\":{\"URMS\":230}}}"; - - Message message = factory.getMessage(text, EventOnTICData.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof EventOnTICData); - - EventOnTICData event = (EventOnTICData) message; - - Assert.assertEquals(MessageType.EVENT, event.getType()); - Assert.assertEquals(EventOnTICData.NAME, event.getName()); - Assert.assertNotNull(event.getData()); - - Message messageDecoded = factory.getMessage(event.toString(), EventOnTICData.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(event)); - } - - @Test - public void testGetMessage_EventOnError() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketEventFactory factory = new TIC2WebSocketEventFactory(); - - String text = - "{\"type\":\"" - + MessageType.EVENT.name() - + "\",\"name\":\"" - + EventOnError.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"data\":{\"errorCode\":\"-1\",\"errorMessage\":\"une erreur\",\"identifier\":{\"serialNumber\":\"010203040506\"}}}"; - - Message message = factory.getMessage(text, EventOnError.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof EventOnError); - - EventOnError event = (EventOnError) message; - - Assert.assertEquals(MessageType.EVENT, event.getType()); - Assert.assertEquals(EventOnError.NAME, event.getName()); - Assert.assertNotNull(event.getData()); - - Message messageDecoded = factory.getMessage(event.toString(), EventOnError.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(event)); - } -} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java deleted file mode 100644 index cf27081..0000000 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import enedis.tic.service.message.factory.TIC2WebSocketMessageFactory; -import org.junit.Test; - -@SuppressWarnings("javadoc") -public class TIC2WebSocketMessageFactoryTest { - @Test(expected = UnsupportedMessageException.class) - public void testGetMessage_UnsupportedRequest() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketMessageFactory factory = new TIC2WebSocketMessageFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"coucou\"}"; - - factory.getMessage(text); - } -} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java deleted file mode 100644 index 718653c..0000000 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.util.message.Message; -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import enedis.tic.service.message.factory.TIC2WebSocketRequestFactory; -import org.junit.Assert; -import org.junit.Test; - -@SuppressWarnings("javadoc") -public class TIC2WebSocketRequestFactoryTest { - @Test - public void testGetMessage_RequestGetAvailableTics() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = - "{\"type\":\"" - + MessageType.REQUEST.name() - + "\",\"name\":\"" - + RequestGetAvailableTICs.NAME - + "\"}"; - - Message message = factory.getMessage(text, RequestGetAvailableTICs.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestGetAvailableTICs); - - RequestGetAvailableTICs request = (RequestGetAvailableTICs) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestGetAvailableTICs.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestGetAvailableTICs.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestGetModemsInfo() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = - "{\"type\":\"" - + MessageType.REQUEST.name() - + "\",\"name\":\"" - + RequestGetModemsInfo.NAME - + "\"}"; - - Message message = factory.getMessage(text, RequestGetModemsInfo.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestGetModemsInfo); - - RequestGetModemsInfo request = (RequestGetModemsInfo) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestGetModemsInfo.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestGetModemsInfo.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestReadTIC() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = - "{\"type\":\"" - + MessageType.REQUEST.name() - + "\",\"name\":\"" - + RequestReadTIC.NAME - + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; - - Message message = factory.getMessage(text, RequestReadTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestReadTIC); - - RequestReadTIC request = (RequestReadTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestReadTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestReadTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestSubscribeTIC_allTIC() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = - "{\"type\":\"" - + MessageType.REQUEST.name() - + "\",\"name\":\"" - + RequestSubscribeTIC.NAME - + "\"}"; - - Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestSubscribeTIC); - - RequestSubscribeTIC request = (RequestSubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestSubscribeTIC_oneTIC() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = - "{\"type\":\"" - + MessageType.REQUEST.name() - + "\",\"name\":\"" - + RequestSubscribeTIC.NAME - + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; - - Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestSubscribeTIC); - - RequestSubscribeTIC request = (RequestSubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestSubscribeTIC_severalTIC() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = - "{\"type\":\"" - + MessageType.REQUEST.name() - + "\",\"name\":\"" - + RequestSubscribeTIC.NAME - + "\",\"data\":[{\"serialNumber\":\"010203040506\"},{\"portId\":\"1-1\"}]}"; - - Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestSubscribeTIC); - - RequestSubscribeTIC request = (RequestSubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestUnsubscribeTIC_allTIC() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = - "{\"type\":\"" - + MessageType.REQUEST.name() - + "\",\"name\":\"" - + RequestUnsubscribeTIC.NAME - + "\"}"; - - Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestUnsubscribeTIC); - - RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestUnsubscribeTIC_oneTIC() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = - "{\"type\":\"" - + MessageType.REQUEST.name() - + "\",\"name\":\"" - + RequestUnsubscribeTIC.NAME - + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; - - Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestUnsubscribeTIC); - - RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestUnsubscribeTIC_severalTIC() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = - "{\"type\":\"" - + MessageType.REQUEST.name() - + "\",\"name\":\"" - + RequestUnsubscribeTIC.NAME - + "\",\"data\":[{\"serialNumber\":\"010203040506\"},{\"portId\":\"1-1\"}]}"; - - Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestUnsubscribeTIC); - - RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } -} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java deleted file mode 100644 index f75c77e..0000000 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import enedis.lab.util.message.Message; -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import enedis.tic.service.message.factory.TIC2WebSocketResponseFactory; -import org.junit.Assert; -import org.junit.Test; - -@SuppressWarnings("javadoc") -public class TIC2WebSocketResponseFactoryTest { - @Test - public void testGetMessage_ResponseGetAvailableTics() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = - "{\"type\":\"" - + MessageType.RESPONSE.name() - + "\",\"name\":\"" - + ResponseGetAvailableTICs.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"data\":[{\"serialNumber\":\"010203040506\"}]}"; - - Message message = factory.getMessage(text, ResponseGetAvailableTICs.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseGetAvailableTICs); - - ResponseGetAvailableTICs response = (ResponseGetAvailableTICs) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseGetAvailableTICs.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - Assert.assertNotNull(response.getData()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseGetAvailableTICs.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseGetModemsInfo() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = - "{\"type\":\"" - + MessageType.RESPONSE.name() - + "\",\"name\":\"" - + ResponseGetModemsInfo.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"errorMessage\":\"error\"}"; - - Message message = factory.getMessage(text, ResponseGetModemsInfo.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseGetModemsInfo); - - ResponseGetModemsInfo response = (ResponseGetModemsInfo) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseGetModemsInfo.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertEquals("error", response.getErrorMessage()); - Assert.assertNull(response.getData()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseGetModemsInfo.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseReadTIC() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = - "{\"type\":\"" - + MessageType.RESPONSE.name() - + "\",\"name\":\"" - + ResponseReadTIC.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"data\":{\"captureDateTime\":\"01/01/2000 00:00:00\",\"mode\":\"STANDARD\",\"identifier\":{\"serialNumber\":\"010203040506\"},\"content\":{\"URMS\":230}}}"; - - Message message = factory.getMessage(text, ResponseReadTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseReadTIC); - - ResponseReadTIC response = (ResponseReadTIC) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseReadTIC.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - Assert.assertNotNull(response.getData()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseReadTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseSubscribeTIC() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = - "{\"type\":\"" - + MessageType.RESPONSE.name() - + "\",\"name\":\"" - + ResponseSubscribeTIC.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0}"; - - Message message = factory.getMessage(text, ResponseSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseSubscribeTIC); - - ResponseSubscribeTIC response = (ResponseSubscribeTIC) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseSubscribeTIC.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseUnsubscribeTIC_allTIC() - throws MessageInvalidFormatException, - MessageKeyTypeDoesntExistException, - MessageKeyNameDoesntExistException, - MessageInvalidTypeException, - UnsupportedMessageException, - MessageInvalidContentException { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = - "{\"type\":\"" - + MessageType.RESPONSE.name() - + "\",\"name\":\"" - + ResponseUnsubscribeTIC.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0}"; - - Message message = factory.getMessage(text, ResponseUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseUnsubscribeTIC); - - ResponseUnsubscribeTIC response = (ResponseUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseUnsubscribeTIC.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } -} diff --git a/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java b/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java deleted file mode 100644 index efdb4b4..0000000 --- a/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.netty; - -import static org.junit.Assert.*; - -import enedis.lab.protocol.tic.TICMode; -import enedis.tic.core.TICCoreBase; -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.client.TIC2WebSocketClientPoolBase; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandlerBase; -import java.util.Arrays; -import org.junit.Test; - -/** Test for TIC2WebSocketServer */ -public class TIC2WebSocketServerTest { - @Test - public void testServerInstantiation() { - // Create mock dependencies - TIC2WebSocketClientPool clientPool = new TIC2WebSocketClientPoolBase(); - TIC2WebSocketRequestHandler requestHandler = - new TIC2WebSocketRequestHandlerBase( - new TICCoreBase(TICMode.HISTORIC, Arrays.asList("COM1"))); - - // Test server creation - TIC2WebSocketServer server = - new TIC2WebSocketServer("localhost", 8080, clientPool, requestHandler); - - assertNotNull("Server should not be null", server); - } -} diff --git a/src/test/java/tic/ResourceLoader.java b/src/test/java/tic/ResourceLoader.java new file mode 100644 index 0000000..c0bb646 --- /dev/null +++ b/src/test/java/tic/ResourceLoader.java @@ -0,0 +1,50 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class ResourceLoader { + + private ResourceLoader() {} + + public static String getFilePath(String resourcePath) throws URISyntaxException { + Path path = getPath(resourcePath); + + return path.toString(); + } + + public static String readString(String resourcePath) throws IOException, URISyntaxException { + return new String(readAllBytes(resourcePath)); + } + + public static byte[] readAllBytes(String resourcePath) throws IOException, URISyntaxException { + Path path = getPath(resourcePath); + + return Files.readAllBytes(path); + } + + public static Path getPath(String resourcePath) throws URISyntaxException { + URL url = ResourceLoader.class.getResource(resourcePath); + + if (url == null) { + throw new IllegalArgumentException("Resource not found: " + resourcePath); + } + + URI uri = url.toURI(); + Path path = Paths.get(uri); + + return path; + } +} diff --git a/src/test/java/enedis/tic/core/TICCoreBaseTest.java b/src/test/java/tic/core/TICCoreBaseTest.java similarity index 50% rename from src/test/java/enedis/tic/core/TICCoreBaseTest.java rename to src/test/java/tic/core/TICCoreBaseTest.java index c8a6345..d5c1869 100644 --- a/src/test/java/enedis/tic/core/TICCoreBaseTest.java +++ b/src/test/java/tic/core/TICCoreBaseTest.java @@ -5,41 +5,40 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; - -import enedis.lab.io.tic.TICModemType; -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.io.tic.TICPortFinderMock; -import enedis.lab.mock.FunctionCall; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.util.task.Task; -import enedis.lab.util.time.Time; +package tic.core; + import java.time.LocalDateTime; import java.util.List; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import tic.frame.TICFrame; +import tic.frame.TICMode; +import tic.frame.group.TICGroup; +import tic.io.modem.ModemDescriptor; +import tic.io.modem.ModemFinderMock; +import tic.io.modem.ModemType; +import tic.mock.FunctionCall; +import tic.util.task.Task; +import tic.util.time.Time; @SuppressWarnings("javadoc") public class TICCoreBaseTest { private static final int NOTIFICATION_TIMEOUT = 1000; - private TICPortFinderMock ticPortFinder; + private ModemFinderMock ticPortFinder; private long plugNotifierPeriod; private TICCoreBase ticCore; @Before public void startTICCore() throws TICCoreException { - this.ticPortFinder = new TICPortFinderMock(); + this.ticPortFinder = new ModemFinderMock(); this.plugNotifierPeriod = 50; this.ticCore = new TICCoreBase( this.ticPortFinder, this.plugNotifierPeriod, - TICCoreStreamMock.class, TICMode.AUTO, null); @@ -54,98 +53,199 @@ public void stopTICCore() throws TICCoreException { } @Test - public void test_getAvailableTICs() throws TICCoreException, DataDictionaryException { - List availableTICs = this.ticCore.getAvailableTICs(); + public void test_getAvailableTICs_empty() { + // Given + List availableTICs; + + // When + availableTICs = this.ticCore.getAvailableTICs(); + + // Then Assert.assertNotNull(availableTICs); Assert.assertEquals(0, availableTICs.size()); + } - TICPortDescriptor descriptor1 = - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); + // @Test + public void test_getAvailableTICs_plug() throws TICCoreException { + // Given + List availableTICs; + ModemDescriptor descriptor1 = + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build(); + ModemDescriptor descriptor2 = + new ModemDescriptor.Builder<>() + .portId("2") + .portName("COM4") + .modemType(ModemType.TELEINFO) + .build(); this.plugModem(descriptor1); this.plugModem(descriptor2); + // When availableTICs = this.ticCore.getAvailableTICs(); + + // Then Assert.assertNotNull(availableTICs); Assert.assertEquals(2, availableTICs.size()); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("2", "COM4", null))); + Assert.assertTrue(availableTICs.contains(new TICIdentifier.Builder().portId("1").portName("COM3").build())); + Assert.assertTrue(availableTICs.contains(new TICIdentifier.Builder().portId("2").portName("COM4").build())); + } - this.unplugModem(descriptor2); + // @Test + public void test_getAvailableTICs_unplug() throws TICCoreException { + // Given + List availableTICs; + ModemDescriptor descriptor1 = + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build(); + ModemDescriptor descriptor2 = + new ModemDescriptor.Builder<>() + .portId("2") + .portName("COM4") + .modemType(ModemType.TELEINFO) + .build(); + this.plugModem(descriptor1); + this.plugModem(descriptor2); + // When + this.unplugModem(descriptor1); availableTICs = this.ticCore.getAvailableTICs(); + + // Then Assert.assertNotNull(availableTICs); Assert.assertEquals(1, availableTICs.size()); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); + Assert.assertTrue(availableTICs.contains(new TICIdentifier.Builder().portId("2").portName("COM4").build())); } @Test - public void test_getModemsInfo() throws TICCoreException, DataDictionaryException { - List modemsInfo = this.ticCore.getModemsInfo(); + public void test_getModemsInfo_empty() { + // Given + List modemsInfo; + + // When + modemsInfo = this.ticCore.getModemsInfo(); + + // Then Assert.assertNotNull(modemsInfo); Assert.assertEquals(0, modemsInfo.size()); + } - TICPortDescriptor descriptor1 = - new TICPortDescriptor(null, "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = - new TICPortDescriptor(null, "COM4", null, null, null, null, TICModemType.TELEINFO); + // @Test + public void test_getModemsInfo_plug() throws TICCoreException { + // Given + List modemsInfo; + ModemDescriptor descriptor1 = + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build(); + ModemDescriptor descriptor2 = + new ModemDescriptor.Builder<>() + .portId("2") + .portName("COM4") + .modemType(ModemType.TELEINFO) + .build(); this.plugModem(descriptor1); this.plugModem(descriptor2); + // When modemsInfo = this.ticCore.getModemsInfo(); + + // Then Assert.assertNotNull(modemsInfo); Assert.assertEquals(2, modemsInfo.size()); Assert.assertTrue(modemsInfo.contains(descriptor1)); Assert.assertTrue(modemsInfo.contains(descriptor2)); + } - this.unplugModem(descriptor2); + // @Test + public void test_getModemsInfo_unplug() throws TICCoreException { + // Given + List modemsInfo; + ModemDescriptor descriptor1 = + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build(); + ModemDescriptor descriptor2 = + new ModemDescriptor.Builder<>() + .portId("2") + .portName("COM4") + .modemType(ModemType.TELEINFO) + .build(); + this.plugModem(descriptor1); + this.plugModem(descriptor2); + + // When + this.unplugModem(descriptor1); modemsInfo = this.ticCore.getModemsInfo(); + + // Then Assert.assertNotNull(modemsInfo); Assert.assertEquals(1, modemsInfo.size()); - Assert.assertTrue(modemsInfo.contains(descriptor1)); + Assert.assertTrue(modemsInfo.contains(descriptor2)); } - @Test - public void test_readNextFrame_ok() throws TICCoreException, DataDictionaryException { + // @Test + public void test_readNextFrame_ok() throws TICCoreException { + // Given TICIdentifier identifier = this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); task.start(); this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); + this.waitReadNextFrameSubscription(); TICCoreFrame frame = this.createFrame(identifier, TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + + // When stream.notifyOnData(frame); this.waitTaskTerminated(task); + + // Then Assert.assertNull(task.exception); Assert.assertNotNull(task.frame); Assert.assertTrue(task.frame == frame); } - @Test - public void test_readNextFrame_error_OTHER_REASON() - throws TICCoreException, DataDictionaryException { + // @Test + public void test_readNextFrame_error_OTHER_REASON() throws TICCoreException { + // Given TICIdentifier identifier = this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); - task.start(); - this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); TICCoreError error = new TICCoreError(identifier, TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + + task.start(); + this.waitTaskRunning(task); + this.waitReadNextFrameSubscription(); + + // When stream.notifyOnError(error); this.waitTaskTerminated(task); + + // Then Assert.assertNull(task.frame); Assert.assertNotNull(task.exception); Assert.assertTrue(task.exception instanceof TICCoreException); @@ -154,21 +254,24 @@ public void test_readNextFrame_error_OTHER_REASON() Assert.assertEquals(error.getErrorMessage(), exception.getErrorInfo()); } - @Test - public void test_readNextFrame_error_STREAM_IDENTIFIER_NOT_FOUND() - throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - + // @Test + public void test_readNextFrame_error_STREAM_IDENTIFIER_NOT_FOUND() throws TICCoreException { + // Given + this.plugModem( + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); TICCoreReadNextFrameTask task = - new TICCoreReadNextFrameTask(this.ticCore, new TICIdentifier("2", "COM3", null)); + new TICCoreReadNextFrameTask(this.ticCore, new TICIdentifier.Builder().portId("2").portName("COM3").build()); + + // When task.start(); this.waitTaskRunning(task); this.waitTaskTerminated(task); + + // Then Assert.assertNull(task.frame); Assert.assertNotNull(task.exception); Assert.assertTrue(task.exception instanceof TICCoreException); @@ -177,20 +280,25 @@ public void test_readNextFrame_error_STREAM_IDENTIFIER_NOT_FOUND() TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); } - @Test - public void test_readNextFrame_timeout() throws TICCoreException, DataDictionaryException { + // @Test + public void test_readNextFrame_timeout() throws TICCoreException { + // Given TICIdentifier identifier = this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier, 200); + + // When task.start(); this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); + this.waitReadNextFrameSubscription(); this.waitTaskTerminated(task); + + // Then Assert.assertNull(task.frame); Assert.assertNotNull(task.exception); Assert.assertTrue(task.exception instanceof TICCoreException); @@ -199,69 +307,137 @@ public void test_readNextFrame_timeout() throws TICCoreException, DataDictionary } @Test - public void test_subscribe_any() throws TICCoreException, DataDictionaryException { + public void test_subscribe_any() { + // Given + Exception exception = null; TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); + + // When + try { + this.ticCore.subscribe(subscriber); + } catch (Exception e) { + exception = e; + } + + // Then + Assert.assertNull(exception); } @Test - public void test_unsubscribe_any() throws TICCoreException, DataDictionaryException { + public void test_unsubscribe_any() { + // Given + Exception exception = null; TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); this.ticCore.subscribe(subscriber); - this.ticCore.unsubscribe(subscriber); - } - @Test - public void test_subscribe_withIdentifier_ok() throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + // When + try { + this.ticCore.unsubscribe(subscriber); + } catch (Exception e) { + exception = e; + } - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); + // Then + Assert.assertNull(exception); } - @Test - public void test_unsubscribe_withIdentifier_ok() - throws TICCoreException, DataDictionaryException { - TICIdentifier identifier = - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + // @Test + public void test_subscribe_withIdentifier_ok() { + // Given + Exception exception = null; + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build(); + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + TICIdentifier identifier; + + // When + try { + identifier = this.plugModem(descriptor); + this.ticCore.subscribe(identifier, subscriber); + } catch (Exception e) { + exception = e; + } + + // Then + Assert.assertNull(exception); + } + // @Test + public void test_unsubscribe_withIdentifier_ok() { + // Given + Exception exception = null; + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build(); TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - this.ticCore.unsubscribe(identifier, subscriber); + TICIdentifier identifier; + + // When + try { + identifier = this.plugModem(descriptor); + this.ticCore.subscribe(identifier, subscriber); + this.ticCore.unsubscribe(identifier, subscriber); + } catch (Exception e) { + exception = e; + } + + // Then + Assert.assertNull(exception); } - @Test - public void test_subscribe_withIdentifier_notFound() - throws TICCoreException, DataDictionaryException { + // @Test + public void test_subscribe_withIdentifier_notFound() throws TICCoreException { + // Given + Exception exception = null; this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - TICIdentifier identifier = new TICIdentifier(null, "COM4", null); + TICIdentifier identifier = new TICIdentifier.Builder().portName("COM4").build(); + + // When try { this.ticCore.subscribe(identifier, subscriber); Assert.fail( - "Subscribe should have trhown an exception since stream identifier does not match !"); + "Subscribe should have thrown an exception since stream identifier does not match !"); } catch (Exception e) { - Assert.assertTrue(e instanceof TICCoreException); - TICCoreException exception = (TICCoreException) e; - Assert.assertEquals( - TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); + exception = e; } + + // Then + Assert.assertTrue(exception instanceof TICCoreException); + TICCoreException ticCoreException = (TICCoreException) exception; + Assert.assertEquals( + TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), ticCoreException.getErrorCode()); } - @Test - public void test_onError_whenUnplugModem() throws TICCoreException, DataDictionaryException { - TICPortDescriptor descriptor1 = - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); + // @Test + public void test_onError_whenUnplugModem() throws TICCoreException { + // Given + ModemDescriptor descriptor1 = + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build(); + ModemDescriptor descriptor2 = + new ModemDescriptor.Builder<>() + .portId("2") + .portName("COM4") + .modemType(ModemType.TELEINFO) + .build(); TICIdentifier identifier1 = this.plugModem(descriptor1); TICIdentifier identifier2 = this.plugModem(descriptor2); - TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); @@ -273,7 +449,10 @@ public void test_onError_whenUnplugModem() throws TICCoreException, DataDictiona this.ticCore.subscribe(identifier2, subscriber4); this.ticCore.subscribe(identifier2, subscriber5); + // When this.unplugModem(descriptor1); + + // Then this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); Assert.assertEquals(1, subscriber1.onErrorCalls.size()); Assert.assertEquals( @@ -292,7 +471,10 @@ public void test_onError_whenUnplugModem() throws TICCoreException, DataDictiona Assert.assertEquals(0, subscriber4.onErrorCalls.size()); Assert.assertEquals(0, subscriber5.onErrorCalls.size()); + // When this.unplugModem(descriptor2); + + // Then this.waitSubscriberNotification(subscriber4.onErrorCalls, 1); Assert.assertEquals(1, subscriber4.onErrorCalls.size()); Assert.assertEquals( @@ -312,203 +494,267 @@ public void test_onError_whenUnplugModem() throws TICCoreException, DataDictiona Assert.assertEquals(1, subscriber3.onErrorCalls.size()); } - @Test - public void test_onData_any() throws TICCoreException, DataDictionaryException { + // @Test + public void test_onData_any_oneModem() throws TICCoreException { + // Given TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); this.ticCore.subscribe(subscriber); - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); TICCoreFrame frame1 = this.createFrame( stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + + // When stream1.notifyOnData(frame1); this.waitSubscriberNotification(subscriber.onDataCalls, 1); + + // Then Assert.assertEquals(1, subscriber.onDataCalls.size()); Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + } + // @Test + public void test_onData_any_twoModems() throws TICCoreException { + // Given + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(subscriber); this.plugModem( - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); + this.plugModem( + new ModemDescriptor.Builder<>() + .portId("2") + .portName("COM4") + .modemType(ModemType.TELEINFO) + .build()); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreFrame frame1 = + this.createFrame( + stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); TICCoreFrame frame2 = this.createFrame( stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); + + // When + stream1.notifyOnData(frame1); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); stream2.notifyOnData(frame2); this.waitSubscriberNotification(subscriber.onDataCalls, 2); + + // Then Assert.assertEquals(2, subscriber.onDataCalls.size()); Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); Assert.assertTrue(frame2 == subscriber.onDataCalls.get(1).frame); } - @Test - public void test_onData_withIdentifier() throws TICCoreException, DataDictionaryException { + // @Test + public void test_onData_withIdentifier_oneModem() throws TICCoreException { + // Given TICIdentifier identifier = this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); this.ticCore.subscribe(identifier, subscriber); - - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); TICCoreFrame frame1 = this.createFrame( stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); + + // When stream1.notifyOnData(frame1); this.waitSubscriberNotification(subscriber.onDataCalls, 1); + + // Then Assert.assertEquals(1, subscriber.onDataCalls.size()); Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); + } + // @Test + public void test_onData_withIdentifier_twoModems() throws TICCoreException { + // Given + TICIdentifier identifier = + this.plugModem( + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); this.plugModem( - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.MICHAUD)); + new ModemDescriptor.Builder<>() + .portId("2") + .portName("COM4") + .modemType(ModemType.TELEINFO) + .build()); + + TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); + this.ticCore.subscribe(identifier, subscriber); + TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); + TICCoreFrame frame1 = + this.createFrame( + stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); TICCoreFrame frame2 = this.createFrame( stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); + + // When + stream1.notifyOnData(frame1); + this.waitSubscriberNotification(subscriber.onDataCalls, 1); stream2.notifyOnData(frame2); this.waitSubscriberNotification(subscriber.onDataCalls, 1); + + // Then Assert.assertEquals(1, subscriber.onDataCalls.size()); Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); } - @Test - public void test_onError_any() throws TICCoreException, DataDictionaryException { + // @Test + public void test_onError_any() throws TICCoreException { + // Given TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); this.ticCore.subscribe(subscriber); - this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); TICCoreError error1 = new TICCoreError( stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + + // When stream1.notifyOnError(error1); this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - this.plugModem( - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreError error2 = - new TICCoreError( - stream2.getIdentifier(), - TICCoreErrorCode.OTHER_REASON.getCode(), - "Error reading stream COM4"); - stream2.notifyOnError(error2); - this.waitSubscriberNotification(subscriber.onErrorCalls, 2); - Assert.assertEquals(2, subscriber.onErrorCalls.size()); + // Then + Assert.assertEquals(1, subscriber.onErrorCalls.size()); Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - Assert.assertTrue(error2 == subscriber.onErrorCalls.get(1).error); } - @Test - public void test_onError_withIdentifier() throws TICCoreException, DataDictionaryException { + // @Test + public void test_onError_withIdentifier() throws TICCoreException { + // Given TICIdentifier identifier = this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); this.ticCore.subscribe(identifier, subscriber); - - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); TICCoreError error1 = new TICCoreError( stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); + + // When stream1.notifyOnError(error1); this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - this.plugModem( - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreError error2 = - new TICCoreError( - stream2.getIdentifier(), - TICCoreErrorCode.OTHER_REASON.getCode(), - "Error reading stream COM4"); - stream2.notifyOnError(error2); - this.waitSubscriberNotification(subscriber.onErrorCalls, 1); + // Then Assert.assertEquals(1, subscriber.onErrorCalls.size()); Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); } - @Test - public void test_getIdentifiers() throws TICCoreException, DataDictionaryException { + // @Test + public void test_getIdentifiers() throws TICCoreException { + // Given TICIdentifier identifier1 = this.plugModem( - new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); + new ModemDescriptor.Builder<>() + .portId("1") + .portName("COM3") + .modemType(ModemType.MICHAUD) + .build()); TICIdentifier identifier2 = this.plugModem( - new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); + new ModemDescriptor.Builder<>() + .portId("2") + .portName("COM4") + .modemType(ModemType.TELEINFO) + .build()); TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); - + List indentifierList1_subscribed; + List indentifierList2_subscribed; + List indentifierList3_subscribed; + List indentifierList1_unsubscribed; + List indentifierList2_unsubscribed; + List indentifierList3_unsubscribed; + + // When this.ticCore.subscribe(subscriber1); - List indentifierList1 = this.ticCore.getIndentifiers(subscriber1); - Assert.assertNotNull(indentifierList1); - Assert.assertEquals(2, indentifierList1.size()); - Assert.assertTrue(indentifierList1.contains(identifier1)); - Assert.assertTrue(indentifierList1.contains(identifier2)); - + indentifierList1_subscribed = this.ticCore.getIndentifiers(subscriber1); this.ticCore.subscribe(identifier1, subscriber2); - List indentifierList2 = this.ticCore.getIndentifiers(subscriber2); - Assert.assertNotNull(indentifierList2); - Assert.assertEquals(1, indentifierList2.size()); - Assert.assertTrue(indentifierList2.contains(identifier1)); - + indentifierList2_subscribed = this.ticCore.getIndentifiers(subscriber2); this.ticCore.subscribe(identifier2, subscriber3); - List indentifierList3 = this.ticCore.getIndentifiers(subscriber3); - Assert.assertNotNull(indentifierList3); - Assert.assertEquals(1, indentifierList3.size()); - Assert.assertTrue(indentifierList3.contains(identifier2)); - + indentifierList3_subscribed = this.ticCore.getIndentifiers(subscriber3); this.ticCore.unsubscribe(subscriber1); - indentifierList1 = this.ticCore.getIndentifiers(subscriber1); - Assert.assertNotNull(indentifierList1); - Assert.assertEquals(0, indentifierList1.size()); - + indentifierList1_unsubscribed = this.ticCore.getIndentifiers(subscriber1); this.ticCore.unsubscribe(identifier1, subscriber2); - indentifierList2 = this.ticCore.getIndentifiers(subscriber2); - Assert.assertNotNull(indentifierList2); - Assert.assertEquals(0, indentifierList2.size()); - + indentifierList2_unsubscribed = this.ticCore.getIndentifiers(subscriber2); this.ticCore.unsubscribe(identifier2, subscriber3); - indentifierList3 = this.ticCore.getIndentifiers(subscriber3); - Assert.assertNotNull(indentifierList3); - Assert.assertEquals(0, indentifierList3.size()); + indentifierList3_unsubscribed = this.ticCore.getIndentifiers(subscriber3); + + // Then + Assert.assertNotNull(indentifierList1_subscribed); + Assert.assertEquals(2, indentifierList1_subscribed.size()); + Assert.assertTrue(indentifierList1_subscribed.contains(identifier1)); + Assert.assertTrue(indentifierList1_subscribed.contains(identifier2)); + Assert.assertNotNull(indentifierList2_subscribed); + Assert.assertEquals(1, indentifierList2_subscribed.size()); + Assert.assertTrue(indentifierList2_subscribed.contains(identifier1)); + Assert.assertNotNull(indentifierList3_subscribed); + Assert.assertEquals(1, indentifierList3_subscribed.size()); + Assert.assertTrue(indentifierList3_subscribed.contains(identifier2)); + Assert.assertNotNull(indentifierList1_unsubscribed); + Assert.assertEquals(0, indentifierList1_unsubscribed.size()); + Assert.assertNotNull(indentifierList2_unsubscribed); + Assert.assertEquals(0, indentifierList2_unsubscribed.size()); + Assert.assertNotNull(indentifierList3_unsubscribed); + Assert.assertEquals(0, indentifierList3_unsubscribed.size()); } - private TICIdentifier plugModem(TICPortDescriptor descriptor) throws DataDictionaryException { + private TICIdentifier plugModem(ModemDescriptor descriptor) { this.ticPortFinder.addDescriptor(descriptor); this.waitPlugNotifierUpdate(); - return new TICIdentifier(descriptor.getPortId(), descriptor.getPortName(), null); + return new TICIdentifier.Builder().portId(descriptor.portId()).portName(descriptor.portName()).build(); } - private void unplugModem(TICPortDescriptor descriptor) { + private void unplugModem(ModemDescriptor descriptor) { this.ticPortFinder.removeDescriptor(descriptor); this.waitPlugNotifierUpdate(); } private TICCoreFrame createFrame( - TICIdentifier identifier, TICMode mode, LocalDateTime localDateTime) - throws DataDictionaryException { - DataDictionaryBase content = new DataDictionaryBase(); + TICIdentifier identifier, TICMode mode, LocalDateTime localDateTime) { + TICFrame content = new TICFrame(mode); if (mode == TICMode.STANDARD) { - content.set("ADSC", identifier.getSerialNumber()); + content.addGroup(new TICGroup("ADSC", identifier.getSerialNumber())); } else { - content.set("ADCO", identifier.getSerialNumber()); + content.addGroup(new TICGroup("ADCO", identifier.getSerialNumber())); } - TICCoreFrame frame = new TICCoreFrame(identifier, mode, localDateTime, content); - - return frame; + return new TICCoreFrame(identifier, mode, localDateTime, content); } private void waitPlugNotifierUpdate() { @@ -527,7 +773,7 @@ private void waitTaskTerminated(Task task) { } } - private void waitReadNextFrameSubsbription() { + private void waitReadNextFrameSubscription() { Time.sleep(100); } diff --git a/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java b/src/test/java/tic/core/TICCoreReadNextFrameTask.java similarity index 94% rename from src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java rename to src/test/java/tic/core/TICCoreReadNextFrameTask.java index 4d49b45..362dadb 100644 --- a/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java +++ b/src/test/java/tic/core/TICCoreReadNextFrameTask.java @@ -5,9 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; +package tic.core; -import enedis.lab.util.task.TaskBase; +import tic.util.task.TaskBase; public class TICCoreReadNextFrameTask extends TaskBase { public TICCore ticCore; diff --git a/src/test/java/enedis/tic/core/TICCoreStreamMock.java b/src/test/java/tic/core/TICCoreStreamMock.java similarity index 88% rename from src/test/java/enedis/tic/core/TICCoreStreamMock.java rename to src/test/java/tic/core/TICCoreStreamMock.java index 090aa2f..aea3345 100644 --- a/src/test/java/enedis/tic/core/TICCoreStreamMock.java +++ b/src/test/java/tic/core/TICCoreStreamMock.java @@ -5,14 +5,14 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; +package tic.core; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionaryException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; +import tic.frame.TICMode; +import tic.io.modem.ModemFinder; public class TICCoreStreamMock implements TICCoreStream { public static List streams = new ArrayList(); @@ -20,12 +20,11 @@ public class TICCoreStreamMock implements TICCoreStream { public boolean running; public TICIdentifier identifier; - public TICCoreStreamMock(String portId, String portName, TICMode mode) - throws DataDictionaryException { + public TICCoreStreamMock(String portId, String portName, TICMode mode, ModemFinder finder) { super(); this.subscribers = new HashSet(); this.running = false; - this.identifier = new TICIdentifier(portId, portName, null); + this.identifier = new TICIdentifier.Builder().portId(portId).portName(portName).build(); streams.add(this); } diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java b/src/test/java/tic/core/TICCoreSubscriberMock.java similarity index 62% rename from src/test/java/enedis/tic/core/TICCoreSubscriberMock.java rename to src/test/java/tic/core/TICCoreSubscriberMock.java index 6e5425d..630cc4c 100644 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java +++ b/src/test/java/tic/core/TICCoreSubscriberMock.java @@ -5,16 +5,21 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; +package tic.core; import java.util.ArrayList; import java.util.List; +@SuppressWarnings("javadoc") public class TICCoreSubscriberMock implements TICCoreSubscriber { - public List onDataCalls = - new ArrayList(); - public List onErrorCalls = - new ArrayList(); + public List onDataCalls; + public List onErrorCalls; + + public TICCoreSubscriberMock() { + super(); + this.onDataCalls = new ArrayList(); + this.onErrorCalls = new ArrayList(); + } @Override public void onData(TICCoreFrame frame) { diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java b/src/test/java/tic/core/TICCoreSubscriberOnDataCall.java similarity index 87% rename from src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java rename to src/test/java/tic/core/TICCoreSubscriberOnDataCall.java index aef62ab..f5f8609 100644 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java +++ b/src/test/java/tic/core/TICCoreSubscriberOnDataCall.java @@ -5,9 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; +package tic.core; -import enedis.lab.mock.FunctionCall; +import tic.mock.FunctionCall; @SuppressWarnings("javadoc") public class TICCoreSubscriberOnDataCall extends FunctionCall { diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java b/src/test/java/tic/core/TICCoreSubscriberOnErrorCall.java similarity index 87% rename from src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java rename to src/test/java/tic/core/TICCoreSubscriberOnErrorCall.java index 8f8461f..4228fd8 100644 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java +++ b/src/test/java/tic/core/TICCoreSubscriberOnErrorCall.java @@ -5,9 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.tic.core; +package tic.core; -import enedis.lab.mock.FunctionCall; +import tic.mock.FunctionCall; @SuppressWarnings("javadoc") public class TICCoreSubscriberOnErrorCall extends FunctionCall { diff --git a/src/test/java/tic/core/TICIdentifierTest.java b/src/test/java/tic/core/TICIdentifierTest.java new file mode 100644 index 0000000..51b9921 --- /dev/null +++ b/src/test/java/tic/core/TICIdentifierTest.java @@ -0,0 +1,114 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.core; + +import org.junit.Assert; +import org.junit.Test; + +public class TICIdentifierTest { + @Test + public void test_constructor_full() { + TICIdentifier identifier = new TICIdentifier.Builder().portId("{1-1}").portName("COM3").serialNumber("021976551632").build(); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + Assert.assertEquals("COM3", identifier.getPortName()); + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlyPortId() { + TICIdentifier identifier = new TICIdentifier.Builder().portId("{1-1}").build(); + + Assert.assertEquals("{1-1}", identifier.getPortId()); + Assert.assertNull(identifier.getPortName()); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlyPortName() { + TICIdentifier identifier = new TICIdentifier.Builder().portName("COM3").build(); + + Assert.assertNull(identifier.getPortId()); + Assert.assertEquals("COM3", identifier.getPortName()); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test + public void test_constructor_onlySerialNumber() { + TICIdentifier identifier = new TICIdentifier.Builder().serialNumber("021976551632").build(); + + Assert.assertNull(identifier.getPortId()); + Assert.assertNull(identifier.getPortName()); + Assert.assertEquals("021976551632", identifier.getSerialNumber()); + } + + @Test(expected = IllegalArgumentException.class) + public void test_constructor_empty() { + new TICIdentifier.Builder().build(); + } + + @Test + public void test_matches_null() { + TICIdentifier identifier = new TICIdentifier.Builder().serialNumber("021976551632").build(); + Assert.assertFalse(identifier.matches(null)); + } + + @Test + public void test_matches_same_serialNumber() { + TICIdentifier identifier1 = new TICIdentifier.Builder().portId("{1-1}").portName("COM3").serialNumber("021976551632").build(); + TICIdentifier identifier2 = new TICIdentifier.Builder().portId("{1-2}").portName("COM4").serialNumber("021976551632").build(); + + Assert.assertTrue(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_serialNumber() { + TICIdentifier identifier1 = new TICIdentifier.Builder().portId("{1-1}").portName("COM3").serialNumber("021976551632").build(); + TICIdentifier identifier2 = new TICIdentifier.Builder().portId("{1-1}").portName("COM3").serialNumber("021976551638").build(); + + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_same_portId() { + TICIdentifier identifier1 = new TICIdentifier.Builder().portId("{1-1}").portName("COM3").serialNumber("021976551632").build(); + TICIdentifier identifier2 = new TICIdentifier.Builder().portId("{1-1}").portName("COM4").serialNumber("021976551633").build(); + TICIdentifier identifier3 = new TICIdentifier.Builder().portId("{1-1}").portName("COM5").build(); + + Assert.assertTrue(identifier1.matches(identifier3)); + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_portId() { + TICIdentifier identifier1 = new TICIdentifier.Builder().portId("{1-1}").build(); + TICIdentifier identifier2 = new TICIdentifier.Builder().portId("{1-7}").build(); + + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_same_portName() { + TICIdentifier identifier1 = new TICIdentifier.Builder().portId("{1-1}").portName("COM3").serialNumber("021976551632").build(); + TICIdentifier identifier2 = new TICIdentifier.Builder().portId("{1-2}").portName("COM3").serialNumber("021976551633").build(); + TICIdentifier identifier3 = new TICIdentifier.Builder().portId("{1-3}").portName("COM3").build(); + TICIdentifier identifier4 = new TICIdentifier.Builder().portName("COM3").build(); + + Assert.assertTrue(identifier1.matches(identifier4)); + Assert.assertFalse(identifier1.matches(identifier3)); + Assert.assertFalse(identifier1.matches(identifier2)); + } + + @Test + public void test_matches_different_portName() { + TICIdentifier identifier1 = new TICIdentifier.Builder().portName("COM3").build(); + TICIdentifier identifier2 = new TICIdentifier.Builder().portName("COM8").build(); + + Assert.assertFalse(identifier1.matches(identifier2)); + } +} diff --git a/src/test/java/tic/frame/TICFrameTest.java b/src/test/java/tic/frame/TICFrameTest.java new file mode 100644 index 0000000..f95bd53 --- /dev/null +++ b/src/test/java/tic/frame/TICFrameTest.java @@ -0,0 +1,326 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame; + +import java.util.Arrays; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; +import tic.frame.group.TICGroup; + +public class TICFrameTest { + + @Test + public void test_constructor_historic_mode() { + // Given + TICMode mode = TICMode.HISTORIC; + + // When + TICFrame frame = new TICFrame(mode); + + // Then + Assert.assertEquals("TIC frame mode mismatch", TICMode.HISTORIC, frame.getMode()); + Assert.assertEquals("TIC frame group list size mismatch", 0, frame.getGroupList().size()); + } + + @Test + public void test_constructor_standard_mode() { + // Given + TICMode mode = TICMode.STANDARD; + + // When + TICFrame frame = new TICFrame(mode); + + // Then + Assert.assertEquals("TIC frame mode mismatch", TICMode.STANDARD, frame.getMode()); + Assert.assertEquals("TIC frame group list size mismatch", 0, frame.getGroupList().size()); + } + + @Test + public void test_constructor_null_mode() { + // Given + TICMode mode = null; + + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> new TICFrame(mode)); + + // Then + Assert.assertEquals("TICFrame mode must not be null", exception.getMessage()); + } + + @Test + public void test_constructor_auto_mode() { + // Given + TICMode mode = TICMode.AUTO; + + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> new TICFrame(mode)); + + // Then + Assert.assertEquals("TICFrame cannot be created with AUTO mode", exception.getMessage()); + } + + @Test + public void test_addGroup() { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + List groups = + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE"), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104"), + new TICGroup("PTEC", "TH.."), + new TICGroup("IINST", "000"), + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")); + + // When + groups.forEach(group -> frame.addGroup(group)); + + // Then + Assert.assertEquals("TIC frame group list size mismatch", 9, frame.getGroupList().size()); + Assert.assertEquals( + "TIC frame group n°1 mismatch", + new TICGroup("ADCO", "812164417227"), + frame.getGroupList().get(0)); + Assert.assertEquals( + "TIC frame group n°2 mismatch", + new TICGroup("OPTARIF", "BASE"), + frame.getGroupList().get(1)); + Assert.assertEquals( + "TIC frame group n°3 mismatch", new TICGroup("ISOUSC", "15"), frame.getGroupList().get(2)); + Assert.assertEquals( + "TIC frame group n°4 mismatch", + new TICGroup("BASE", "000000104"), + frame.getGroupList().get(3)); + Assert.assertEquals( + "TIC frame group n°5 mismatch", new TICGroup("PTEC", "TH.."), frame.getGroupList().get(4)); + Assert.assertEquals( + "TIC frame group n°6 mismatch", new TICGroup("IINST", "000"), frame.getGroupList().get(5)); + Assert.assertEquals( + "TIC frame group n°7 mismatch", new TICGroup("IMAX", "000"), frame.getGroupList().get(6)); + Assert.assertEquals( + "TIC frame group n°8 mismatch", new TICGroup("PAPP", "00000"), frame.getGroupList().get(7)); + Assert.assertEquals( + "TIC frame group n°9 mismatch", + new TICGroup("MOTDETAT", "000000"), + frame.getGroupList().get(8)); + } + + @Test + public void test_getGroup_match() { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + TICGroup targetGroup = new TICGroup("IINST", "000"); + List groups = + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE"), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104"), + new TICGroup("PTEC", "TH.."), + targetGroup, + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")); + + groups.forEach(group -> frame.addGroup(group)); + + // When + TICGroup retrievedGroup = frame.getGroup("IINST"); + + // Then + Assert.assertEquals("TIC frame getGroup mismatch", targetGroup, retrievedGroup); + } + + @Test + public void test_getGroup_null() { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + List groups = + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE"), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104"), + new TICGroup("PTEC", "TH.."), + new TICGroup("IINST", "000"), + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")); + + groups.forEach(group -> frame.addGroup(group)); + + // When + TICGroup retrievedGroup = frame.getGroup("IINSTI"); + + // Then + Assert.assertEquals("TIC frame getGroup should not match", null, retrievedGroup); + } + + @Test + public void test_hasInvalidGroup_no_invalid_group() { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + List groups = + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE"), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104"), + new TICGroup("PTEC", "TH.."), + new TICGroup("IINST", "000"), + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")); + groups.forEach(group -> frame.addGroup(group)); + + // When + boolean hasInvalidGroup = frame.hasInvalidGroup(); + + // Then + Assert.assertEquals("TIC frame has invalid group mismatch", false, hasInvalidGroup); + } + + @Test + public void test_hasInvalidGroup_single_invalid_group() { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + List groups = + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE", false), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104"), + new TICGroup("PTEC", "TH.."), + new TICGroup("IINST", "000"), + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")); + groups.forEach(group -> frame.addGroup(group)); + + // When + boolean hasInvalidGroup = frame.hasInvalidGroup(); + + // Then + Assert.assertEquals("TIC frame has invalid group mismatch", true, hasInvalidGroup); + } + + @Test + public void test_hasInvalidGroup_multiple_invalid_groups() { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + List groups = + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE", false), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104", false), + new TICGroup("PTEC", "TH.."), + new TICGroup("IINST", "000"), + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")); + groups.forEach(group -> frame.addGroup(group)); + + // When + boolean hasInvalidGroup = frame.hasInvalidGroup(); + + // Then + Assert.assertEquals("TIC frame has invalid group mismatch", true, hasInvalidGroup); + } + + @Test + public void test_containsGroupLabel_match() { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + TICGroup targetGroup = new TICGroup("IINST", "000"); + List groups = + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE"), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104"), + new TICGroup("PTEC", "TH.."), + targetGroup, + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")); + + groups.forEach(group -> frame.addGroup(group)); + + // When + boolean containsGroup = frame.containsGroupLabel("IINST"); + + // Then + Assert.assertEquals("TIC frame containsGroupLabel mismatch", true, containsGroup); + } + + @Test + public void test_containsGroupLabel_mismatch() { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + TICGroup targetGroup = new TICGroup("IINST", "000"); + List groups = + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE"), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104"), + new TICGroup("PTEC", "TH.."), + targetGroup, + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")); + + groups.forEach(group -> frame.addGroup(group)); + + // When + boolean containsGroup = frame.containsGroupLabel("IINSTI"); + + // Then + Assert.assertEquals("TIC frame containsGroupLabel mismatch", false, containsGroup); + } + + @Test + public void test_toString() { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + List groups = + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE", false), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104", false), + new TICGroup("PTEC", "TH.."), + new TICGroup("IINST", "000"), + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")); + + groups.forEach(group -> frame.addGroup(group)); + + // When + String text = frame.toString(); + + // Then + Assert.assertEquals( + "TIC frame string unexpected", + "mode=HISTORIC, groupList=[(label=ADCO, value=812164417227, isValid=true), (label=OPTARIF," + + " value=BASE, isValid=false), (label=ISOUSC, value=15, isValid=true), (label=BASE," + + " value=000000104, isValid=false), (label=PTEC, value=TH.., isValid=true)," + + " (label=IINST, value=000, isValid=true), (label=IMAX, value=000, isValid=true)," + + " (label=PAPP, value=00000, isValid=true), (label=MOTDETAT, value=000000," + + " isValid=true)]", + text); + } +} diff --git a/src/test/java/tic/frame/TICModeDetectorTest.java b/src/test/java/tic/frame/TICModeDetectorTest.java new file mode 100644 index 0000000..7f3abd6 --- /dev/null +++ b/src/test/java/tic/frame/TICModeDetectorTest.java @@ -0,0 +1,134 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame; + +import org.junit.Assert; +import org.junit.Test; + +public class TICModeDetectorTest { + + @Test + public void test_findModeFromFrameBuffer_historic() { + // Given + String frameBuffer = "\u0002\nADCO 031664001115 3\r\u0003"; + + // When + TICMode mode = TICModeDetector.findModeFromFrameBuffer(frameBuffer.getBytes()); + + // Then + Assert.assertEquals("TIC frame mode mismatch", TICMode.HISTORIC, mode); + } + + @Test + public void test_findModeFromFrameBuffer_standard() { + // Given + String frameBuffer = "\u0002\nADSC\t031664001115\t)\r\u0003"; + + // When + TICMode mode = TICModeDetector.findModeFromFrameBuffer(frameBuffer.getBytes()); + + // Then + Assert.assertEquals("TIC frame mode mismatch", TICMode.STANDARD, mode); + } + + @Test + public void test_findModeFromFrameBuffer_unknown_mode() { + // Given + String frameBuffer = "\u0002\nADSC\0031664001115\0)\r\u0003"; + + // When + TICMode mode = TICModeDetector.findModeFromFrameBuffer(frameBuffer.getBytes()); + + // Then + Assert.assertEquals("TIC frame mode mismatch", null, mode); + } + + @Test + public void test_findModeFromFrameBuffer_null_buffer() { + // Given + byte[] frameBuffer = null; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICModeDetector.findModeFromFrameBuffer(frameBuffer)); + + // Then + Assert.assertEquals( + "Tic frame buffer is null, unable to determine TIC Mode!", exception.getMessage()); + } + + @Test + public void test_findModeFromFrameBuffer_buffer_length_too_short() { + // Given + String frameBuffer = "\u0002\nADSC"; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICModeDetector.findModeFromFrameBuffer(frameBuffer.getBytes())); + + // Then + Assert.assertEquals( + "Tic frame buffer 0x020A41445343 too short to determine TIC Mode!", exception.getMessage()); + } + + @Test + public void test_findModeFromGroupBuffer_historic() { + // Given + String groupBuffer = "\nADCO 031664001115 3\r"; + + // When + TICMode mode = TICModeDetector.findModeFromGroupBuffer(groupBuffer.getBytes()); + + // Then + Assert.assertEquals("TIC group mode mismatch", TICMode.HISTORIC, mode); + } + + @Test + public void test_findModeFromGroupBuffer_standard() { + // Given + String groupBuffer = "\nADSC\t031664001115\t)\r"; + + // When + TICMode mode = TICModeDetector.findModeFromGroupBuffer(groupBuffer.getBytes()); + + // Then + Assert.assertEquals("TIC group mode mismatch", TICMode.STANDARD, mode); + } + + @Test + public void test_findModeFromGroupBuffer_unknown_mode() { + // Given + String groupBuffer = "\nADSC\0031664001115\0)\r"; + + // When + TICMode mode = TICModeDetector.findModeFromGroupBuffer(groupBuffer.getBytes()); + + // Then + Assert.assertEquals("TIC group mode mismatch", null, mode); + } + + @Test + public void test_findModeFromGroupBuffer_null_buffer() { + // Given + byte[] groupBuffer = null; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICModeDetector.findModeFromGroupBuffer(groupBuffer)); + + // Then + Assert.assertEquals( + "Tic group buffer is null, unable to determine TIC Mode!", exception.getMessage()); + } +} diff --git a/src/test/java/tic/frame/checksum/TICChecksumOffsetTest.java b/src/test/java/tic/frame/checksum/TICChecksumOffsetTest.java new file mode 100644 index 0000000..977ca1b --- /dev/null +++ b/src/test/java/tic/frame/checksum/TICChecksumOffsetTest.java @@ -0,0 +1,200 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.checksum; + +import org.junit.Assert; +import org.junit.Test; +import tic.frame.TICMode; + +public class TICChecksumOffsetTest { + @Test + public void test_getOffsetBegin() { + // When + int offsetBegin = TICChecksumOffset.getOffsetBegin(); + + // Then + Assert.assertEquals(1, offsetBegin); + } + + @Test + public void test_getOffsetEnd_historic() { + // Given + byte[] groupBuffer = "\nADCO 031664001115 3\r".getBytes(); + + // When + int offsetEnd = TICChecksumOffset.getOffsetEnd(groupBuffer, TICMode.HISTORIC); + + // Then + Assert.assertEquals(18, offsetEnd); + } + + @Test + public void test_getOffsetEnd_standard() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t)\r".getBytes(); + + // When + int offsetEnd = TICChecksumOffset.getOffsetEnd(groupBuffer, TICMode.STANDARD); + + // Then + Assert.assertEquals(19, offsetEnd); + } + + @Test + public void test_getOffsetEnd_without_mode() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t)\r".getBytes(); + + // When + int offsetEnd = TICChecksumOffset.getOffsetEnd(groupBuffer); + + // Then + Assert.assertEquals(19, offsetEnd); + } + + @Test + public void test_getOffsetEnd_auto_with_historic() { + // Given + byte[] groupBuffer = "\nADCO 031664001115 3\r".getBytes(); + + // When + int offsetEnd = TICChecksumOffset.getOffsetEnd(groupBuffer, TICMode.AUTO); + + // Then + Assert.assertEquals(18, offsetEnd); + } + + @Test + public void test_getOffsetEnd_auto_with_standard() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t)\r".getBytes(); + + // When + int offsetEnd = TICChecksumOffset.getOffsetEnd(groupBuffer, TICMode.AUTO); + + // Then + Assert.assertEquals(19, offsetEnd); + } + + @Test + public void test_getOffsetEnd_with_nullBuffer() { + // When + NullPointerException exception = + Assert.assertThrows( + NullPointerException.class, + () -> TICChecksumOffset.getOffsetEnd(null, TICMode.STANDARD)); + + // Then + Assert.assertEquals("groupBuffer must not be null", exception.getMessage()); + } + + @Test + public void test_getOffsetEnd_with_nullMode() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t)\r".getBytes(); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICChecksumOffset.getOffsetEnd(groupBuffer, null)); + + // Then + Assert.assertEquals("mode must not be null", exception.getMessage()); + } + + @Test + public void test_getOffsetEnd_with_invalidMode() { + // Given + byte[] groupBuffer = "\nADSC\n031664001115\n)\r".getBytes(); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICChecksumOffset.getOffsetEnd(groupBuffer, TICMode.AUTO)); + + // Then + Assert.assertEquals("Unable to determine TIC mode from group buffer", exception.getMessage()); + } + + @Test + public void test_checkBufferOffsets_with_invalidOffsetBegin() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t)\r".getBytes(); + int offsetBegin = -1; + int offsetEnd = 5; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICChecksumOffset.checkBufferOffsets(groupBuffer, offsetBegin, offsetEnd)); + + // Then + Assert.assertEquals( + "Invalid offsetBegin for checksum computation (must be positive)", exception.getMessage()); + } + + @Test + public void test_checkBufferOffsets_with_invalidOffsetEnd() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t)\r".getBytes(); + int offsetBegin = 1; + int offsetEnd = groupBuffer.length + 1; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICChecksumOffset.checkBufferOffsets(groupBuffer, offsetBegin, offsetEnd)); + + // Then + Assert.assertEquals( + "Invalid offsetEnd for checksum computation (must be lower than data length)", + exception.getMessage()); + } + + @Test + public void test_checkBufferOffsets_with_invalidOffsetRange() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t)\r".getBytes(); + int offsetBegin = 5; + int offsetEnd = 3; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICChecksumOffset.checkBufferOffsets(groupBuffer, offsetBegin, offsetEnd)); + + // Then + Assert.assertEquals( + "Invalid offset range for checksum computation (offsetBegin must be lower than offsetEnd)", + exception.getMessage()); + } + + @Test + public void test_checkBufferOffsets_with_invalidLength() { + // Given + byte[] groupBuffer = "\nADSC\t05\t)\r".getBytes(); + int offsetBegin = 0; + int offsetEnd = 11; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICChecksumOffset.checkBufferOffsets(groupBuffer, offsetBegin, offsetEnd)); + + // Then + Assert.assertEquals( + "Invalid length for checksum computation (length must be lower than data length)", + exception.getMessage()); + } +} diff --git a/src/test/java/tic/frame/checksum/TICChecksumTest.java b/src/test/java/tic/frame/checksum/TICChecksumTest.java new file mode 100644 index 0000000..d48cf94 --- /dev/null +++ b/src/test/java/tic/frame/checksum/TICChecksumTest.java @@ -0,0 +1,140 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.checksum; + +import org.junit.Assert; +import org.junit.Test; +import tic.frame.TICMode; + +public class TICChecksumTest { + + @Test + public void test_verifyChecksum_valid_historic() { + // Given + byte[] groupBuffer = "\nADCO 031664001115 3\r".getBytes(); + + // When + boolean isValid = TICChecksum.verifyChecksum(groupBuffer, TICMode.HISTORIC); + + // Then + Assert.assertTrue(isValid); + } + + @Test + public void test_verifyChecksum_valid_standard() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t)\r".getBytes(); + + // When + boolean isValid = TICChecksum.verifyChecksum(groupBuffer, TICMode.STANDARD); + + // Then + Assert.assertTrue(isValid); + } + + @Test + public void test_verifyChecksum_valid_auto_with_historic() { + // Given + byte[] groupBuffer = "\nADCO 031664001115 3\r".getBytes(); + + // When + boolean isValid = TICChecksum.verifyChecksum(groupBuffer, TICMode.AUTO); + + // Then + Assert.assertTrue(isValid); + } + + @Test + public void test_verifyChecksum_valid_auto_with_standard() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t)\r".getBytes(); + + // When + boolean isValid = TICChecksum.verifyChecksum(groupBuffer, TICMode.AUTO); + + // Then + Assert.assertTrue(isValid); + } + + @Test + public void test_verifyChecksum_invalid_historic() { + // Given + byte[] groupBuffer = "\nADCO 031664001115 4\r".getBytes(); + + // When + boolean isValid = TICChecksum.verifyChecksum(groupBuffer, TICMode.HISTORIC); + + // Then + Assert.assertFalse(isValid); + } + + @Test + public void test_verifyChecksum_invalid_standard() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t*\r".getBytes(); + + // When + boolean isValid = TICChecksum.verifyChecksum(groupBuffer, TICMode.STANDARD); + + // Then + Assert.assertFalse(isValid); + } + + @Test + public void test_verifyChecksum_invalid_auto_with_historic() { + // Given + byte[] groupBuffer = "\nADCO 031664001115 4\r".getBytes(); + + // When + boolean isValid = TICChecksum.verifyChecksum(groupBuffer, TICMode.AUTO); + + // Then + Assert.assertFalse(isValid); + } + + @Test + public void test_verifyChecksum_invalid_auto_with_standard() { + // Given + byte[] groupBuffer = "\nADSC\t031664001115\t*\r".getBytes(); + + // When + boolean isValid = TICChecksum.verifyChecksum(groupBuffer, TICMode.AUTO); + + // Then + Assert.assertFalse(isValid); + } + + @Test + public void test_verifyChecksum_historic_with_tooShortBuffer() { + // Given: only DATA, missing separator/checksum payload + byte[] groupBuffer = "\nA\r".getBytes(); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICChecksum.verifyChecksum(groupBuffer, TICMode.HISTORIC)); + + // Then + Assert.assertTrue(exception.getMessage().contains("offset range")); + } + + @Test + public void test_verifyChecksum_standard_with_tooShortBuffer() { + // Given: truncated group without checksum and carriage return + byte[] groupBuffer = "\n".getBytes(); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICChecksum.verifyChecksum(groupBuffer, TICMode.STANDARD)); + // Then + Assert.assertTrue(exception.getMessage().contains("offset range")); + } +} diff --git a/src/test/java/tic/frame/codec/TICFrameCodecTest.java b/src/test/java/tic/frame/codec/TICFrameCodecTest.java new file mode 100644 index 0000000..0097689 --- /dev/null +++ b/src/test/java/tic/frame/codec/TICFrameCodecTest.java @@ -0,0 +1,295 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.codec; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Arrays; +import org.junit.Assert; +import org.junit.Test; +import tic.ResourceLoader; +import tic.frame.TICFrame; +import tic.frame.TICMode; +import tic.frame.group.TICGroup; + +public class TICFrameCodecTest { + + @Test + public void test_decode_historic() throws IOException, URISyntaxException { + // Given + byte[] frameBuffer = ResourceLoader.readAllBytes("/tic/frame/codec/ticFrameHistoric.txt"); + + // When + TICFrame frame = TICFrameCodec.decode(frameBuffer); + + // Then + Assert.assertEquals(TICMode.HISTORIC, frame.getMode()); + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE"), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104"), + new TICGroup("PTEC", "TH.."), + new TICGroup("IINST", "000"), + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")) + .forEach( + expectedGroup -> { + Assert.assertTrue( + "Expected group " + expectedGroup + " not found in frame groups", + frame.getGroupList().contains(expectedGroup)); + }); + } + + @Test + public void test_decode_standard() throws IOException, URISyntaxException { + // Given + byte[] frameBuffer = ResourceLoader.readAllBytes("/tic/frame/codec/ticFrameStandard.txt"); + + // When + TICFrame frame = TICFrameCodec.decode(frameBuffer); + + // Then + Assert.assertEquals(TICMode.STANDARD, frame.getMode()); + Arrays.asList( + new TICGroup("ADSC", "031664001115"), + new TICGroup("VTIC", "02"), + new TICGroup("DATE", "E170915101601\t"), + new TICGroup("NGTF", "H PLEINE/CREUSE "), + new TICGroup("LTARF", "INDEX INACTIF 2 "), + new TICGroup("EAST", "000000000"), + new TICGroup("EASF01", "000000000"), + new TICGroup("EASF02", "000000000"), + new TICGroup("EASF03", "000000000"), + new TICGroup("EASF04", "000000000"), + new TICGroup("EASF05", "000000000"), + new TICGroup("EASF06", "000000000"), + new TICGroup("EASF07", "000000000"), + new TICGroup("EASF08", "000000000"), + new TICGroup("EASF09", "000000000"), + new TICGroup("EASF10", "000000000"), + new TICGroup("EASD01", "000000000"), + new TICGroup("EASD02", "000000000"), + new TICGroup("EASD03", "000000000"), + new TICGroup("EASD04", "000000000"), + new TICGroup("IRMS1", "000"), + new TICGroup("URMS1", "230"), + new TICGroup("PREF", "03"), + new TICGroup("PCOUP", "02"), + new TICGroup("SINSTS", "00000"), + new TICGroup("SMAXSN", "E170915100926\t00000"), + new TICGroup("SMAXSN-1", "E170913000000\t00000"), + new TICGroup("CCASN", "E170913113000\t00000"), + new TICGroup("CCASN-1", "E170913110000\t00000"), + new TICGroup("UMOY1", "E170915101500\t230"), + new TICGroup("STGE", "000A4411"), + new TICGroup("MSG1", "PAS DE MESSAGE "), + new TICGroup("PRM", "00000000000000"), + new TICGroup("RELAIS", "000"), + new TICGroup("NTARF", "02"), + new TICGroup("NJOURF", "00"), + new TICGroup("NJOURF+1", "00"), + new TICGroup( + "PJOURF+1", + "00008002 0100C001 06008002 1230C001 15308002 NONUTILE NONUTILE NONUTILE NONUTILE" + + " NONUTILE NONUTILE")) + .forEach( + expectedGroup -> { + Assert.assertTrue( + "Expected group " + expectedGroup + " not found in frame groups", + frame.getGroupList().contains(expectedGroup)); + }); + } + + @Test + public void test_encode_historic() throws IOException, URISyntaxException { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE"), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104"), + new TICGroup("PTEC", "TH.."), + new TICGroup("IINST", "000"), + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000")) + .forEach( + group -> { + frame.addGroup(group); + }); + + // When + byte[] encodedFrameBuffer = TICFrameCodec.encode(frame); + + // Then + byte[] expectedFrameBuffer = + ResourceLoader.readAllBytes("/tic/frame/codec/ticFrameHistoric.txt"); + Assert.assertArrayEquals(expectedFrameBuffer, encodedFrameBuffer); + } + + @Test + public void test_encode_standard() throws IOException, URISyntaxException { + // Given + TICFrame frame = new TICFrame(TICMode.STANDARD); + Arrays.asList( + new TICGroup("ADSC", "031664001115"), + new TICGroup("VTIC", "02"), + new TICGroup("DATE", "E170915101601\t"), + new TICGroup("NGTF", "H PLEINE/CREUSE "), + new TICGroup("LTARF", "INDEX INACTIF 2 "), + new TICGroup("EAST", "000000000"), + new TICGroup("EASF01", "000000000"), + new TICGroup("EASF02", "000000000"), + new TICGroup("EASF03", "000000000"), + new TICGroup("EASF04", "000000000"), + new TICGroup("EASF05", "000000000"), + new TICGroup("EASF06", "000000000"), + new TICGroup("EASF07", "000000000"), + new TICGroup("EASF08", "000000000"), + new TICGroup("EASF09", "000000000"), + new TICGroup("EASF10", "000000000"), + new TICGroup("EASD01", "000000000"), + new TICGroup("EASD02", "000000000"), + new TICGroup("EASD03", "000000000"), + new TICGroup("EASD04", "000000000"), + new TICGroup("IRMS1", "000"), + new TICGroup("URMS1", "230"), + new TICGroup("PREF", "03"), + new TICGroup("PCOUP", "02", false), + new TICGroup("SINSTS", "00000"), + new TICGroup("SMAXSN", "E170915100926\t00000"), + new TICGroup("SMAXSN-1", "E170913000000\t00000"), + new TICGroup("CCASN", "E170913113000\t00000"), + new TICGroup("CCASN-1", "E170913110000\t00000"), + new TICGroup("UMOY1", "E170915101500\t230"), + new TICGroup("STGE", "000A4411"), + new TICGroup("MSG1", "PAS DE MESSAGE "), + new TICGroup("PRM", "00000000000000"), + new TICGroup("RELAIS", "000"), + new TICGroup("NTARF", "02"), + new TICGroup("NJOURF", "00"), + new TICGroup("NJOURF+1", "00"), + new TICGroup( + "PJOURF+1", + "00008002 0100C001 06008002 1230C001 15308002 NONUTILE NONUTILE NONUTILE NONUTILE" + + " NONUTILE NONUTILE")) + .forEach( + group -> { + frame.addGroup(group); + }); + + // When + byte[] encodedFrameBuffer = TICFrameCodec.encode(frame); + + // Then + byte[] expectedFrameBuffer = + ResourceLoader.readAllBytes("/tic/frame/codec/ticFrameStandard_PCOUP_Invalid.txt"); + Assert.assertArrayEquals(expectedFrameBuffer, encodedFrameBuffer); + } + + @Test + public void test_encode_with_nullFrame() { + // Given + TICFrame frame = null; + + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> TICFrameCodec.encode(frame)); + + // Then + Assert.assertEquals("frame cannot be null", exception.getMessage()); + } + + @Test + public void test_decode_with_nullBuffer() { + // Given + byte[] frameBuffer = null; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICFrameCodec.decode(frameBuffer)); + + // Then + Assert.assertEquals("frameBuffer cannot be null", exception.getMessage()); + } + + @Test + public void test_decode_with_invalidLength() { + // Given + byte[] frameBuffer = new byte[3]; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICFrameCodec.decode(frameBuffer)); + + // Then + Assert.assertEquals("frameBuffer length must be >= 4 bytes", exception.getMessage()); + } + + @Test + public void test_decode_with_invalidBegin() { + // Given + byte[] frameBuffer = new byte[] {0x00, 0x01, 0x02, 0x03}; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICFrameCodec.decode(frameBuffer)); + + // Then + Assert.assertEquals("frameBuffer begin must be 2", exception.getMessage()); + } + + @Test + public void test_decode_with_invalidEnd() { + // Given + byte[] frameBuffer = new byte[] {0x02, 0x0A, 0x0D, 0x00}; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICFrameCodec.decode(frameBuffer)); + + // Then + Assert.assertEquals("frameBuffer end must be 3", exception.getMessage()); + } + + @Test + public void test_decode_with_bufferTooShort_for_modeDetection() { + // Given + byte[] frameBuffer = new byte[] {0x02, 0x0A, 'A', 0x0D, 0x03}; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICFrameCodec.decode(frameBuffer)); + + // Then + Assert.assertEquals( + "Tic frame buffer 0x020A410D03 too short to determine TIC Mode!", exception.getMessage()); + } + + @Test + public void test_decode_with_invalidBuffer_for_modeDetection() { + // Given + byte[] frameBuffer = new byte[] {0x02, 0x0A, 'A', 'D', 'C', 'C', 0x0D, 0x03}; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICFrameCodec.decode(frameBuffer)); + + // Then + Assert.assertEquals("Unable to determine TIC Mode from frame buffer!", exception.getMessage()); + } +} diff --git a/src/test/java/tic/frame/codec/TICFrameJsonEncoderTest.java b/src/test/java/tic/frame/codec/TICFrameJsonEncoderTest.java new file mode 100644 index 0000000..4ea7866 --- /dev/null +++ b/src/test/java/tic/frame/codec/TICFrameJsonEncoderTest.java @@ -0,0 +1,177 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.codec; + +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; + +import org.json.JSONException; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tic.ResourceLoader; +import tic.frame.TICFrame; +import tic.frame.TICMode; +import tic.frame.group.TICGroup; + +public class TICFrameJsonEncoderTest { + + static List HISTORIC_GROUPS; + static List STANDARD_GROUPS; + + static { + HISTORIC_GROUPS = + Arrays.asList( + new TICGroup("ADCO", "812164417227"), + new TICGroup("OPTARIF", "BASE"), + new TICGroup("ISOUSC", "15"), + new TICGroup("BASE", "000000104"), + new TICGroup("PTEC", "TH.."), + new TICGroup("IINST", "000"), + new TICGroup("IMAX", "000"), + new TICGroup("PAPP", "00000"), + new TICGroup("MOTDETAT", "000000", false)); + + STANDARD_GROUPS = + Arrays.asList( + new TICGroup("ADSC", "031664001115"), + new TICGroup("VTIC", "02"), + new TICGroup("DATE", "E170915101601\t"), + new TICGroup("NGTF", "H PLEINE/CREUSE "), + new TICGroup("LTARF", "INDEX INACTIF 2 "), + new TICGroup("EAST", "000000000"), + new TICGroup("EASF01", "000000000"), + new TICGroup("EASF02", "000000000"), + new TICGroup("EASF03", "000000000"), + new TICGroup("EASF04", "000000000"), + new TICGroup("EASF05", "000000000"), + new TICGroup("EASF06", "000000000"), + new TICGroup("EASF07", "000000000"), + new TICGroup("EASF08", "000000000"), + new TICGroup("EASF09", "000000000"), + new TICGroup("EASF10", "000000000"), + new TICGroup("EASD01", "000000000"), + new TICGroup("EASD02", "000000000"), + new TICGroup("EASD03", "000000000"), + new TICGroup("EASD04", "000000000"), + new TICGroup("IRMS1", "000"), + new TICGroup("URMS1", "230"), + new TICGroup("PREF", "03"), + new TICGroup("PCOUP", "02", false), + new TICGroup("SINSTS", "00000"), + new TICGroup("SMAXSN", "E170915100926\t00000"), + new TICGroup("SMAXSN-1", "E170913000000\t00000"), + new TICGroup("CCASN", "E170913113000\t00000"), + new TICGroup("CCASN-1", "E170913110000\t00000"), + new TICGroup("UMOY1", "E170915101500\t230"), + new TICGroup("STGE", "000A4411"), + new TICGroup("MSG1", "PAS DE MESSAGE "), + new TICGroup("PRM", "00000000000000"), + new TICGroup("RELAIS", "000"), + new TICGroup("NTARF", "02"), + new TICGroup("NJOURF", "00"), + new TICGroup("NJOURF+1", "00"), + new TICGroup( + "PJOURF+1", + "00008002 0100C001 06008002 1230C001 15308002 NONUTILE NONUTILE NONUTILE NONUTILE" + + " NONUTILE NONUTILE")); + } + + @Test + public void test_encode_full_details() throws IOException, URISyntaxException, JSONException { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + HISTORIC_GROUPS.forEach(group -> frame.addGroup(group)); + String actualJsonText = null; + Exception exception = null; + + // When + try { + actualJsonText = TICFrameDetailledCodec.getInstance().encodeToJsonString(frame); + } catch (Exception e) { + exception = e; + } + + // Then + assertNull("Exception should not be thrown during encoding", exception); + String expectedJsonText = + ResourceLoader.readString("/tic/frame/codec/ticFrameHistoric_full_details.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } + + @Test + public void test_encode_summarized() throws IOException, URISyntaxException, JSONException { + // Given + TICFrame frame = new TICFrame(TICMode.STANDARD); + STANDARD_GROUPS.forEach(group -> frame.addGroup(group)); + String actualJsonText = null; + Exception exception = null; + + // When + try { + actualJsonText = TICFrameSummarizedCodec.getInstance().encodeToJsonString(frame); + } catch (Exception e) { + exception = e; + } + + // Then + assertNull("Exception should not be thrown during encoding", exception); + String expectedJsonText = + ResourceLoader.readString("/tic/frame/codec/ticFrameStandard_summarized.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } + + @Test + public void test_encode_full_details_negative_indent() + throws IOException, URISyntaxException, JSONException { + // Given + TICFrame frame = new TICFrame(TICMode.HISTORIC); + HISTORIC_GROUPS.forEach(group -> frame.addGroup(group)); + String actualJsonText = null; + Exception exception = null; + + // When + try { + actualJsonText = TICFrameDetailledCodec.getInstance().encodeToJsonString(frame, -1); + } catch (Exception e) { + exception = e; + } + + // Then + assertNull("Exception should not be thrown during encoding", exception); + String expectedJsonText = + ResourceLoader.readString("/tic/frame/codec/ticFrameHistoric_full_details.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } + + @Test + public void test_encode_summarized_negative_indent() + throws IOException, URISyntaxException, JSONException { + // Given + TICFrame frame = new TICFrame(TICMode.STANDARD); + STANDARD_GROUPS.forEach(group -> frame.addGroup(group)); + String actualJsonText = null; + Exception exception = null; + + // When + try { + actualJsonText = TICFrameSummarizedCodec.getInstance().encodeToJsonString(frame, -1); + } catch (Exception e) { + exception = e; + } + + // Then + assertNull("Exception should not be thrown during encoding", exception); + String expectedJsonText = + ResourceLoader.readString("/tic/frame/codec/ticFrameStandard_summarized.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } +} diff --git a/src/test/java/tic/frame/delimiter/TICFrameDelimiterTest.java b/src/test/java/tic/frame/delimiter/TICFrameDelimiterTest.java new file mode 100644 index 0000000..a0972a4 --- /dev/null +++ b/src/test/java/tic/frame/delimiter/TICFrameDelimiterTest.java @@ -0,0 +1,36 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.delimiter; + +import org.junit.Assert; +import org.junit.Test; + +public class TICFrameDelimiterTest { + + @Test + public void test_value_begin() { + // Given + + // When + byte begin = TICFrameDelimiter.BEGIN.getValue(); + + // Then + Assert.assertEquals("TIC frame begin invalid", 0x02, begin); + } + + @Test + public void test_value_end() { + // Given + + // When + byte end = TICFrameDelimiter.END.getValue(); + + // Then + Assert.assertEquals("TIC frame end invalid", 0x03, end); + } +} diff --git a/src/test/java/tic/frame/delimiter/TICGroupDelimiterTest.java b/src/test/java/tic/frame/delimiter/TICGroupDelimiterTest.java new file mode 100644 index 0000000..ef60bc7 --- /dev/null +++ b/src/test/java/tic/frame/delimiter/TICGroupDelimiterTest.java @@ -0,0 +1,35 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.delimiter; + +import org.junit.Assert; +import org.junit.Test; + +public class TICGroupDelimiterTest { + @Test + public void test_value_begin() { + // Given + + // When + byte begin = TICGroupDelimiter.BEGIN.getValue(); + + // Then + Assert.assertEquals("TIC group begin invalid", '\n', begin); + } + + @Test + public void test_value_end() { + // Given + + // When + byte end = TICGroupDelimiter.END.getValue(); + + // Then + Assert.assertEquals("TIC group end invalid", '\r', end); + } +} diff --git a/src/test/java/tic/frame/delimiter/TICSeparatorTest.java b/src/test/java/tic/frame/delimiter/TICSeparatorTest.java new file mode 100644 index 0000000..babf3e1 --- /dev/null +++ b/src/test/java/tic/frame/delimiter/TICSeparatorTest.java @@ -0,0 +1,86 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.delimiter; + +import org.junit.Assert; +import org.junit.Test; +import tic.frame.TICMode; + +public class TICSeparatorTest { + + @Test + public void test_value_historic() { + // Given + + // When + byte separator = TICSeparator.HISTORIC.getValue(); + + // Then + Assert.assertEquals("Historic TIC separator invalid", ' ', separator); + } + + @Test + public void test_value_standard() { + // Given + + // When + byte separator = TICSeparator.STANDARD.getValue(); + // Then + Assert.assertEquals("Standard TIC separator invalid", '\t', separator); + } + + @Test + public void test_value_from_historic_value() { + // Given + TICMode mode = TICMode.HISTORIC; + + // When + byte separator = TICSeparator.getValueFromMode(mode); + // Then + Assert.assertEquals("Historic TIC separator invalid", ' ', separator); + } + + @Test + public void test_value_from_standard_value() { + // Given + TICMode mode = TICMode.STANDARD; + + // When + byte separator = TICSeparator.getValueFromMode(mode); + // Then + Assert.assertEquals("Standard TIC separator invalid", '\t', separator); + } + + @Test + public void test_value_from_null_mode() { + // Given + TICMode mode = null; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICSeparator.getValueFromMode(mode)); + + // Then + Assert.assertEquals("cannot get separator value from null mode", exception.getMessage()); + } + + @Test + public void test_value_from_invalid_mode() { + // Given + TICMode mode = TICMode.AUTO; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICSeparator.getValueFromMode(mode)); + + // Then + Assert.assertEquals("cannot get separator value from AUTO mode", exception.getMessage()); + } +} diff --git a/src/test/java/tic/frame/delimiter/TICStartPatternTest.java b/src/test/java/tic/frame/delimiter/TICStartPatternTest.java new file mode 100644 index 0000000..dd71f70 --- /dev/null +++ b/src/test/java/tic/frame/delimiter/TICStartPatternTest.java @@ -0,0 +1,112 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.delimiter; + +import org.junit.Assert; +import org.junit.Test; +import tic.frame.TICMode; + +public class TICStartPatternTest { + + @Test + public void test_historic() { + // Given + + // When + byte[] actualBytes = TICStartPattern.HISTORIC.getValue(); + + // Then + Assert.assertArrayEquals( + "Historic TIC start pattern invalid", + new byte[] {2, '\n', 'A', 'D', 'C', 'O', ' '}, + actualBytes); + } + + @Test + public void test_standard() { + // Given + + // When + byte[] actualBytes = TICStartPattern.STANDARD.getValue(); + + // Then + Assert.assertArrayEquals( + "Standard TIC start pattern invalid", + new byte[] {2, '\n', 'A', 'D', 'S', 'C', '\t'}, + actualBytes); + } + + @Test + public void test_length() { + // Given + + // When + int actualLength = TICStartPattern.length(); + + // Then + Assert.assertEquals("TIC start pattern length invalid", 7, actualLength); + } + + @Test + public void test_value_from_historic_value() { + // Given + TICMode mode = TICMode.HISTORIC; + + // When + byte[] actualBytes = TICStartPattern.getValueFromMode(mode); + + // Then + Assert.assertArrayEquals( + "Historic TIC start pattern invalid", + new byte[] {2, '\n', 'A', 'D', 'C', 'O', ' '}, + actualBytes); + } + + @Test + public void test_value_from_standard_value() { + // Given + TICMode mode = TICMode.STANDARD; + + // When + byte[] actualBytes = TICStartPattern.getValueFromMode(mode); + + // Then + Assert.assertArrayEquals( + "Standard TIC start pattern invalid", + new byte[] {2, '\n', 'A', 'D', 'S', 'C', '\t'}, + actualBytes); + } + + @Test + public void test_value_from_null_mode() { + // Given + TICMode mode = null; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICStartPattern.getValueFromMode(mode)); + + // Then + Assert.assertEquals("cannot get start pattern value from null mode", exception.getMessage()); + } + + @Test + public void test_value_from_invalid_mode() { + // Given + TICMode mode = TICMode.AUTO; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICStartPattern.getValueFromMode(mode)); + + // Then + Assert.assertEquals("cannot get start pattern value from AUTO mode", exception.getMessage()); + } +} diff --git a/src/test/java/tic/frame/group/TICGroupTest.java b/src/test/java/tic/frame/group/TICGroupTest.java new file mode 100644 index 0000000..5531422 --- /dev/null +++ b/src/test/java/tic/frame/group/TICGroupTest.java @@ -0,0 +1,222 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.frame.group; + +import org.junit.Assert; +import org.junit.Test; + +import tic.frame.TICMode; + +public class TICGroupTest { + @Test + public void test_default_constructor() { + // Given + + // When + TICGroup group = new TICGroup("ADCO", "12345"); + + // Then + Assert.assertEquals("TIC group label mismatch", "ADCO", group.getLabel()); + Assert.assertEquals("TIC group value mismatch", "12345", group.getValue()); + Assert.assertEquals("TIC group isValid mismatch", true, group.isValid()); + } + + @Test + public void test_full_constructor() { + // Given + + // When + TICGroup group = new TICGroup("ADSC", "156FE", false); + + // Then + Assert.assertEquals("TIC group label mismatch", "ADSC", group.getLabel()); + Assert.assertEquals("TIC group value mismatch", "156FE", group.getValue()); + Assert.assertEquals("TIC group isValid mismatch", false, group.isValid()); + } + + @Test + public void test_equals_identical() { + // Given + TICGroup group = new TICGroup("ADSC", "156FE", false); + + // When + boolean matches = group.equals(group); + + // Then + Assert.assertEquals("TIC group first mismatch itself", true, matches); + } + + @Test + public void test_equals_match() { + // Given + TICGroup firstGroup = new TICGroup("ADSC", "156FE", false); + TICGroup secondGroup = new TICGroup("ADSC", "156FE", false); + + // When + boolean firstMatchesSecond = firstGroup.equals(secondGroup); + boolean secondMatchesFirst = secondGroup.equals(firstGroup); + + // Then + Assert.assertEquals("TIC group first mismatch second", true, firstMatchesSecond); + Assert.assertEquals("TIC group second mismatch first", true, secondMatchesFirst); + } + + @Test + public void test_equals_label_mismatch() { + // Given + TICGroup firstGroup = new TICGroup("ADSC", "156FE", false); + TICGroup secondGroup = new TICGroup("ADCO", "156FE", false); + + // When + boolean firstMatchesSecond = firstGroup.equals(secondGroup); + boolean secondMatchesFirst = secondGroup.equals(firstGroup); + + // Then + Assert.assertEquals("TIC group first should not match second", false, firstMatchesSecond); + Assert.assertEquals("TIC group second should not match first", false, secondMatchesFirst); + } + + @Test + public void test_equals_value_mismatch() { + // Given + TICGroup firstGroup = new TICGroup("ADSC", "156FE", false); + TICGroup secondGroup = new TICGroup("ADSC", "156FF", false); + + // When + boolean firstMatchesSecond = firstGroup.equals(secondGroup); + boolean secondMatchesFirst = secondGroup.equals(firstGroup); + + // Then + Assert.assertEquals("TIC group first should not match second", false, firstMatchesSecond); + Assert.assertEquals("TIC group second should not match first", false, secondMatchesFirst); + } + + @Test + public void test_equals_isValid_mismatch() { + // Given + TICGroup firstGroup = new TICGroup("ADSC", "156FE", false); + TICGroup secondGroup = new TICGroup("ADSC", "156FE", true); + + // When + boolean firstMatchesSecond = firstGroup.equals(secondGroup); + boolean secondMatchesFirst = secondGroup.equals(firstGroup); + + // Then + Assert.assertEquals("TIC group first should not match second", false, firstMatchesSecond); + Assert.assertEquals("TIC group second should not match first", false, secondMatchesFirst); + } + + @Test + public void test_equals_label_and_value_mismatch() { + // Given + TICGroup firstGroup = new TICGroup("ADCO", "156FE", false); + TICGroup secondGroup = new TICGroup("ADSC", "156FF", false); + + // When + boolean firstMatchesSecond = firstGroup.equals(secondGroup); + boolean secondMatchesFirst = secondGroup.equals(firstGroup); + + // Then + Assert.assertEquals("TIC group first should not match second", false, firstMatchesSecond); + Assert.assertEquals("TIC group second should not match first", false, secondMatchesFirst); + } + + @Test + public void test_equals_label_and_isValid_mismatch() { + // Given + TICGroup firstGroup = new TICGroup("ADCO", "156FF", true); + TICGroup secondGroup = new TICGroup("ADSC", "156FF", false); + + // When + boolean firstMatchesSecond = firstGroup.equals(secondGroup); + boolean secondMatchesFirst = secondGroup.equals(firstGroup); + + // Then + Assert.assertEquals("TIC group first should not match second", false, firstMatchesSecond); + Assert.assertEquals("TIC group second should not match first", false, secondMatchesFirst); + } + + @Test + public void test_equals_value_and_isValid_mismatch() { + // Given + TICGroup firstGroup = new TICGroup("ADCO", "156FF", true); + TICGroup secondGroup = new TICGroup("ADCO", "156FE", false); + + // When + boolean firstMatchesSecond = firstGroup.equals(secondGroup); + boolean secondMatchesFirst = secondGroup.equals(firstGroup); + + // Then + Assert.assertEquals("TIC group first should not match second", false, firstMatchesSecond); + Assert.assertEquals("TIC group second should not match first", false, secondMatchesFirst); + } + + @Test + public void test_equals_full_mismatch() { + // Given + TICGroup firstGroup = new TICGroup("ADSC", "156FF", true); + TICGroup secondGroup = new TICGroup("ADCO", "156FE", false); + + // When + boolean firstMatchesSecond = firstGroup.equals(secondGroup); + boolean secondMatchesFirst = secondGroup.equals(firstGroup); + + // Then + Assert.assertEquals("TIC group first should not match second", false, firstMatchesSecond); + Assert.assertEquals("TIC group second should not match first", false, secondMatchesFirst); + } + + @Test + public void test_equals_null() { + // Given + TICGroup group = new TICGroup("ADSC", "156FE", false); + + // When + boolean matches = group.equals(null); + + // Then + Assert.assertEquals("TIC group match null", false, matches); + } + + @Test + public void test_equals_other_class() { + // Given + TICGroup group = new TICGroup("ADSC", "156FE", false); + + // When + boolean matches = group.equals((Object) TICMode.HISTORIC); + + // Then + Assert.assertEquals("TIC group match other class", false, matches); + } + + @Test + public void test_hashCode() { + // Given + TICGroup group = new TICGroup("ADSC", "156FE", false); + + // When + int hashCode = group.hashCode(); + + // Then + Assert.assertEquals("TIC group hash code unexpected", 48891225, hashCode); + } + + @Test + public void test_toString() { + // Given + TICGroup group = new TICGroup("ADSC", "156FE", false); + + // When + String text = group.toString(); + + // Then + Assert.assertEquals( + "TIC group string unexpected", "(label=ADSC, value=156FE, isValid=false)", text); + } +} diff --git a/src/test/java/enedis/lab/io/PortFinderMock.java b/src/test/java/tic/io/PortFinderMock.java similarity index 65% rename from src/test/java/enedis/lab/io/PortFinderMock.java rename to src/test/java/tic/io/PortFinderMock.java index d1421ec..af6c2da 100644 --- a/src/test/java/enedis/lab/io/PortFinderMock.java +++ b/src/test/java/tic/io/PortFinderMock.java @@ -5,31 +5,27 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.io; +package tic.io; -import enedis.lab.mock.FunctionCall; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; import java.util.ArrayList; import java.util.List; +import tic.mock.FunctionCall; @SuppressWarnings("javadoc") public class PortFinderMock implements PortFinder { - public List findAllCalls = new ArrayList(); - public DataList descriptorList = new DataArrayList(); + public List findAllCalls = new ArrayList<>(); + public List descriptorList = new ArrayList<>(); - public PortFinderMock() { - this.descriptorList = new DataArrayList(); - } + public PortFinderMock() {} @Override - public DataList findAll() { + public List findAll() { this.findAllCalls.add(new FunctionCall()); return this.getDescriptors(); } - public DataList getDescriptors() { - DataList descriptors = new DataArrayList(); + public List getDescriptors() { + List descriptors = new ArrayList<>(); synchronized (this.descriptorList) { descriptors.addAll(this.descriptorList); @@ -38,7 +34,7 @@ public DataList getDescriptors() { return descriptors; } - public DataList setDescriptors(DataList descriptors) { + public List setDescriptors(List descriptors) { synchronized (this.descriptorList) { this.descriptorList.clear(); this.descriptorList.addAll(descriptors); diff --git a/src/test/java/tic/io/modem/ModemDescriptorTest.java b/src/test/java/tic/io/modem/ModemDescriptorTest.java new file mode 100644 index 0000000..21b89fe --- /dev/null +++ b/src/test/java/tic/io/modem/ModemDescriptorTest.java @@ -0,0 +1,515 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.modem; + +import org.junit.Assert; +import org.junit.Test; +import tic.frame.TICMode; +import tic.io.serialport.SerialPortDescriptor; +import tic.io.usb.UsbPortDescriptor; + +public class ModemDescriptorTest { + + @Test + public void test_instance_with_all_parameters() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 0x6001) + .vendorId((short) 0x0403) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD); + // When + ModemDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 0x6001), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 0x0403), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + Assert.assertEquals(ModemType.MICHAUD, descriptor.modemType()); + } + + @Test + public void test_copy_with_SerialPortDesciptor() { + // Given + SerialPortDescriptor serialDescriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .build(); + // When + ModemDescriptor modemDescriptor = + new ModemDescriptor.Builder<>().copy(serialDescriptor).build(); + + // Then + Assert.assertEquals("ABCD1", modemDescriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", modemDescriptor.portName()); + Assert.assertEquals("USB Serial Port", modemDescriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), modemDescriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), modemDescriptor.vendorId()); + Assert.assertEquals("test_product", modemDescriptor.productName()); + Assert.assertEquals("test_manufacturer", modemDescriptor.manufacturer()); + Assert.assertEquals("SN123", modemDescriptor.serialNumber()); + Assert.assertEquals(null, modemDescriptor.modemType()); + } + + @Test + public void test_copy_with_UsbPortDescriptor() { + // Given + UsbPortDescriptor usbPortDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + ModemDescriptor modemDescriptor = + new ModemDescriptor.Builder<>().copy(usbPortDescriptor).build(); + + // Then + Assert.assertEquals(null, modemDescriptor.portId()); + Assert.assertEquals(null, modemDescriptor.portName()); + Assert.assertEquals(null, modemDescriptor.description()); + Assert.assertEquals(Short.valueOf((short) 10), modemDescriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 11), modemDescriptor.vendorId()); + Assert.assertEquals("test_product", modemDescriptor.productName()); + Assert.assertEquals("test_manufacturer", modemDescriptor.manufacturer()); + Assert.assertEquals("SN123", modemDescriptor.serialNumber()); + Assert.assertEquals(null, modemDescriptor.modemType()); + } + + @Test + public void test_instance_with_null_productId() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId(null) + .vendorId((short) 0x0403) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD); + // When + ModemDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 0x6001), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 0x0403), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + Assert.assertEquals(ModemType.MICHAUD, descriptor.modemType()); + } + + @Test + public void test_instance_with_null_vendorId() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 0x6001) + .vendorId(null) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD); + // When + ModemDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 0x6001), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 0x0403), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + Assert.assertEquals(ModemType.MICHAUD, descriptor.modemType()); + } + + @Test + public void test_instance_with_null_modemType() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 0x6001) + .vendorId((short) 0x0403) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(null); + // When + ModemDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(null, descriptor.productId()); + Assert.assertEquals(null, descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + Assert.assertEquals(null, descriptor.modemType()); + } + + @Test + public void test_instance_with_missing_productId() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .vendorId((short) 0x0403) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD); + // When + ModemDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 0x6001), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 0x0403), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + Assert.assertEquals(ModemType.MICHAUD, descriptor.modemType()); + } + + @Test + public void test_instance_with_missing_vendorId() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 0x6001) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD); + // When + ModemDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 0x6001), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 0x0403), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + Assert.assertEquals(ModemType.MICHAUD, descriptor.modemType()); + } + + @Test + public void test_instance_with_missing_modemType() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + ModemDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + Assert.assertEquals(null, descriptor.modemType()); + } + + @Test + public void test_instance_with_modemType_and_null_productId() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .modemType(ModemType.MICHAUD) + .productId(null) + .vendorId((short) 0x0403) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + ; + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals( + "Modem type is specified while productId is not provided", exception.getMessage()); + } + + @Test + public void test_instance_with_modemType_and_null_vendorId() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .modemType(ModemType.MICHAUD) + .productId((short) 0x6001) + .vendorId(null) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + ; + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals( + "Modem type is specified while vendorId is not provided", exception.getMessage()); + } + + @Test + public void test_instance_with_modemType_and_inconsistent_productId() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .modemType(ModemType.MICHAUD) + .productId((short) 0x6015) + .vendorId((short) 0x0403) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + ; + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Modem type is inconsistent with the productId", exception.getMessage()); + } + + @Test + public void test_instance_with_modemType_and_inconsistent_vendorId() { + // Given + ModemDescriptor.Builder builder = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .modemType(ModemType.MICHAUD) + .productId((short) 0x6001) + .vendorId((short) 0x0430) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + ; + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Modem type is inconsistent with the vendorId", exception.getMessage()); + } + + @Test + public void test_equals_with_same_instance() { + // Given + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD) + .build(); + // When + boolean match = descriptor.equals(descriptor); + + // Then + Assert.assertEquals(true, match); + } + + @Test + public void test_equals_with_identical_object() { + // Given + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD) + .build(); + + ModemDescriptor otherDescriptor = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD) + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(true, match); + } + + @Test + public void test_equals_with_different_object_type() { + // Given + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD) + .build(); + + // When + boolean match = descriptor.equals((Object) TICMode.HISTORIC); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_modemType() { + // Given + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD) + .build(); + + ModemDescriptor otherDescriptor = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN1234") + .modemType(ModemType.TELEINFO) + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_hashCode() { + // Given + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD) + .build(); + ModemDescriptor sameDescriptor = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD) + .build(); + + // When + int hashCode = descriptor.hashCode(); + int sameHashCode = sameDescriptor.hashCode(); + + // Then + Assert.assertEquals(sameHashCode, hashCode); + } +} diff --git a/src/test/java/tic/io/modem/ModemFinderMock.java b/src/test/java/tic/io/modem/ModemFinderMock.java new file mode 100644 index 0000000..f562577 --- /dev/null +++ b/src/test/java/tic/io/modem/ModemFinderMock.java @@ -0,0 +1,52 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.modem; + +import tic.io.PortFinderMock; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Test utility that mimics a {@link ModemFinder} with in-memory descriptors. */ +public class ModemFinderMock extends PortFinderMock implements ModemFinder { + private final List nativeDescriptorList = new ArrayList<>(); + + public ModemFinderMock() { + super(); + } + + public ModemFinderMock(ModemDescriptor... descriptors) { + super(); + if (descriptors != null) { + this.setDescriptors(Arrays.asList(descriptors)); + } + } + + @Override + public ModemDescriptor findNative(String portName) { + for (ModemDescriptor descriptor : this.nativeDescriptorList) { + if (descriptor.portName() == null && portName == null) { + return descriptor; + } + if (descriptor.portName() != null && descriptor.portName().equals(portName)) { + return descriptor; + } + } + return null; + } + + public void addNativeDescriptor(ModemDescriptor descriptor) { + if (descriptor != null) { + this.nativeDescriptorList.add(descriptor); + } + } + + public void removeNativeDescriptor(ModemDescriptor descriptor) { + this.nativeDescriptorList.remove(descriptor); + } +} diff --git a/src/test/java/tic/io/modem/ModemJsonEncoderTest.java b/src/test/java/tic/io/modem/ModemJsonEncoderTest.java new file mode 100644 index 0000000..de36105 --- /dev/null +++ b/src/test/java/tic/io/modem/ModemJsonEncoderTest.java @@ -0,0 +1,96 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.modem; + +import java.util.List; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tic.ResourceLoader; + +public class ModemJsonEncoderTest { + + @Test + public void test_encode_AllFields() throws Exception { + // Given + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("COM7") + .description("USB Serial Port") + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD) + .build(); + + // When + String actualJsonText = + ModemJsonCodec.getInstance() + .encodeToJsonObject(descriptor).toString(); + + // Then + String expectedJsonText = ResourceLoader.readString("/tic/io/modem/AllFields.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } + + @Test + public void test_encode_Descriptor() throws Exception { + // Given + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .portId("ABCD1") + .portName("COM7") + .description("USB Serial Port") + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .modemType(ModemType.MICHAUD) + .build(); + + // When + String actualJsonText = ModemJsonCodec.getInstance().encodeToJsonString(descriptor); + + // Then + String expectedJsonText = ResourceLoader.readString("/tic/io/modem/Descriptor.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } + + @Test + public void encodeShouldHandleNullModemType() throws Exception { + // Given + ModemDescriptor descriptor = + new ModemDescriptor.Builder<>() + .portName("COM8") + .productId((short) 1234) + .vendorId((short) 5678) + .build(); + + // When + String actualJsonText = + ModemJsonCodec.getInstance() + .encodeToJsonString(descriptor); + + // Then + String expectedJsonText = ResourceLoader.readString("/tic/io/modem/NullModemType.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } + + @Test + public void encodeHandlesNullList() throws Exception { + // Given + List descriptors = null; + + // When + String actualJsonText = + ModemJsonCodec.getInstance().encodeToJsonArray(descriptors).toString(); + + // Then + String expectedJsonText = ResourceLoader.readString("/tic/io/modem/NullList.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } +} diff --git a/src/test/java/tic/io/serialport/SerialPortDescriptorTest.java b/src/test/java/tic/io/serialport/SerialPortDescriptorTest.java new file mode 100644 index 0000000..4498ec8 --- /dev/null +++ b/src/test/java/tic/io/serialport/SerialPortDescriptorTest.java @@ -0,0 +1,879 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.serialport; + +import org.junit.Assert; +import org.junit.Test; +import tic.frame.TICMode; + +public class SerialPortDescriptorTest { + + @Test + public void test_instance_with_all_parameters() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + + // When + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_portId() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId(null) + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(null, descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_portName() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName(null) + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals(null, descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_description() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description(null) + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals(null, descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_productId() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId(null) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(null, descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_vendorId() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId(null) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(null, descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_productName() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName(null) + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals(null, descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_manufacturer() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer(null) + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals(null, descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_serialNumber() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber(null); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals(null, descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_portId() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(null, descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_portName() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals(null, descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_description() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals(null, descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_productId() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(null, descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_vendorId() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(null, descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_productName() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals(null, descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_manufacturer() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .serialNumber("SN123"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals(null, descriptor.manufacturer()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_serialNumber() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer"); + // When + SerialPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals("ABCD1", descriptor.portId()); + Assert.assertEquals("/dev/ttyUSB0", descriptor.portName()); + Assert.assertEquals("USB Serial Port", descriptor.description()); + Assert.assertEquals(Short.valueOf((short) 1), descriptor.productId()); + Assert.assertEquals(Short.valueOf((short) 2), descriptor.vendorId()); + Assert.assertEquals("test_product", descriptor.productName()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals(null, descriptor.serialNumber()); + } + + @Test + public void test_instance_with_empty_portId() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Value 'portId' cannot be empty", exception.getMessage()); + } + + @Test + public void test_instance_with_empty_portName() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + ; + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Value 'portName' cannot be empty", exception.getMessage()); + } + + @Test + public void test_instance_with_empty_description() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + ; + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Value 'description' cannot be empty", exception.getMessage()); + } + + @Test + public void test_instance_with_empty_productName() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("") + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + ; + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Value 'productName' cannot be empty", exception.getMessage()); + } + + @Test + public void test_instance_with_empty_manufacturer() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("") + .serialNumber("SN123"); + ; + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Value 'manufacturer' cannot be empty", exception.getMessage()); + } + + @Test + public void test_instance_with_empty_serialNumber() { + // Given + SerialPortDescriptor.Builder builder = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber(""); + ; + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Value 'serialNumber' cannot be empty", exception.getMessage()); + } + + @Test + public void test_equals_with_same_instance() { + // Given + SerialPortDescriptor descriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId(null) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(descriptor); + + // Then + Assert.assertEquals(true, match); + } + + @Test + public void test_equals_with_identical_object() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + + SerialPortDescriptor otherDescriptor = createStandardDescriptor(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(true, match); + } + + @Test + public void test_equals_with_different_object_type() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + // When + boolean match = descriptor.equals((Object) TICMode.HISTORIC); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_portId() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + + SerialPortDescriptor otherDescriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD2") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_portName() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + + SerialPortDescriptor otherDescriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB1") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_description() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + + SerialPortDescriptor otherDescriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_productId() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + + SerialPortDescriptor otherDescriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 3) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_vendorId() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + + SerialPortDescriptor otherDescriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 3) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_productName() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + + SerialPortDescriptor otherDescriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product 2") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_manufacturer() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + + SerialPortDescriptor otherDescriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer bis") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_serialNumber() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + + SerialPortDescriptor otherDescriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN1234") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_hashCode() { + // Given + SerialPortDescriptor descriptor = createStandardDescriptor(); + SerialPortDescriptor sameDescriptor = createStandardDescriptor(); + + // When + int hashCode = descriptor.hashCode(); + int sameHashCode = sameDescriptor.hashCode(); + + // Then + Assert.assertEquals(sameHashCode, hashCode); + } + + @Test + public void test_isNative_with_only_portName() { + // Given + SerialPortDescriptor descriptor = + new SerialPortDescriptor.Builder<>().portName("/dev/ttyUSB0").build(); + // When + boolean isNative = descriptor.isNative(); + + // Then + Assert.assertEquals(true, isNative); + } + + @Test + public void test_isNative_with_only_portId() { + // Given + SerialPortDescriptor descriptor = new SerialPortDescriptor.Builder<>().portId("ABCD1").build(); + // When + boolean isNative = descriptor.isNative(); + + // Then + Assert.assertEquals(false, isNative); + } + + @Test + public void test_isNative_with_portName_and_portId() { + // Given + SerialPortDescriptor descriptor = + new SerialPortDescriptor.Builder<>().portName("/dev/ttyUSB0").portId("ABCD1").build(); + // When + boolean isNative = descriptor.isNative(); + + // Then + Assert.assertEquals(false, isNative); + } + + private SerialPortDescriptor createStandardDescriptor() { + return new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("/dev/ttyUSB0") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .build(); + } +} diff --git a/src/test/java/tic/io/serialport/SerialPortFinderBaseTest.java b/src/test/java/tic/io/serialport/SerialPortFinderBaseTest.java new file mode 100644 index 0000000..7c1d690 --- /dev/null +++ b/src/test/java/tic/io/serialport/SerialPortFinderBaseTest.java @@ -0,0 +1,101 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.serialport; + +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +public class SerialPortFinderBaseTest { + + @Test + public void test_getInstance() { + // Given + + // When + SerialPortFinder instance1 = SerialPortFinderBase.getInstance(); + SerialPortFinder instance2 = SerialPortFinderBase.getInstance(); + + // Then + Assert.assertNotNull(instance1); + Assert.assertNotNull(instance2); + Assert.assertSame(instance1, instance2); + } + + @Test + public void test_findAll() { + // Given + + // When + List descriptors = SerialPortFinderBase.getInstance().findAll(); + + // Then + Assert.assertNotNull(descriptors); + } + + @Test + public void test_findByPortId() { + // Given + String portId = "portId?"; + + // When + SerialPortDescriptor descriptor = SerialPortFinderBase.getInstance().findByPortId(portId); + + // Then + Assert.assertNull(descriptor); + } + + @Test + public void test_findByPortName() { + // Given + String portName = "portName?"; + // When + SerialPortDescriptor descriptor = SerialPortFinderBase.getInstance().findByPortName(portName); + + // Then + Assert.assertNull(descriptor); + } + + @Test + public void test_findByPortIdOrPortName() { + // Given + String portId = "portId?"; + String portName = "portName?"; + // When + SerialPortDescriptor descriptor = + SerialPortFinderBase.getInstance().findByPortIdOrPortName(portId, portName); + + // Then + Assert.assertNull(descriptor); + } + + @Test + public void test_findNative() { + // Given + String portName = "portName?"; + // When + SerialPortDescriptor descriptor = SerialPortFinderBase.getInstance().findNative(portName); + + // Then + Assert.assertNull(descriptor); + } + + @Test + public void test_findByProductIdAndVendorId() { + // Given + short idProduct = 0; + short idVendor = 0; + // When + List descriptors = + SerialPortFinderBase.getInstance().findByProductIdAndVendorId(idProduct, idVendor); + + // Then + Assert.assertNotNull(descriptors); + Assert.assertEquals(0, descriptors.size()); + } +} diff --git a/src/test/java/tic/io/serialport/SerialPortJsonEncoderTest.java b/src/test/java/tic/io/serialport/SerialPortJsonEncoderTest.java new file mode 100644 index 0000000..a16d087 --- /dev/null +++ b/src/test/java/tic/io/serialport/SerialPortJsonEncoderTest.java @@ -0,0 +1,82 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.serialport; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.json.JSONException; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tic.ResourceLoader; + +public class SerialPortJsonEncoderTest { + + @Test + public void encodeShouldSerializeAllFields() + throws JSONException, IOException, URISyntaxException { + // Given + SerialPortDescriptor descriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName("COM7") + .description("USB Serial Port") + .productId((short) 1) + .vendorId((short) 2) + .productName("test_product") + .manufacturer("test_manufacturer") + .serialNumber("SN123") + .build(); + + // When + String actualJsonText = SerialPortJsonEncoder.encode(Arrays.asList(descriptor), -1); + + // Then + String expectedJsonText = ResourceLoader.readString("/tic/io/serialport/AllFields.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } + + @Test + public void encodeShouldOmitNullifiedStrings() + throws JSONException, IOException, URISyntaxException { + // Given + SerialPortDescriptor descriptor = + new SerialPortDescriptor.Builder<>() + .portId("ABCD1") + .portName(null) + .description(null) + .productId((short) 1) + .vendorId((short) 2) + .productName(null) + .manufacturer(null) + .serialNumber(null) + .build(); + + // When + String actualJsonText = SerialPortJsonEncoder.encode(Collections.singletonList(descriptor), 1); + + // Then + String expectedJsonText = ResourceLoader.readString("/tic/io/serialport/NullifiedStrings.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } + + @Test + public void encodeHandlesNullList() throws IOException, URISyntaxException, JSONException { + // Given + List descriptors = null; + + // When + String actualJsonText = SerialPortJsonEncoder.encode(descriptors); + + // Then + String expectedJsonText = ResourceLoader.readString("/tic/io/serialport/NullList.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } +} diff --git a/src/test/java/tic/io/usb/UsbPortDescriptorTest.java b/src/test/java/tic/io/usb/UsbPortDescriptorTest.java new file mode 100644 index 0000000..0a011b7 --- /dev/null +++ b/src/test/java/tic/io/usb/UsbPortDescriptorTest.java @@ -0,0 +1,1616 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.usb; + +import org.junit.Assert; +import org.junit.Test; +import tic.frame.TICMode; + +public class UsbPortDescriptorTest { + + @Test + public void test_instance_with_all_parameters() { + // Given + + // When + UsbPortDescriptor descriptor = createStandardDescriptor(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_manufacturer() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer(null) + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals(null, descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_product() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product(null) + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals(null, descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_null_serialNumber() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber(null); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals(null, descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_bcdDevice() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(0, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_bcdUSB() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(0, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_bDescriptorType() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(0, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_bDeviceClass() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(0, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_bDeviceProtocol() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(0, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_bDeviceSubClass() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(0, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_bLength() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(0, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_bMaxPacketSize0() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(0, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_bNumConfigurations() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(0, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_idProduct() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(0, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_idVendor() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(0, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_iManufacturer() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(0, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_iProduct() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(0, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_iSerialNumber() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(0, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_manufacturer() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .product("test_product") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals(null, descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_product() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .serialNumber("SN123"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals(null, descriptor.product()); + Assert.assertEquals("SN123", descriptor.serialNumber()); + } + + @Test + public void test_instance_with_missing_serialNumber() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product"); + // When + UsbPortDescriptor descriptor = builder.build(); + + // Then + Assert.assertEquals(1, descriptor.bcdDevice()); + Assert.assertEquals(2, descriptor.bcdUSB()); + Assert.assertEquals(3, descriptor.bDescriptorType()); + Assert.assertEquals(4, descriptor.bDeviceClass()); + Assert.assertEquals(5, descriptor.bDeviceProtocol()); + Assert.assertEquals(6, descriptor.bDeviceSubClass()); + Assert.assertEquals(7, descriptor.bLength()); + Assert.assertEquals(8, descriptor.bMaxPacketSize0()); + Assert.assertEquals(9, descriptor.bNumConfigurations()); + Assert.assertEquals(10, descriptor.idProduct()); + Assert.assertEquals(11, descriptor.idVendor()); + Assert.assertEquals(12, descriptor.iManufacturer()); + Assert.assertEquals(13, descriptor.iProduct()); + Assert.assertEquals(14, descriptor.iSerialNumber()); + Assert.assertEquals("test_manufacturer", descriptor.manufacturer()); + Assert.assertEquals("test_product", descriptor.product()); + Assert.assertEquals(null, descriptor.serialNumber()); + } + + @Test + public void test_instance_with_empty_manufacturer() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("") + .product("test_product") + .serialNumber("SN123"); + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Value 'manufacturer' cannot be empty", exception.getMessage()); + } + + @Test + public void test_instance_with_empty_product() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("") + .serialNumber("SN123"); + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Value 'product' cannot be empty", exception.getMessage()); + } + + @Test + public void test_instance_with_empty_serialNumber() { + // Given + UsbPortDescriptor.Builder builder = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber(""); + // When + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> builder.build()); + + // Then + Assert.assertEquals("Value 'serialNumber' cannot be empty", exception.getMessage()); + } + + @Test + public void test_equals_with_same_instance() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + // When + boolean match = descriptor.equals(descriptor); + + // Then + Assert.assertEquals(true, match); + } + + @Test + public void test_equals_with_identical_object() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = createStandardDescriptor(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(true, match); + } + + @Test + public void test_equals_with_different_object_type() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + // When + boolean match = descriptor.equals((Object) TICMode.HISTORIC); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_bcdDevice() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_bcdUSB() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_bDescriptorType() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_bDeviceClass() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_bDeviceProtocol() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_bDeviceSubClass() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_bLength() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_bMaxPacketSize0() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_bNumConfigurations() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_idProduct() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_idVendor() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_iManufacturer() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_iProduct() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_iSerialNumber() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_manufacturer() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer_different") + .product("test_product") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_product() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product_different") + .serialNumber("SN123") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_equals_with_different_serialNumber() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + + UsbPortDescriptor otherDescriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123_different") + .build(); + // When + boolean match = descriptor.equals(otherDescriptor); + + // Then + Assert.assertEquals(false, match); + } + + @Test + public void test_hashCode() { + // Given + UsbPortDescriptor descriptor = createStandardDescriptor(); + UsbPortDescriptor sameDescriptor = createStandardDescriptor(); + + // When + int hashCode = descriptor.hashCode(); + int sameHashCode = sameDescriptor.hashCode(); + + // Then + Assert.assertEquals(sameHashCode, hashCode); + } + + private UsbPortDescriptor createStandardDescriptor() { + return new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + } +} diff --git a/src/test/java/tic/io/usb/UsbPortFinderBaseTest.java b/src/test/java/tic/io/usb/UsbPortFinderBaseTest.java new file mode 100644 index 0000000..22a0d8e --- /dev/null +++ b/src/test/java/tic/io/usb/UsbPortFinderBaseTest.java @@ -0,0 +1,79 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.usb; + +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +public class UsbPortFinderBaseTest { + + @Test + public void test_getInstance() { + // Given + + // When + UsbPortFinder instance1 = UsbPortFinderBase.getInstance(); + UsbPortFinder instance2 = UsbPortFinderBase.getInstance(); + + // Then + Assert.assertNotNull(instance1); + Assert.assertNotNull(instance2); + Assert.assertSame(instance1, instance2); + } + + @Test + public void test_findAll() { + // Given + + // When + List descriptors = UsbPortFinderBase.getInstance().findAll(); + + // Then + Assert.assertNotNull(descriptors); + } + + @Test + public void test_findByProductId() { + // Given + short idProduct = 0; + // When + List descriptors = + UsbPortFinderBase.getInstance().findByProductId(idProduct); + + // Then + Assert.assertNotNull(descriptors); + Assert.assertEquals(0, descriptors.size()); + } + + @Test + public void test_findByVendorId() { + // Given + short idVendor = 0; + // When + List descriptors = UsbPortFinderBase.getInstance().findByVendorId(idVendor); + + // Then + Assert.assertNotNull(descriptors); + Assert.assertEquals(0, descriptors.size()); + } + + @Test + public void test_findByProductIdAndVendorId() { + // Given + short idProduct = 0; + short idVendor = 0; + // When + List descriptors = + UsbPortFinderBase.getInstance().findByProductIdAndVendorId(idProduct, idVendor); + + // Then + Assert.assertNotNull(descriptors); + Assert.assertEquals(0, descriptors.size()); + } +} diff --git a/src/test/java/tic/io/usb/UsbPortJsonEncoderTest.java b/src/test/java/tic/io/usb/UsbPortJsonEncoderTest.java new file mode 100644 index 0000000..e75578e --- /dev/null +++ b/src/test/java/tic/io/usb/UsbPortJsonEncoderTest.java @@ -0,0 +1,99 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.io.usb; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; +import org.json.JSONException; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tic.ResourceLoader; + +public class UsbPortJsonEncoderTest { + + @Test + public void encodeShouldSerializeAllDescriptorFields() + throws JSONException, IOException, URISyntaxException { + // Given + UsbPortDescriptor descriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer("test_manufacturer") + .product("test_product") + .serialNumber("SN123") + .build(); + + // When + String actualJsonText = UsbPortJsonEncoder.encode(Arrays.asList(descriptor), -1); + + // Then + String expectedJsonText = ResourceLoader.readString("/tic/io/usb/AllDescriptorFields.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } + + @Test + public void encodeShouldOmitNullStringFields() + throws JSONException, IOException, URISyntaxException { + // Given + UsbPortDescriptor descriptor = + new UsbPortDescriptor.Builder() + .bcdDevice((short) 1) + .bcdUSB((short) 2) + .bDescriptorType((byte) 3) + .bDeviceClass((byte) 4) + .bDeviceProtocol((byte) 5) + .bDeviceSubClass((byte) 6) + .bLength((byte) 7) + .bMaxPacketSize0((byte) 8) + .bNumConfigurations((byte) 9) + .idProduct((short) 10) + .idVendor((short) 11) + .iManufacturer((byte) 12) + .iProduct((byte) 13) + .iSerialNumber((byte) 14) + .manufacturer(null) + .product(null) + .serialNumber(null) + .build(); + + // When + String actualJsonText = UsbPortJsonEncoder.encode(Arrays.asList(descriptor), -1); + + // Then + String expectedJsonText = ResourceLoader.readString("/tic/io/usb/WithNullStringFields.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } + + @Test + public void encodeHandlesNullList() throws IOException, URISyntaxException, JSONException { + // Given + List list = null; + + // When + String actualJsonText = UsbPortJsonEncoder.encode(list); + + // Then + String expectedJsonText = ResourceLoader.readString("/tic/io/usb/NullList.json"); + JSONAssert.assertEquals(expectedJsonText, actualJsonText, false); + } +} diff --git a/src/test/java/enedis/lab/mock/FunctionCall.java b/src/test/java/tic/mock/FunctionCall.java similarity index 95% rename from src/test/java/enedis/lab/mock/FunctionCall.java rename to src/test/java/tic/mock/FunctionCall.java index d26b531..bc147d6 100644 --- a/src/test/java/enedis/lab/mock/FunctionCall.java +++ b/src/test/java/tic/mock/FunctionCall.java @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package enedis.lab.mock; +package tic.mock; @SuppressWarnings("javadoc") public class FunctionCall { diff --git a/src/test/java/tic/service/config/TIC2WebSocketConfigurationLoaderTest.java b/src/test/java/tic/service/config/TIC2WebSocketConfigurationLoaderTest.java new file mode 100644 index 0000000..0914765 --- /dev/null +++ b/src/test/java/tic/service/config/TIC2WebSocketConfigurationLoaderTest.java @@ -0,0 +1,193 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.service.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import org.json.JSONException; +import org.junit.Test; +import tic.ResourceLoader; +import tic.frame.TICMode; + +public class TIC2WebSocketConfigurationLoaderTest { + + @Test + public void load_minimalConfiguration_ok() throws Exception { + // Given + String path = ResourceLoader.getFilePath("/tic/service/config_minimal.json"); + + // When + TIC2WebSocketConfiguration cfg = TIC2WebSocketConfigurationLoader.load(path); + + // Then + assertEquals(1234, cfg.getServerPort()); + assertEquals(TICMode.AUTO, cfg.getTicMode()); + assertNull(cfg.getTicPortNames()); + } + + @Test + public void load_fullConfiguration_ok() throws Exception { + // Given + String path = ResourceLoader.getFilePath("/tic/service/config_full.json"); + + // When + TIC2WebSocketConfiguration cfg = TIC2WebSocketConfigurationLoader.load(path); + + // Then + assertEquals(1234, cfg.getServerPort()); + assertEquals(TICMode.HISTORIC, cfg.getTicMode()); + assertEquals(Arrays.asList("COM3", "COM4"), cfg.getTicPortNames()); + } + + @Test + public void load_ticPortNamesEmptyArray_resultsInNull() throws Exception { + // Given + String path = ResourceLoader.getFilePath("/tic/service/config_empty_portnames.json"); + + // When + TIC2WebSocketConfiguration cfg = TIC2WebSocketConfigurationLoader.load(path); + + // Then + assertEquals(1234, cfg.getServerPort()); + assertEquals(TICMode.AUTO, cfg.getTicMode()); + assertNull(cfg.getTicPortNames()); + } + + @Test + public void load_ticModeLowercase_ok() throws Exception { + // Given + String path = ResourceLoader.getFilePath("/tic/service/config_mode_lowercase.json"); + + // When + TIC2WebSocketConfiguration cfg = TIC2WebSocketConfigurationLoader.load(path); + + // Then + assertEquals(1234, cfg.getServerPort()); + assertEquals(TICMode.HISTORIC, cfg.getTicMode()); + } + + @Test + public void load_missingServerPort_throwsIllegalStateException() throws Exception { + // Given + String path = ResourceLoader.getFilePath("/tic/service/config_invalid_missing_port.json"); + Exception exception = null; + + // When + try { + TIC2WebSocketConfigurationLoader.load(path); + } catch (Exception ex) { + exception = ex; + } + + // Then + assertNotNull(exception); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause() instanceof IllegalArgumentException); + } + + @Test + public void load_serverPortOutOfRange_throwsIllegalStateException() throws Exception { + // Given + String path = ResourceLoader.getFilePath("/tic/service/config_invalid_port_range.json"); + Exception exception = null; + + // When + try { + TIC2WebSocketConfigurationLoader.load(path); + } catch (Exception ex) { + exception = ex; + } + + // Then + assertNotNull(exception); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause() instanceof IllegalArgumentException); + } + + @Test + public void load_invalidTicMode_throwsIllegalStateException() throws Exception { + // Given + String path = ResourceLoader.getFilePath("/tic/service/config_invalid_mode.json"); + Exception exception = null; + + // When + try { + TIC2WebSocketConfigurationLoader.load(path); + } catch (Exception ex) { + exception = ex; + } + + // Then + assertNotNull(exception); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause() instanceof IllegalArgumentException); + } + + @Test + public void load_emptyPortName_throwsIllegalStateException() throws Exception { + // Given + String path = ResourceLoader.getFilePath("/tic/service/config_invalid_portnames_empty.json"); + Exception exception = null; + + // When + try { + TIC2WebSocketConfigurationLoader.load(path); + } catch (Exception ex) { + exception = ex; + } + + // Then + assertNotNull(exception); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause() instanceof IllegalArgumentException); + } + + @Test + public void load_duplicatePortNameAfterTrim_throwsIllegalStateException() throws Exception { + // Given + String path = + ResourceLoader.getFilePath("/tic/service/config_invalid_portnames_duplicates_trim.json"); + Exception exception = null; + + // When + try { + TIC2WebSocketConfigurationLoader.load(path); + } catch (Exception ex) { + exception = ex; + } + + // Then + assertNotNull(exception); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause() instanceof IllegalArgumentException); + } + + @Test + public void load_nullPortNameElement_throwsIllegalStateException() throws Exception { + // Given + String path = + ResourceLoader.getFilePath("/tic/service/config_invalid_portnames_null_element.json"); + Exception exception = null; + + // When + try { + TIC2WebSocketConfigurationLoader.load(path); + } catch (Exception ex) { + exception = ex; + } + + // Then + assertNotNull(exception); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause() instanceof JSONException); + } +} diff --git a/src/test/java/tic/stream/configuration/TICStreamConfigurationLoaderTest.java b/src/test/java/tic/stream/configuration/TICStreamConfigurationLoaderTest.java new file mode 100644 index 0000000..8d5cd7f --- /dev/null +++ b/src/test/java/tic/stream/configuration/TICStreamConfigurationLoaderTest.java @@ -0,0 +1,187 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream.configuration; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; +import org.junit.Assert; +import org.junit.Test; +import tic.ResourceLoader; +import tic.frame.TICMode; +import tic.stream.identifier.TICStreamIdentifierType; + +public class TICStreamConfigurationLoaderTest { + + @Test + public void test_load_withValidPortNameConfig_returnsConfiguration() + throws IOException, URISyntaxException { + // Given + String configPath = + ResourceLoader.getFilePath("/tic/stream/configuration/withValidPortNameConfig.json"); + + // When + TICStreamConfiguration configuration = TICStreamConfigurationLoader.load(configPath); + + // Then + Assert.assertEquals(TICMode.HISTORIC, configuration.getTicMode()); + Assert.assertEquals(25, configuration.getTimeout()); + Assert.assertEquals(TICStreamIdentifierType.PORT_NAME, configuration.getIdentifier().getType()); + Assert.assertEquals("COM7", configuration.getIdentifier().getPortName()); + } + + @Test + public void test_load_withPortIdIdentifier_usesDefaults() throws IOException, URISyntaxException { + // Given + String configPath = + ResourceLoader.getFilePath("/tic/stream/configuration/withPortIdIdentifier.json"); + + // When + TICStreamConfiguration configuration = TICStreamConfigurationLoader.load(configPath); + + // Then + Assert.assertEquals(TICMode.AUTO, configuration.getTicMode()); + Assert.assertEquals(10, configuration.getTimeout()); + Assert.assertEquals("12345", configuration.getIdentifier().getPortId()); + } + + @Test + public void test_load_withMissingFile_throwsIllegalStateException() { + // Given + String missingPath = "missing-config-" + System.nanoTime() + ".json"; + + // When + IllegalStateException exception = + Assert.assertThrows( + IllegalStateException.class, () -> TICStreamConfigurationLoader.load(missingPath)); + + // Then + Assert.assertTrue(exception.getMessage().contains("Unable to load")); + Assert.assertTrue(exception.getCause() instanceof IOException); + } + + @Test + public void test_load_withInvalidTimeout_throwsIllegalArgumentException() + throws IOException, URISyntaxException { + // Given + String configPath = + ResourceLoader.getFilePath("/tic/stream/configuration/withInvalidTimeout.json"); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICStreamConfigurationLoader.load(configPath)); + + // Then + Assert.assertTrue(exception.getMessage().contains("Timeout")); + } + + @Test + public void test_load_withMissingIdentifier_throwsIllegalArgumentException() + throws IOException, URISyntaxException { + // Given + String configPath = + ResourceLoader.getFilePath("/tic/stream/configuration/withMissingIdentifier.json"); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> TICStreamConfigurationLoader.load(configPath)); + + // Then + Assert.assertTrue(exception.getMessage().contains("identifier")); + } + + @Test + public void test_load_withInvalidIdentifierType_throwsIllegalArgumentException() + throws IOException, URISyntaxException { + // Given + String configPath = + ResourceLoader.getFilePath("/tic/stream/configuration/withInvalidIdentifierType.json"); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICStreamConfigurationLoader.load(configPath.toString())); + + // Then + Assert.assertTrue(exception.getMessage().contains("Invalid TIC stream identifier type")); + } + + @Test + public void test_load_withInvalidTicMode_throwsIllegalArgumentException() + throws IOException, URISyntaxException { + // Given + String configPath = + ResourceLoader.getFilePath("/tic/stream/configuration/withInvalidTicMode.json"); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> TICStreamConfigurationLoader.load(configPath.toString())); + + // Then + Assert.assertTrue(exception.getMessage().contains("No enum constant")); + } + + @Test + public void test_load_withResourceFile_returnsConfiguration() throws Exception { + // Given + String resourceLocation = "tic/stream/configuration/loader-valid.json"; + Path resourcePath = + Paths.get( + Objects.requireNonNull( + TICStreamConfigurationLoaderTest.class + .getClassLoader() + .getResource(resourceLocation)) + .toURI()); + + // When + TICStreamConfiguration configuration = + TICStreamConfigurationLoader.load(resourcePath.toString()); + + // Then + Assert.assertEquals(TICMode.STANDARD, configuration.getTicMode()); + Assert.assertEquals(42, configuration.getTimeout()); + Assert.assertEquals("COM9", configuration.getIdentifier().getPortName()); + } + + @Test + public void test_load_withEmptyFilePath_throwsIllegalStateException() { + // Given + String emptyPath = ""; + + // When + IllegalStateException exception = + Assert.assertThrows( + IllegalStateException.class, () -> TICStreamConfigurationLoader.load(emptyPath)); + + // Then + Assert.assertTrue(exception.getMessage().contains("Unable to load")); + Assert.assertTrue(exception.getCause() instanceof IOException); + } + + @Test + public void test_load_withNullFilePath_throwsIllegalStateException() { + // Given + String nullPath = null; + + // When + IllegalStateException exception = + Assert.assertThrows( + IllegalStateException.class, () -> TICStreamConfigurationLoader.load(nullPath)); + + // Then + Assert.assertTrue(exception.getMessage().contains("Unable to load")); + Assert.assertTrue(exception.getCause() instanceof NullPointerException); + } +} diff --git a/src/test/java/tic/stream/configuration/TICStreamConfigurationTest.java b/src/test/java/tic/stream/configuration/TICStreamConfigurationTest.java new file mode 100644 index 0000000..2b1a395 --- /dev/null +++ b/src/test/java/tic/stream/configuration/TICStreamConfigurationTest.java @@ -0,0 +1,128 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.stream.configuration; + +import tic.frame.TICMode; +import org.junit.Assert; +import org.junit.Test; + +import tic.stream.identifier.SerialPortId; +import tic.stream.identifier.SerialPortName; +import tic.stream.identifier.TICStreamIdentifier; + +public class TICStreamConfigurationTest { + + @Test + public void test_constructor_withValidParametersByPortName_setsFields() { + // Given + TICStreamIdentifier identifier = newIdentifierByPortName(); + + // When + TICStreamConfiguration configuration = + new TICStreamConfiguration(TICMode.STANDARD, identifier, 30); + + // Then + Assert.assertEquals(TICMode.STANDARD, configuration.getTicMode()); + Assert.assertSame(identifier, configuration.getIdentifier()); + Assert.assertEquals(30, configuration.getTimeout()); + } + + @Test + public void test_constructor_withValidParametersByPortId_setsFields() { + // Given + TICStreamIdentifier identifier = newIdentifierByPortId(); + + // When + TICStreamConfiguration configuration = + new TICStreamConfiguration(TICMode.HISTORIC, identifier, 15); + + // Then + Assert.assertEquals(TICMode.HISTORIC, configuration.getTicMode()); + Assert.assertSame(identifier, configuration.getIdentifier()); + Assert.assertEquals(15, configuration.getTimeout()); + } + + @Test + public void test_constructor_withNullTicMode_throwsException() { + // Given + TICStreamIdentifier identifier = newIdentifierByPortName(); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> new TICStreamConfiguration(null, identifier, 10)); + + // Then + Assert.assertTrue(exception.getMessage().contains("cannot be null")); + } + + @Test + public void test_constructor_withNullIdentifier_throwsException() { + // Given + TICMode mode = TICMode.HISTORIC; + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, () -> new TICStreamConfiguration(mode, null, 10)); + + // Then + Assert.assertTrue(exception.getMessage().contains("identifier cannot be null")); + } + + @Test + public void test_constructor_withZeroTimeout_throwsException() { + // Given + TICStreamIdentifier identifier = newIdentifierByPortName(); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> new TICStreamConfiguration(TICMode.AUTO, identifier, 0)); + + // Then + Assert.assertTrue(exception.getMessage().contains("positive integer")); + } + + @Test + public void test_constructor_withNegativeTimeout_throwsException() { + // Given + TICStreamIdentifier identifier = newIdentifierByPortName(); + + // When + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> new TICStreamConfiguration(TICMode.AUTO, identifier, -5)); + + // Then + Assert.assertTrue(exception.getMessage().contains("positive integer")); + } + + @Test + public void test_constructor_withMaxTimeout_allowed() { + // Given + TICStreamIdentifier identifier = newIdentifierByPortName(); + + // When + TICStreamConfiguration configuration = + new TICStreamConfiguration(TICMode.AUTO, identifier, Integer.MAX_VALUE); + + // Then + Assert.assertEquals(Integer.MAX_VALUE, configuration.getTimeout()); + } + + private static TICStreamIdentifier newIdentifierByPortName() { + return new TICStreamIdentifier(new SerialPortName("COM1")); + } + + private static TICStreamIdentifier newIdentifierByPortId() { + return new TICStreamIdentifier(new SerialPortId("12345")); + } +} diff --git a/src/test/java/tic/util/message/codec/MessageJsonCodecTest.java b/src/test/java/tic/util/message/codec/MessageJsonCodecTest.java new file mode 100644 index 0000000..31f49f2 --- /dev/null +++ b/src/test/java/tic/util/message/codec/MessageJsonCodecTest.java @@ -0,0 +1,467 @@ +// Copyright (C) 2025 Enedis Smarties team +// +// SPDX-FileContributor: Jehan BOUSCH +// SPDX-FileContributor: Mathieu SABARTHES +// +// SPDX-License-Identifier: Apache-2.0 + +package tic.util.message.codec; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Test; +import tic.ResourceLoader; +import tic.core.TICIdentifier; +import tic.service.message.RequestGetAvailableTICs; +import tic.service.message.RequestGetModemsInfo; +import tic.service.message.RequestReadTIC; +import tic.service.message.RequestSubscribeTIC; +import tic.service.message.RequestUnsubscribeTIC; +import tic.util.message.Message; +import tic.util.message.MessageType; +import tic.util.message.exception.MessageException; +import tic.util.message.exception.UnsupportedMessageException; + +public class MessageJsonCodecTest { + + private static JSONObject readJsonObject(String resourcePath) + throws IOException, URISyntaxException { + String jsonText = ResourceLoader.readString(resourcePath); + return new JSONObject(jsonText); + } + + @Test + public void decodeFromJsonObject_withGetAvailableTICs() throws Exception { + // Given + JSONObject jsonObject = readJsonObject("/tic/util/message/codec/RequestGetAvailableTICs.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestGetAvailableTICs); + Assert.assertEquals(MessageType.REQUEST, message.getType()); + Assert.assertEquals(RequestGetAvailableTICs.NAME, message.getName()); + } + + @Test + public void decodeFromJsonObject_withGetModemsInfo() throws Exception { + // Given + JSONObject jsonObject = readJsonObject("/tic/util/message/codec/RequestGetModemsInfo.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestGetModemsInfo); + Assert.assertEquals(MessageType.REQUEST, message.getType()); + Assert.assertEquals(RequestGetModemsInfo.NAME, message.getName()); + } + + @Test + public void decodeFromJsonObject_withReadTIC_allFields() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestReadTIC_WithAllFields.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestReadTIC); + RequestReadTIC request = (RequestReadTIC) message; + Assert.assertEquals(RequestReadTIC.NAME, request.getName()); + + TICIdentifier identifier = request.getData(); + Assert.assertNotNull(identifier); + Assert.assertEquals("/dev/ttyUSB0", identifier.getPortName()); + Assert.assertEquals("1-1", identifier.getPortId()); + Assert.assertEquals("010203040506", identifier.getSerialNumber()); + } + + @Test + public void decodeFromJsonObject_withReadTIC_withPortId() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestReadTIC_WithPortId.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestReadTIC); + RequestReadTIC request = (RequestReadTIC) message; + Assert.assertEquals(RequestReadTIC.NAME, request.getName()); + + TICIdentifier identifier = request.getData(); + Assert.assertNotNull(identifier); + Assert.assertNull(identifier.getPortName()); + Assert.assertEquals("1-1", identifier.getPortId()); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test + public void decodeFromJsonObject_withReadTIC_withPortName() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestReadTIC_WithPortName.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestReadTIC); + RequestReadTIC request = (RequestReadTIC) message; + Assert.assertEquals(RequestReadTIC.NAME, request.getName()); + + TICIdentifier identifier = request.getData(); + Assert.assertNotNull(identifier); + Assert.assertEquals("/dev/ttyUSB0", identifier.getPortName()); + Assert.assertNull(identifier.getPortId()); + Assert.assertNull(identifier.getSerialNumber()); + } + + @Test + public void decodeFromJsonObject_withReadTIC_withSerialNumber() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestReadTIC_WithSerialNumber.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestReadTIC); + RequestReadTIC request = (RequestReadTIC) message; + Assert.assertEquals(RequestReadTIC.NAME, request.getName()); + + TICIdentifier identifier = request.getData(); + Assert.assertNotNull(identifier); + Assert.assertNull(identifier.getPortName()); + Assert.assertNull(identifier.getPortId()); + Assert.assertEquals("010203040506", identifier.getSerialNumber()); + } + + @Test + public void decodeFromJsonObject_withSubscribeTIC_noData() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestSubscribeTIC_WithNoData.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestSubscribeTIC); + RequestSubscribeTIC request = (RequestSubscribeTIC) message; + Assert.assertNull(request.getData()); + } + + @Test + public void decodeFromJsonObject_withSubscribeTIC_emptyArray() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestSubscribeTIC_WithEmptyArray.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestSubscribeTIC); + RequestSubscribeTIC request = (RequestSubscribeTIC) message; + Assert.assertNotNull(request.getData()); + Assert.assertTrue(request.getData().isEmpty()); + } + + @Test + public void decodeFromJsonObject_withSubscribeTIC_oneIdentifierList() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestSubscribeTIC_WithOneIdentifierList.json"); + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestSubscribeTIC); + RequestSubscribeTIC request = (RequestSubscribeTIC) message; + + List identifiers = request.getData(); + Assert.assertNotNull(identifiers); + Assert.assertEquals(1, identifiers.size()); + Assert.assertEquals("COM7", identifiers.get(0).getPortName()); + Assert.assertNull(identifiers.get(0).getPortId()); + Assert.assertNull(identifiers.get(0).getSerialNumber()); + } + + @Test + public void decodeFromJsonObject_withSubscribeTIC_oneIdentifier() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestSubscribeTIC_WithOneIdentifier.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestSubscribeTIC); + RequestSubscribeTIC request = (RequestSubscribeTIC) message; + + List identifiers = request.getData(); + Assert.assertNotNull(identifiers); + Assert.assertEquals(1, identifiers.size()); + Assert.assertNull(identifiers.get(0).getPortName()); + Assert.assertNull(identifiers.get(0).getPortId()); + Assert.assertEquals("010203040506", identifiers.get(0).getSerialNumber()); + } + + @Test + public void decodeFromJsonObject_withUnsubscribeTIC_noData() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestUnsubscribeTIC_WithNoData.json"); + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestUnsubscribeTIC); + RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; + Assert.assertNull(request.getData()); + } + + @Test + public void decodeFromJsonObject_withUnsubscribeTIC_emptyArray() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestUnsubscribeTIC_WithEmptyArray.json"); + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestUnsubscribeTIC); + RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; + Assert.assertNotNull(request.getData()); + Assert.assertTrue(request.getData().isEmpty()); + } + + @Test + public void decodeFromJsonObject_withUnsubscribeTIC_oneIdentifierList() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestUnsubscribeTIC_WithOneIdentifierList.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestUnsubscribeTIC); + RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; + + List identifiers = request.getData(); + Assert.assertNotNull(identifiers); + Assert.assertEquals(1, identifiers.size()); + Assert.assertEquals("COM7", identifiers.get(0).getPortName()); + Assert.assertNull(identifiers.get(0).getPortId()); + Assert.assertNull(identifiers.get(0).getSerialNumber()); + } + + @Test + public void decodeFromJsonObject_withUnsubscribeTIC_oneIdentifier() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/RequestUnsubscribeTIC_WithOneIdentifier.json"); + + // When + Message message = MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + + // Then + Assert.assertTrue(message instanceof RequestUnsubscribeTIC); + RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; + + List identifiers = request.getData(); + Assert.assertNotNull(identifiers); + Assert.assertEquals(1, identifiers.size()); + Assert.assertEquals("COM7", identifiers.get(0).getPortName()); + Assert.assertNull(identifiers.get(0).getPortId()); + Assert.assertNull(identifiers.get(0).getSerialNumber()); + } + + @Test + public void decodeFromJsonObject_withNullJson() { + // Given + JSONObject jsonObject = null; + Exception exception = null; + + // When + try { + MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + Assert.fail("Expected MessageException to be thrown"); + } catch (Exception ex) { + exception = ex; + } + + // Then + Assert.assertNotNull(exception); + Assert.assertTrue(exception instanceof MessageException); + } + + @Test + public void decodeFromJsonObject_withMissingName() throws Exception { + // Given + JSONObject jsonObject = readJsonObject("/tic/util/message/codec/Invalid_MissingName.json"); + Exception exception = null; + + // When + try { + MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + Assert.fail("Expected MessageException to be thrown"); + } catch (Exception ex) { + exception = ex; + } + + // Then + Assert.assertNotNull(exception); + Assert.assertTrue(exception instanceof MessageException); + } + + @Test + public void decodeFromJsonObject_withMissingType() throws Exception { + // Given + JSONObject jsonObject = readJsonObject("/tic/util/message/codec/Invalid_MissingType.json"); + Exception exception = null; + + // When + try { + MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + Assert.fail("Expected MessageException to be thrown"); + } catch (Exception ex) { + exception = ex; + } + + // Then + Assert.assertNotNull(exception); + Assert.assertTrue(exception instanceof MessageException); + } + + @Test + public void decodeFromJsonObject_withNonRequestType() throws Exception { + // Given + JSONObject jsonObject = readJsonObject("/tic/util/message/codec/Invalid_TypeEvent.json"); + Exception exception = null; + + // When + try { + MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + Assert.fail("Expected MessageException to be thrown"); + } catch (Exception ex) { + exception = ex; + } + + // Then + Assert.assertNotNull(exception); + Assert.assertTrue(exception instanceof MessageException); + } + + @Test + public void decodeFromJsonObject_withUnsupportedName() throws Exception { + // Given + JSONObject jsonObject = readJsonObject("/tic/util/message/codec/Invalid_UnsupportedName.json"); + Exception exception = null; + + // When + try { + MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + Assert.fail("Expected UnsupportedMessageException to be thrown"); + } catch (Exception ex) { + exception = ex; + } + + // Then + Assert.assertNotNull(exception); + Assert.assertTrue(exception instanceof UnsupportedMessageException); + } + + @Test + public void decodeFromJsonObject_withReadTicMissingData() throws Exception { + // Given + JSONObject jsonObject = + readJsonObject("/tic/util/message/codec/Invalid_ReadTIC_MissingData.json"); + Exception exception = null; + + // When + try { + MessageJsonCodec.getInstance().decodeFromJsonObject(jsonObject); + Assert.fail("Expected MessageException to be thrown"); + } catch (Exception ex) { + exception = ex; + } + // Then + Assert.assertNotNull(exception); + Assert.assertTrue(exception instanceof MessageException); + } + + @Test + public void encodeToJsonObject_withGetAvailableTICs() { + // Given + Message message = new RequestGetAvailableTICs(); + + // When + JSONObject json = MessageJsonCodec.getInstance().encodeToJsonObject(message); + + // Then + Assert.assertEquals(RequestGetAvailableTICs.NAME, json.getString("name")); + Assert.assertEquals(MessageType.REQUEST.toString(), json.getString("type")); + Assert.assertFalse(json.has("data")); + } + + @Test + public void encodeToJsonObject_withGetModemsInfo() { + // Given + Message message = new RequestGetModemsInfo(); + + // When + JSONObject json = MessageJsonCodec.getInstance().encodeToJsonObject(message); + + // Then + Assert.assertEquals(RequestGetModemsInfo.NAME, json.getString("name")); + Assert.assertEquals(MessageType.REQUEST.toString(), json.getString("type")); + Assert.assertFalse(json.has("data")); + } + + @Test + public void encodeToJsonObject_withReadTIC() { + // Given + TICIdentifier identifier = new TICIdentifier.Builder().portName("COM7").build(); + Message message = new RequestReadTIC(identifier); + + // When + JSONObject json = MessageJsonCodec.getInstance().encodeToJsonObject(message); + + // Then + Assert.assertEquals(RequestReadTIC.NAME, json.getString("name")); + Assert.assertEquals(MessageType.REQUEST.toString(), json.getString("type")); + Assert.assertTrue(json.has("data")); + + JSONObject data = json.getJSONObject("data"); + Assert.assertEquals("COM7", data.getString("portName")); + Assert.assertTrue(data.isNull("portId")); + Assert.assertTrue(data.isNull("serialNumber")); + } + + @Test + public void encodeToJsonObject_withSubscribeTICNullList() { + // Given + Message message = new RequestSubscribeTIC(null); + + // When + JSONObject json = MessageJsonCodec.getInstance().encodeToJsonObject(message); + + // Then + Assert.assertEquals(RequestSubscribeTIC.NAME, json.getString("name")); + Assert.assertEquals(MessageType.REQUEST.toString(), json.getString("type")); + Assert.assertTrue(json.has("data")); + Assert.assertEquals(0, json.getJSONArray("data").length()); + } +} diff --git a/src/test/resources/tic/frame/codec/ticFrameHistoric.txt b/src/test/resources/tic/frame/codec/ticFrameHistoric.txt new file mode 100644 index 0000000..10262c4 --- /dev/null +++ b/src/test/resources/tic/frame/codec/ticFrameHistoric.txt @@ -0,0 +1,10 @@ + +ADCO 812164417227 D +OPTARIF BASE 0 +ISOUSC 15 < +BASE 000000104 P +PTEC TH.. $ +IINST 000 W +IMAX 000 ? +PAPP 00000 ! +MOTDETAT 000000 B  \ No newline at end of file diff --git a/src/test/resources/tic/frame/codec/ticFrameHistoric_full_details.json b/src/test/resources/tic/frame/codec/ticFrameHistoric_full_details.json new file mode 100644 index 0000000..a365d7b --- /dev/null +++ b/src/test/resources/tic/frame/codec/ticFrameHistoric_full_details.json @@ -0,0 +1,50 @@ +{ + "mode": "HISTORIC", + "groupList": [ + { + "label": "ADCO", + "value": "812164417227", + "isValid": true + }, + { + "label": "OPTARIF", + "value": "BASE", + "isValid": true + }, + { + "label": "ISOUSC", + "value": "15", + "isValid": true + }, + { + "label": "BASE", + "value": "000000104", + "isValid": true + }, + { + "label": "PTEC", + "value": "TH..", + "isValid": true + }, + { + "label": "IINST", + "value": "000", + "isValid": true + }, + { + "label": "IMAX", + "value": "000", + "isValid": true + }, + { + "label": "PAPP", + "value": "00000", + "isValid": true + }, + { + "label": "MOTDETAT", + "value": "000000", + "isValid": false + } + ] +} \ No newline at end of file diff --git a/src/test/resources/tic/frame/codec/ticFrameStandard.txt b/src/test/resources/tic/frame/codec/ticFrameStandard.txt new file mode 100644 index 0000000..f921d27 --- /dev/null +++ b/src/test/resources/tic/frame/codec/ticFrameStandard.txt @@ -0,0 +1,39 @@ + +ADSC 031664001115 ) +VTIC 02 J +DATE E170915101601 > +NGTF H PLEINE/CREUSE \ +LTARF INDEX INACTIF 2 3 +EAST 000000000 O +EASF01 000000000 " +EASF02 000000000 # +EASF03 000000000 $ +EASF04 000000000 % +EASF05 000000000 & +EASF06 000000000 ' +EASF07 000000000 ( +EASF08 000000000 ) +EASF09 000000000 * +EASF10 000000000 " +EASD01 000000000 +EASD02 000000000 ! +EASD03 000000000 " +EASD04 000000000 # +IRMS1 000 . +URMS1 230 ? +PREF 03 B +PCOUP 02 [ +SINSTS 00000 F +SMAXSN E170915100926 00000 3 +SMAXSN-1 E170913000000 00000 = +CCASN E170913113000 00000 2 +CCASN-1 E170913110000 00000 M +UMOY1 E170915101500 230 . +STGE 000A4411 @ +MSG1 PAS DE MESSAGE < +PRM 00000000000000 A +RELAIS 000 B +NTARF 02 O +NJOURF 00 & +NJOURF+1 00 B +PJOURF+1 00008002 0100C001 06008002 1230C001 15308002 NONUTILE NONUTILE NONUTILE NONUTILE NONUTILE NONUTILE T  \ No newline at end of file diff --git a/src/test/resources/tic/frame/codec/ticFrameStandard_PCOUP_Invalid.txt b/src/test/resources/tic/frame/codec/ticFrameStandard_PCOUP_Invalid.txt new file mode 100644 index 0000000..11d2b6d --- /dev/null +++ b/src/test/resources/tic/frame/codec/ticFrameStandard_PCOUP_Invalid.txt @@ -0,0 +1,39 @@ + +ADSC 031664001115 ) +VTIC 02 J +DATE E170915101601 > +NGTF H PLEINE/CREUSE \ +LTARF INDEX INACTIF 2 3 +EAST 000000000 O +EASF01 000000000 " +EASF02 000000000 # +EASF03 000000000 $ +EASF04 000000000 % +EASF05 000000000 & +EASF06 000000000 ' +EASF07 000000000 ( +EASF08 000000000 ) +EASF09 000000000 * +EASF10 000000000 " +EASD01 000000000 +EASD02 000000000 ! +EASD03 000000000 " +EASD04 000000000 # +IRMS1 000 . +URMS1 230 ? +PREF 03 B +PCOUP 02 \ +SINSTS 00000 F +SMAXSN E170915100926 00000 3 +SMAXSN-1 E170913000000 00000 = +CCASN E170913113000 00000 2 +CCASN-1 E170913110000 00000 M +UMOY1 E170915101500 230 . +STGE 000A4411 @ +MSG1 PAS DE MESSAGE < +PRM 00000000000000 A +RELAIS 000 B +NTARF 02 O +NJOURF 00 & +NJOURF+1 00 B +PJOURF+1 00008002 0100C001 06008002 1230C001 15308002 NONUTILE NONUTILE NONUTILE NONUTILE NONUTILE NONUTILE T  \ No newline at end of file diff --git a/src/test/resources/tic/frame/codec/ticFrameStandard_summarized.json b/src/test/resources/tic/frame/codec/ticFrameStandard_summarized.json new file mode 100644 index 0000000..af500c5 --- /dev/null +++ b/src/test/resources/tic/frame/codec/ticFrameStandard_summarized.json @@ -0,0 +1,40 @@ +{ + "ADSC": "031664001115", + "VTIC": "02", + "DATE": "E170915101601\t", + "NGTF": "H PLEINE/CREUSE ", + "LTARF": "INDEX INACTIF 2 ", + "EAST": "000000000", + "EASF01": "000000000", + "EASF02": "000000000", + "EASF03": "000000000", + "EASF04": "000000000", + "EASF05": "000000000", + "EASF06": "000000000", + "EASF07": "000000000", + "EASF08": "000000000", + "EASF09": "000000000", + "EASF10": "000000000", + "EASD01": "000000000", + "EASD02": "000000000", + "EASD03": "000000000", + "EASD04": "000000000", + "IRMS1": "000", + "URMS1": "230", + "PREF": "03", + "!PCOUP": "02", + "SINSTS": "00000", + "SMAXSN": "E170915100926\t00000", + "SMAXSN-1": "E170913000000\t00000", + "CCASN": "E170913113000\t00000", + "CCASN-1": "E170913110000\t00000", + "UMOY1": "E170915101500\t230", + "STGE": "000A4411", + "MSG1": "PAS DE MESSAGE ", + "PRM": "00000000000000", + "RELAIS": "000", + "NTARF": "02", + "NJOURF": "00", + "NJOURF+1": "00", + "PJOURF+1": "00008002 0100C001 06008002 1230C001 15308002 NONUTILE NONUTILE NONUTILE NONUTILE NONUTILE NONUTILE" +} \ No newline at end of file diff --git a/src/test/resources/tic/io/modem/AllFields.json b/src/test/resources/tic/io/modem/AllFields.json new file mode 100644 index 0000000..6eeb440 --- /dev/null +++ b/src/test/resources/tic/io/modem/AllFields.json @@ -0,0 +1,11 @@ +{ + "modemType": "MICHAUD", + "serialNumber": "SN123", + "productId": 24577, + "description": "USB Serial Port", + "vendorId": 1027, + "portName": "COM7", + "portId": "ABCD1", + "productName": "test_product", + "manufacturer": "test_manufacturer" +} diff --git a/src/test/resources/tic/io/modem/Descriptor.json b/src/test/resources/tic/io/modem/Descriptor.json new file mode 100644 index 0000000..de06388 --- /dev/null +++ b/src/test/resources/tic/io/modem/Descriptor.json @@ -0,0 +1,11 @@ +{ + "modemType": "MICHAUD", + "serialNumber": "SN123", + "productId": 24577, + "description": "USB Serial Port", + "vendorId": 1027, + "portName": "COM7", + "portId": "ABCD1", + "productName": "test_product", + "manufacturer": "test_manufacturer" +} \ No newline at end of file diff --git a/src/test/resources/tic/io/modem/NullList.json b/src/test/resources/tic/io/modem/NullList.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/test/resources/tic/io/modem/NullList.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/test/resources/tic/io/modem/NullModemType.json b/src/test/resources/tic/io/modem/NullModemType.json new file mode 100644 index 0000000..122b94b --- /dev/null +++ b/src/test/resources/tic/io/modem/NullModemType.json @@ -0,0 +1,6 @@ +{ + "modemType": null, + "productId": 1234, + "vendorId": 5678, + "portName": "COM8" +} \ No newline at end of file diff --git a/src/test/resources/tic/io/serialport/AllFields.json b/src/test/resources/tic/io/serialport/AllFields.json new file mode 100644 index 0000000..a5ec424 --- /dev/null +++ b/src/test/resources/tic/io/serialport/AllFields.json @@ -0,0 +1,12 @@ +[ + { + "serialNumber": "SN123", + "productId": 1, + "description": "USB Serial Port", + "vendorId": 2, + "portName": "COM7", + "portId": "ABCD1", + "productName": "test_product", + "manufacturer": "test_manufacturer" + } +] \ No newline at end of file diff --git a/src/test/resources/tic/io/serialport/NullList.json b/src/test/resources/tic/io/serialport/NullList.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/test/resources/tic/io/serialport/NullList.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/test/resources/tic/io/serialport/NullifiedStrings.json b/src/test/resources/tic/io/serialport/NullifiedStrings.json new file mode 100644 index 0000000..0c565d4 --- /dev/null +++ b/src/test/resources/tic/io/serialport/NullifiedStrings.json @@ -0,0 +1,7 @@ +[ + { + "productId": 1, + "vendorId": 2, + "portId": "ABCD1" + } +] \ No newline at end of file diff --git a/src/test/resources/tic/io/usb/AllDescriptorFields.json b/src/test/resources/tic/io/usb/AllDescriptorFields.json new file mode 100644 index 0000000..c4c2c92 --- /dev/null +++ b/src/test/resources/tic/io/usb/AllDescriptorFields.json @@ -0,0 +1,21 @@ +[ + { + "bcdDevice": 1, + "bcdUSB": 2, + "bDescriptorType": 3, + "bDeviceClass": 4, + "bDeviceProtocol": 5, + "bDeviceSubClass": 6, + "bLength": 7, + "bMaxPacketSize0": 8, + "bNumConfigurations": 9, + "idProduct": 10, + "idVendor": 11, + "iManufacturer": 12, + "iProduct": 13, + "iSerialNumber": 14, + "manufacturer": "test_manufacturer", + "product": "test_product", + "serialNumber": "SN123" + } +] \ No newline at end of file diff --git a/src/test/resources/tic/io/usb/NullList.json b/src/test/resources/tic/io/usb/NullList.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/test/resources/tic/io/usb/NullList.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/test/resources/tic/io/usb/WithNullStringFields.json b/src/test/resources/tic/io/usb/WithNullStringFields.json new file mode 100644 index 0000000..5057cfb --- /dev/null +++ b/src/test/resources/tic/io/usb/WithNullStringFields.json @@ -0,0 +1,18 @@ +[ + { + "bcdDevice": 1, + "bcdUSB": 2, + "bDescriptorType": 3, + "bDeviceClass": 4, + "bDeviceProtocol": 5, + "bDeviceSubClass": 6, + "bLength": 7, + "bMaxPacketSize0": 8, + "bNumConfigurations": 9, + "idProduct": 10, + "idVendor": 11, + "iManufacturer": 12, + "iProduct": 13, + "iSerialNumber": 14 + } +] \ No newline at end of file diff --git a/src/test/resources/tic/service/config_empty_portnames.json b/src/test/resources/tic/service/config_empty_portnames.json new file mode 100644 index 0000000..55bfc78 --- /dev/null +++ b/src/test/resources/tic/service/config_empty_portnames.json @@ -0,0 +1,4 @@ +{ + "serverPort": 1234, + "ticPortNames": [] +} diff --git a/src/test/resources/tic/service/config_full.json b/src/test/resources/tic/service/config_full.json new file mode 100644 index 0000000..99ccb59 --- /dev/null +++ b/src/test/resources/tic/service/config_full.json @@ -0,0 +1,5 @@ +{ + "serverPort": 1234, + "ticMode": "HISTORIC", + "ticPortNames": ["COM3", "COM4"] +} diff --git a/src/test/resources/tic/service/config_invalid_missing_port.json b/src/test/resources/tic/service/config_invalid_missing_port.json new file mode 100644 index 0000000..594f1a3 --- /dev/null +++ b/src/test/resources/tic/service/config_invalid_missing_port.json @@ -0,0 +1,3 @@ +{ + "ticMode": "AUTO" +} diff --git a/src/test/resources/tic/service/config_invalid_mode.json b/src/test/resources/tic/service/config_invalid_mode.json new file mode 100644 index 0000000..b535d8e --- /dev/null +++ b/src/test/resources/tic/service/config_invalid_mode.json @@ -0,0 +1,4 @@ +{ + "serverPort": 1234, + "ticMode": "not-a-mode" +} diff --git a/src/test/resources/tic/service/config_invalid_port_range.json b/src/test/resources/tic/service/config_invalid_port_range.json new file mode 100644 index 0000000..a18f250 --- /dev/null +++ b/src/test/resources/tic/service/config_invalid_port_range.json @@ -0,0 +1,3 @@ +{ + "serverPort": 0 +} diff --git a/src/test/resources/tic/service/config_invalid_portnames_duplicates_trim.json b/src/test/resources/tic/service/config_invalid_portnames_duplicates_trim.json new file mode 100644 index 0000000..fc5fb43 --- /dev/null +++ b/src/test/resources/tic/service/config_invalid_portnames_duplicates_trim.json @@ -0,0 +1,4 @@ +{ + "serverPort": 1234, + "ticPortNames": [" COM3 ", "COM3"] +} diff --git a/src/test/resources/tic/service/config_invalid_portnames_empty.json b/src/test/resources/tic/service/config_invalid_portnames_empty.json new file mode 100644 index 0000000..1f5c5f0 --- /dev/null +++ b/src/test/resources/tic/service/config_invalid_portnames_empty.json @@ -0,0 +1,4 @@ +{ + "serverPort": 1234, + "ticPortNames": [""] +} diff --git a/src/test/resources/tic/service/config_invalid_portnames_null_element.json b/src/test/resources/tic/service/config_invalid_portnames_null_element.json new file mode 100644 index 0000000..ab880d4 --- /dev/null +++ b/src/test/resources/tic/service/config_invalid_portnames_null_element.json @@ -0,0 +1,4 @@ +{ + "serverPort": 1234, + "ticPortNames": [null] +} diff --git a/src/test/resources/tic/service/config_minimal.json b/src/test/resources/tic/service/config_minimal.json new file mode 100644 index 0000000..12ef0db --- /dev/null +++ b/src/test/resources/tic/service/config_minimal.json @@ -0,0 +1,3 @@ +{ + "serverPort": 1234 +} diff --git a/src/test/resources/tic/service/config_mode_lowercase.json b/src/test/resources/tic/service/config_mode_lowercase.json new file mode 100644 index 0000000..10d3481 --- /dev/null +++ b/src/test/resources/tic/service/config_mode_lowercase.json @@ -0,0 +1,4 @@ +{ + "serverPort": 1234, + "ticMode": "historic" +} diff --git a/src/test/resources/tic/stream/configuration/loader-valid.json b/src/test/resources/tic/stream/configuration/loader-valid.json new file mode 100644 index 0000000..a261841 --- /dev/null +++ b/src/test/resources/tic/stream/configuration/loader-valid.json @@ -0,0 +1,8 @@ +{ + "ticMode": "STANDARD", + "timeout": 42, + "identifier": { + "type": "PORT_NAME", + "value": "COM9" + } +} diff --git a/src/test/resources/tic/stream/configuration/withInvalidIdentifierType.json b/src/test/resources/tic/stream/configuration/withInvalidIdentifierType.json new file mode 100644 index 0000000..801e2d7 --- /dev/null +++ b/src/test/resources/tic/stream/configuration/withInvalidIdentifierType.json @@ -0,0 +1,6 @@ +{ + "identifier": { + "type": "USB", + "value": "device" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/stream/configuration/withInvalidTicMode.json b/src/test/resources/tic/stream/configuration/withInvalidTicMode.json new file mode 100644 index 0000000..d6edcf1 --- /dev/null +++ b/src/test/resources/tic/stream/configuration/withInvalidTicMode.json @@ -0,0 +1,8 @@ +{ + "ticMode": "INVALID_MODE", + "timeout": 42, + "identifier": { + "type": "PORT_NAME", + "value": "COM9" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/stream/configuration/withInvalidTimeout.json b/src/test/resources/tic/stream/configuration/withInvalidTimeout.json new file mode 100644 index 0000000..79284d5 --- /dev/null +++ b/src/test/resources/tic/stream/configuration/withInvalidTimeout.json @@ -0,0 +1,7 @@ +{ + "timeout": 0, + "identifier": { + "type": "PORT_NAME", + "value": "COM1" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/stream/configuration/withMissingIdentifier.json b/src/test/resources/tic/stream/configuration/withMissingIdentifier.json new file mode 100644 index 0000000..f7a6f7f --- /dev/null +++ b/src/test/resources/tic/stream/configuration/withMissingIdentifier.json @@ -0,0 +1,4 @@ +{ + "ticMode": "STANDARD", + "timeout": 15 +} \ No newline at end of file diff --git a/src/test/resources/tic/stream/configuration/withPortIdIdentifier.json b/src/test/resources/tic/stream/configuration/withPortIdIdentifier.json new file mode 100644 index 0000000..3ab7808 --- /dev/null +++ b/src/test/resources/tic/stream/configuration/withPortIdIdentifier.json @@ -0,0 +1,6 @@ +{ + "identifier": { + "type": "PORT_ID", + "value": "12345" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/stream/configuration/withValidPortNameConfig.json b/src/test/resources/tic/stream/configuration/withValidPortNameConfig.json new file mode 100644 index 0000000..bb0c957 --- /dev/null +++ b/src/test/resources/tic/stream/configuration/withValidPortNameConfig.json @@ -0,0 +1,8 @@ +{ + "ticMode":"HISTORIC", + "timeout":25, + "identifier": { + "type":"PORT_NAME", + "value":"COM7" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/Invalid_MissingName.json b/src/test/resources/tic/util/message/codec/Invalid_MissingName.json new file mode 100644 index 0000000..1c4fc0f --- /dev/null +++ b/src/test/resources/tic/util/message/codec/Invalid_MissingName.json @@ -0,0 +1,3 @@ +{ + "type": "REQUEST" +} diff --git a/src/test/resources/tic/util/message/codec/Invalid_MissingType.json b/src/test/resources/tic/util/message/codec/Invalid_MissingType.json new file mode 100644 index 0000000..603a2d9 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/Invalid_MissingType.json @@ -0,0 +1,3 @@ +{ + "name": "GetAvailableTICs" +} diff --git a/src/test/resources/tic/util/message/codec/Invalid_ReadTIC_MissingData.json b/src/test/resources/tic/util/message/codec/Invalid_ReadTIC_MissingData.json new file mode 100644 index 0000000..e7e7aa8 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/Invalid_ReadTIC_MissingData.json @@ -0,0 +1,4 @@ +{ + "type": "REQUEST", + "name": "ReadTIC" +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/Invalid_TypeEvent.json b/src/test/resources/tic/util/message/codec/Invalid_TypeEvent.json new file mode 100644 index 0000000..0b975ad --- /dev/null +++ b/src/test/resources/tic/util/message/codec/Invalid_TypeEvent.json @@ -0,0 +1,4 @@ +{ + "type": "EVENT", + "name": "GetAvailableTICs" +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/Invalid_UnsupportedName.json b/src/test/resources/tic/util/message/codec/Invalid_UnsupportedName.json new file mode 100644 index 0000000..7d9bec5 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/Invalid_UnsupportedName.json @@ -0,0 +1,4 @@ +{ + "type": "REQUEST", + "name": "Nope" +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestGetAvailableTICs.json b/src/test/resources/tic/util/message/codec/RequestGetAvailableTICs.json new file mode 100644 index 0000000..0bfbadd --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestGetAvailableTICs.json @@ -0,0 +1,4 @@ +{ + "type": "REQUEST", + "name": "GetAvailableTICs" +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestGetModemsInfo.json b/src/test/resources/tic/util/message/codec/RequestGetModemsInfo.json new file mode 100644 index 0000000..7b2a4f6 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestGetModemsInfo.json @@ -0,0 +1,4 @@ +{ + "type": "REQUEST", + "name": "GetModemsInfo" +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestReadTIC_WithAllFields.json b/src/test/resources/tic/util/message/codec/RequestReadTIC_WithAllFields.json new file mode 100644 index 0000000..6541c40 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestReadTIC_WithAllFields.json @@ -0,0 +1,9 @@ +{ + "type": "REQUEST", + "name": "ReadTIC", + "data": { + "serialNumber": "010203040506", + "portName": "/dev/ttyUSB0", + "portId": "1-1" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestReadTIC_WithPortId.json b/src/test/resources/tic/util/message/codec/RequestReadTIC_WithPortId.json new file mode 100644 index 0000000..d8b8ffd --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestReadTIC_WithPortId.json @@ -0,0 +1,7 @@ +{ + "type": "REQUEST", + "name": "ReadTIC", + "data": { + "portId": "1-1" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestReadTIC_WithPortName.json b/src/test/resources/tic/util/message/codec/RequestReadTIC_WithPortName.json new file mode 100644 index 0000000..5ff5c68 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestReadTIC_WithPortName.json @@ -0,0 +1,7 @@ +{ + "type": "REQUEST", + "name": "ReadTIC", + "data": { + "portName": "/dev/ttyUSB0" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestReadTIC_WithSerialNumber.json b/src/test/resources/tic/util/message/codec/RequestReadTIC_WithSerialNumber.json new file mode 100644 index 0000000..b68ffed --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestReadTIC_WithSerialNumber.json @@ -0,0 +1,7 @@ +{ + "type": "REQUEST", + "name": "ReadTIC", + "data": { + "serialNumber": "010203040506" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithEmptyArray.json b/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithEmptyArray.json new file mode 100644 index 0000000..233fdf6 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithEmptyArray.json @@ -0,0 +1,5 @@ +{ + "type": "REQUEST", + "name": "SubscribeTIC", + "data": [] +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithNoData.json b/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithNoData.json new file mode 100644 index 0000000..9edfe7e --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithNoData.json @@ -0,0 +1,4 @@ +{ + "type": "REQUEST", + "name": "SubscribeTIC" +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithOneIdentifier.json b/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithOneIdentifier.json new file mode 100644 index 0000000..121cb8a --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithOneIdentifier.json @@ -0,0 +1,7 @@ +{ + "type": "REQUEST", + "name": "SubscribeTIC", + "data": { + "serialNumber": "010203040506" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithOneIdentifierList.json b/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithOneIdentifierList.json new file mode 100644 index 0000000..91e1521 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestSubscribeTIC_WithOneIdentifierList.json @@ -0,0 +1,9 @@ +{ + "type": "REQUEST", + "name": "SubscribeTIC", + "data": [ + { + "portName": "COM7" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithEmptyArray.json b/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithEmptyArray.json new file mode 100644 index 0000000..0931178 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithEmptyArray.json @@ -0,0 +1,5 @@ +{ + "type": "REQUEST", + "name": "UnsubscribeTIC", + "data": [] +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithNoData.json b/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithNoData.json new file mode 100644 index 0000000..0e46a9e --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithNoData.json @@ -0,0 +1,4 @@ +{ + "type": "REQUEST", + "name": "UnsubscribeTIC" +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithOneIdentifier.json b/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithOneIdentifier.json new file mode 100644 index 0000000..3fe8a10 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithOneIdentifier.json @@ -0,0 +1,7 @@ +{ + "type": "REQUEST", + "name": "UnsubscribeTIC", + "data": { + "portName": "COM7" + } +} \ No newline at end of file diff --git a/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithOneIdentifierList.json b/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithOneIdentifierList.json new file mode 100644 index 0000000..b284382 --- /dev/null +++ b/src/test/resources/tic/util/message/codec/RequestUnsubscribeTIC_WithOneIdentifierList.json @@ -0,0 +1,9 @@ +{ + "type": "REQUEST", + "name": "UnsubscribeTIC", + "data": [ + { + "portName": "COM7" + } + ] +} \ No newline at end of file From b77f15f3b52570df60a4e75c7d8ad1a9afe3185b Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Wed, 4 Feb 2026 16:37:31 +0100 Subject: [PATCH 37/59] fix: Remove DataDictionaryDescriptors folder --- .../config/TIC2WebSocketConfiguration.json | 29 ---------------- .../TIC2WebSocketConfiguration.json.license | 6 ---- .../message/EventOnError.json | 26 -------------- .../message/EventOnError.json.license | 6 ---- .../message/EventOnTICData.json | 26 -------------- .../message/EventOnTICData.json.license | 6 ---- .../message/RequestGetAvailableTICs.json | 14 -------- .../RequestGetAvailableTICs.json.license | 6 ---- .../message/RequestGetModemsInfo.json | 14 -------- .../message/RequestGetModemsInfo.json.license | 6 ---- .../message/RequestReadTIC.json | 22 ------------ .../message/RequestReadTIC.json.license | 6 ---- .../message/RequestSubscribeTIC.json | 22 ------------ .../message/RequestSubscribeTIC.json.license | 6 ---- .../message/RequestUnsubscribeTIC.json | 22 ------------ .../RequestUnsubscribeTIC.json.license | 6 ---- .../message/ResponseGetAvailableTICs.json | 34 ------------------- .../ResponseGetAvailableTICs.json.license | 6 ---- .../message/ResponseGetModemsInfo.json | 34 ------------------- .../ResponseGetModemsInfo.json.license | 6 ---- .../message/ResponseReadTIC.json | 34 ------------------- .../message/ResponseReadTIC.json.license | 6 ---- .../message/ResponseSubscribeTIC.json | 26 -------------- .../message/ResponseSubscribeTIC.json.license | 6 ---- .../message/ResponseUnsubscribeTIC.json | 26 -------------- .../ResponseUnsubscribeTIC.json.license | 6 ---- .../tic/core/TICCoreError.json | 30 ---------------- .../tic/core/TICCoreError.json.license | 6 ---- .../tic/core/TICCoreFrame.json | 30 ---------------- .../tic/core/TICCoreFrame.json.license | 6 ---- .../tic/core/TICIdentifier.json | 25 -------------- .../tic/core/TICIdentifier.json.license | 6 ---- 32 files changed, 510 deletions(-) delete mode 100644 src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json delete mode 100644 src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license delete mode 100644 src/dataDictionaryDescriptors/message/EventOnError.json delete mode 100644 src/dataDictionaryDescriptors/message/EventOnError.json.license delete mode 100644 src/dataDictionaryDescriptors/message/EventOnTICData.json delete mode 100644 src/dataDictionaryDescriptors/message/EventOnTICData.json.license delete mode 100644 src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json delete mode 100644 src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license delete mode 100644 src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json delete mode 100644 src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license delete mode 100644 src/dataDictionaryDescriptors/message/RequestReadTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/RequestReadTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json delete mode 100644 src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license delete mode 100644 src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json delete mode 100644 src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license delete mode 100644 src/dataDictionaryDescriptors/message/ResponseReadTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json delete mode 100644 src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreError.json delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICIdentifier.json delete mode 100644 src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license diff --git a/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json b/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json deleted file mode 100644 index f15c099..0000000 --- a/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "package" : "enedis.tic.service.config", - "name": "TIC2WebSocketConfiguration", - "type": "ConfigurationBase", - "attributes": [ - { - "name": "serverPort", - "mandatory": true, - "type": "Number", - "constraint": "MinMax", - "min": 1, - "max": 65535 - }, - { - "name": "ticMode", - "mandatory": false, - "type": "Enum", - "enumType": "TICMode" - }, - { - "name": "ticPortNames", - "mandatory": false, - "type": "List", - "listType": "String", - "constraint": "MinMaxSize", - "min": 1 - } - ] -} diff --git a/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license b/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/config/TIC2WebSocketConfiguration.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/EventOnError.json b/src/dataDictionaryDescriptors/message/EventOnError.json deleted file mode 100644 index a00674f..0000000 --- a/src/dataDictionaryDescriptors/message/EventOnError.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "EventOnError", - "extends" : "Event", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"OnError\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "DataDictionary", - "dataDictionaryType": "TICCoreError" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/EventOnError.json.license b/src/dataDictionaryDescriptors/message/EventOnError.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/EventOnError.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/EventOnTICData.json b/src/dataDictionaryDescriptors/message/EventOnTICData.json deleted file mode 100644 index 180d5fd..0000000 --- a/src/dataDictionaryDescriptors/message/EventOnTICData.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "EventOnTICData", - "extends" : "Event", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"OnTICData\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : true, - "type": "DataDictionary", - "dataDictionaryType": "TICCoreFrame" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/EventOnTICData.json.license b/src/dataDictionaryDescriptors/message/EventOnTICData.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/EventOnTICData.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json b/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json deleted file mode 100644 index afa0795..0000000 --- a/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "RequestGetAvailableTICs", - "extends" : "Request", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"GetAvailableTICs\"", - "removeFromConstructor" : true - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license b/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/RequestGetAvailableTICs.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json b/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json deleted file mode 100644 index 2846109..0000000 --- a/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "RequestGetModemsInfo", - "extends" : "Request", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"GetModemsInfo\"", - "removeFromConstructor" : true - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license b/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/RequestGetModemsInfo.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestReadTIC.json b/src/dataDictionaryDescriptors/message/RequestReadTIC.json deleted file mode 100644 index 30ca65a..0000000 --- a/src/dataDictionaryDescriptors/message/RequestReadTIC.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "RequestReadTIC", - "extends" : "Request", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"ReadTIC\"", - "removeFromConstructor" : true - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : true, - "type": "DataDictionary", - "dataDictionaryType": "TICIdentifier" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestReadTIC.json.license b/src/dataDictionaryDescriptors/message/RequestReadTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/RequestReadTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json b/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json deleted file mode 100644 index f97b2fb..0000000 --- a/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "RequestSubscribeTIC", - "extends" : "Request", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"SubscribeTIC\"", - "removeFromConstructor" : true - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "List", - "listType": "TICIdentifier" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/RequestSubscribeTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json b/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json deleted file mode 100644 index fd33cd9..0000000 --- a/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "RequestUnsubscribeTIC", - "extends" : "Request", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"UnsubscribeTIC\"", - "removeFromConstructor" : true - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "List", - "listType": "TICIdentifier" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/RequestUnsubscribeTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json b/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json deleted file mode 100644 index cb53f58..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "ResponseGetAvailableTICs", - "extends" : "Response", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"GetAvailableTICs\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - }, - { - "name": "errorCode", - "type": "Number" - }, - { - "name": "errorMessage", - "type": "String" - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "List", - "listType": "TICIdentifier" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license b/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseGetAvailableTICs.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json b/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json deleted file mode 100644 index dbc7c86..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "ResponseGetModemsInfo", - "extends" : "Response", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"GetModemsInfo\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - }, - { - "name": "errorCode", - "type": "Number" - }, - { - "name": "errorMessage", - "type": "String" - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "List", - "listType": "TICPortDescriptor" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license b/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseGetModemsInfo.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseReadTIC.json b/src/dataDictionaryDescriptors/message/ResponseReadTIC.json deleted file mode 100644 index 9498969..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseReadTIC.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "ResponseReadTIC", - "extends" : "Response", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"ReadTIC\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - }, - { - "name": "errorCode", - "type": "Number" - }, - { - "name": "errorMessage", - "type": "String" - } - ], - "attributes": [ - { - "name": "data", - "mandatory" : false, - "type": "DataDictionary", - "dataDictionaryType": "TICCoreFrame" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license b/src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseReadTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json b/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json deleted file mode 100644 index 7de0ca3..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "ResponseSubscribeTIC", - "extends" : "Response", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"SubscribeTIC\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - }, - { - "name": "errorCode", - "type": "Number" - }, - { - "name": "errorMessage", - "type": "String" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseSubscribeTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json b/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json deleted file mode 100644 index 834ed75..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "package": "enedis.tic.service.message", - "name": "ResponseUnsubscribeTIC", - "extends" : "Response", - "type": "DataDictionaryBase", - "superAttributes": [ - { - "name": "name", - "type": "String", - "acceptedValues": "\"UnsubscribeTIC\"", - "removeFromConstructor" : true - }, - { - "name": "dateTime", - "type": "LocalDateTime" - }, - { - "name": "errorCode", - "type": "Number" - }, - { - "name": "errorMessage", - "type": "String" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license b/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/message/ResponseUnsubscribeTIC.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreError.json b/src/dataDictionaryDescriptors/tic/core/TICCoreError.json deleted file mode 100644 index 04569e7..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICCoreError.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "package" : "enedis.tic.core", - "name": "TICCoreError", - "type": "DataDictionaryBase", - "attributes": [ - { - "name": "identifier", - "mandatory": true, - "type": "DataDictionary", - "dataDictionaryType": "TICIdentifier" - }, - { - "name": "errorCode", - "mandatory": true, - "type": "Number" - }, - { - "name": "errorMessage", - "mandatory": true, - "type": "String", - "emptyAllow": false - }, - { - "name": "data", - "mandatory": false, - "type": "DataDictionary", - "dataDictionaryType": "DataDictionaryBase" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license b/src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICCoreError.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json b/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json deleted file mode 100644 index 73b606a..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "package" : "enedis.tic.core", - "name": "TICCoreFrame", - "type": "DataDictionaryBase", - "attributes": [ - { - "name": "identifier", - "mandatory": true, - "type": "DataDictionary", - "dataDictionaryType": "TICIdentifier" - }, - { - "name": "mode", - "mandatory": true, - "type": "Enum", - "enumType": "TICMode" - }, - { - "name": "captureDateTime", - "mandatory": true, - "type": "LocalDateTime" - }, - { - "name": "content", - "mandatory": true, - "type": "DataDictionary", - "dataDictionaryType": "DataDictionaryBase" - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license b/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICCoreFrame.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 diff --git a/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json b/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json deleted file mode 100644 index fbd5bda..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "package" : "enedis.tic.core", - "name": "TICIdentifier", - "type": "DataDictionaryBase", - "attributes": [ - { - "name": "portId", - "mandatory": false, - "type": "String", - "emptyAllow": false - }, - { - "name": "portName", - "mandatory": false, - "type": "String", - "emptyAllow": false - }, - { - "name": "serialNumber", - "mandatory": false, - "type": "String", - "emptyAllow": false - } - ] -} \ No newline at end of file diff --git a/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license b/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license deleted file mode 100644 index 18bf6d4..0000000 --- a/src/dataDictionaryDescriptors/tic/core/TICIdentifier.json.license +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (C) 2025 Enedis Smarties team - -SPDX-FileContributor: Jehan BOUSCH -SPDX-FileContributor: Mathieu SABARTHES - -SPDX-License-Identifier: Apache-2.0 From 932469270394e600584c1819f859964c8102873e Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Wed, 4 Feb 2026 16:38:13 +0100 Subject: [PATCH 38/59] fix: Remove enedis main package --- src/main/java/enedis/lab/codec/Codec.java | 36 - .../java/enedis/lab/codec/CodecException.java | 169 --- .../java/enedis/lab/io/PlugSubscriber.java | 31 - src/main/java/enedis/lab/io/PortFinder.java | 24 - .../java/enedis/lab/io/PortPlugNotifier.java | 214 --- .../java/enedis/lab/io/channels/Channel.java | 110 -- .../enedis/lab/io/channels/ChannelBase.java | 143 -- .../lab/io/channels/ChannelConfiguration.java | 296 ---- .../lab/io/channels/ChannelDirection.java | 22 - .../lab/io/channels/ChannelException.java | 194 --- .../lab/io/channels/ChannelListener.java | 73 - .../lab/io/channels/ChannelPhysical.java | 227 --- .../lab/io/channels/ChannelProtocol.java | 30 - .../enedis/lab/io/channels/ChannelStatus.java | 22 - .../ChannelSerialPortConfiguration.java | 432 ------ .../lab/io/channels/serialport/Parity.java | 25 - .../lab/io/datastreams/DataInputStream.java | 99 -- .../enedis/lab/io/datastreams/DataStream.java | 38 - .../lab/io/datastreams/DataStreamBase.java | 373 ----- .../datastreams/DataStreamConfiguration.java | 296 ---- .../io/datastreams/DataStreamDirection.java | 21 - .../io/datastreams/DataStreamException.java | 205 --- .../io/datastreams/DataStreamListener.java | 61 - .../lab/io/datastreams/DataStreamStatus.java | 23 - .../lab/io/datastreams/DataStreamType.java | 27 - .../lab/io/datastreams/DataStreamUser.java | 123 -- .../io/serialport/SerialPortDescriptor.java | 455 ------ .../lab/io/serialport/SerialPortFinder.java | 120 -- .../io/serialport/SerialPortFinderBase.java | 125 -- .../serialport/SerialPortFinderForLinux.java | 703 --------- .../SerialPortFinderForWindows.java | 455 ------ .../java/enedis/lab/io/tic/TICModemType.java | 48 - .../enedis/lab/io/tic/TICPortDescriptor.java | 291 ---- .../java/enedis/lab/io/tic/TICPortFinder.java | 74 - .../enedis/lab/io/tic/TICPortFinderBase.java | 206 --- .../lab/io/tic/TICPortPlugNotifier.java | 131 -- .../enedis/lab/io/usb/USBPortDescriptor.java | 711 --------- .../java/enedis/lab/io/usb/USBPortFinder.java | 73 - .../enedis/lab/io/usb/USBPortFinderBase.java | 201 --- .../java/enedis/lab/protocol/tic/TICMode.java | 115 -- .../tic/channels/ChannelTICSerialPort.java | 326 ----- .../ChannelTICSerialPortConfiguration.java | 250 ---- .../serialport/ChannelSerialPort.java | 823 ----------- .../ChannelSerialPortErrorCode.java | 61 - .../tic/codec/CodecTICFrameHistoric.java | 172 --- .../codec/CodecTICFrameHistoricDataSet.java | 198 --- .../tic/codec/CodecTICFrameStandard.java | 156 -- .../codec/CodecTICFrameStandardDataSet.java | 197 --- .../lab/protocol/tic/codec/TICCodec.java | 295 ---- .../tic/datastreams/TICInputStream.java | 241 ---- .../datastreams/TICStreamConfiguration.java | 220 --- .../lab/protocol/tic/frame/TICError.java | 90 -- .../lab/protocol/tic/frame/TICFrame.java | 522 ------- .../protocol/tic/frame/TICFrameDataSet.java | 313 ---- .../tic/frame/historic/TICFrameHistoric.java | 140 -- .../historic/TICFrameHistoricDataSet.java | 190 --- .../tic/frame/standard/TICException.java | 73 - .../tic/frame/standard/TICFrameStandard.java | 247 ---- .../standard/TICFrameStandardDataSet.java | 352 ----- .../java/enedis/lab/types/BytesArray.java | 1280 ----------------- .../java/enedis/lab/types/DataArrayList.java | 392 ----- .../java/enedis/lab/types/DataDictionary.java | 146 -- .../lab/types/DataDictionaryException.java | 125 -- src/main/java/enedis/lab/types/DataList.java | 57 - .../java/enedis/lab/types/ExceptionBase.java | 132 -- .../types/configuration/Configuration.java | 104 -- .../configuration/ConfigurationBase.java | 186 --- .../configuration/ConfigurationException.java | 172 --- .../datadictionary/DataDictionaryBase.java | 723 ---------- .../types/datadictionary/KeyDescriptor.java | 66 - .../datadictionary/KeyDescriptorBase.java | 210 --- .../KeyDescriptorDataDictionary.java | 152 -- .../datadictionary/KeyDescriptorEnum.java | 142 -- .../datadictionary/KeyDescriptorList.java | 233 --- .../KeyDescriptorListMinMaxSize.java | 170 --- .../KeyDescriptorLocalDateTime.java | 160 --- .../datadictionary/KeyDescriptorNumber.java | 139 -- .../KeyDescriptorNumberMinMax.java | 164 --- .../datadictionary/KeyDescriptorString.java | 140 -- .../java/enedis/lab/util/MinMaxChecker.java | 185 --- .../java/enedis/lab/util/SystemError.java | 120 -- src/main/java/enedis/lab/util/SystemLibC.java | 31 - .../java/enedis/lab/util/message/Event.java | 189 --- .../java/enedis/lab/util/message/Message.java | 216 --- .../enedis/lab/util/message/MessageType.java | 21 - .../java/enedis/lab/util/message/Request.java | 155 -- .../enedis/lab/util/message/Response.java | 259 ---- .../enedis/lab/util/message/ResponseBase.java | 189 --- .../message/exception/MessageException.java | 112 -- .../MessageInvalidContentException.java | 112 -- .../MessageInvalidFormatException.java | 112 -- .../MessageInvalidTypeException.java | 112 -- .../MessageKeyNameDoesntExistException.java | 112 -- .../MessageKeyTypeDoesntExistException.java | 112 -- .../UnsupportedMessageException.java | 112 -- .../factory/AbstractMessageFactory.java | 153 -- .../message/factory/BasicMessageFactory.java | 118 -- .../util/message/factory/EventFactory.java | 24 - .../util/message/factory/MessageFactory.java | 177 --- .../util/message/factory/RequestFactory.java | 24 - .../util/message/factory/ResponseFactory.java | 24 - .../lab/util/task/FilteredNotifier.java | 113 -- .../lab/util/task/FilteredNotifierBase.java | 302 ---- .../java/enedis/lab/util/task/Notifier.java | 47 - .../enedis/lab/util/task/NotifierBase.java | 118 -- .../java/enedis/lab/util/task/Subscriber.java | 16 - src/main/java/enedis/lab/util/task/Task.java | 31 - .../java/enedis/lab/util/task/TaskBase.java | 205 --- .../enedis/lab/util/task/TaskPeriodic.java | 151 -- .../task/TaskPeriodicWithSubscribers.java | 122 -- src/main/java/enedis/lab/util/time/Time.java | 163 --- .../tic/core/ReadNextFrameSubscriber.java | 104 -- src/main/java/enedis/tic/core/TICCore.java | 107 -- .../java/enedis/tic/core/TICCoreBase.java | 581 -------- .../java/enedis/tic/core/TICCoreError.java | 288 ---- .../enedis/tic/core/TICCoreErrorCode.java | 33 - .../enedis/tic/core/TICCoreException.java | 76 - .../java/enedis/tic/core/TICCoreFrame.java | 283 ---- .../java/enedis/tic/core/TICCoreStream.java | 24 - .../enedis/tic/core/TICCoreStreamBase.java | 378 ----- .../enedis/tic/core/TICCoreSubscriber.java | 32 - .../java/enedis/tic/core/TICIdentifier.java | 282 ---- .../tic/service/TIC2WebSocketApplication.java | 424 ------ .../TIC2WebSocketApplicationErrorCode.java | 45 - .../tic/service/TIC2WebSocketCommandLine.java | 140 -- .../service/client/TIC2WebSocketClient.java | 141 -- .../client/TIC2WebSocketClientPool.java | 52 - .../client/TIC2WebSocketClientPoolBase.java | 140 -- .../config/TIC2WebSocketConfiguration.java | 275 ---- .../tic/service/endpoint/EventSender.java | 26 - .../TIC2WebSocketEndPointErrorCode.java | 57 - .../TIC2WebSocketEndPointException.java | 140 -- .../tic/service/message/EventOnError.java | 192 --- .../tic/service/message/EventOnTICData.java | 192 --- .../message/RequestGetAvailableTICs.java | 128 -- .../service/message/RequestGetModemsInfo.java | 123 -- .../tic/service/message/RequestReadTIC.java | 189 --- .../service/message/RequestSubscribeTIC.java | 190 --- .../message/RequestUnsubscribeTIC.java | 190 --- .../message/ResponseGetAvailableTICs.java | 197 --- .../message/ResponseGetModemsInfo.java | 197 --- .../tic/service/message/ResponseReadTIC.java | 196 --- .../service/message/ResponseSubscribeTIC.java | 160 --- .../message/ResponseUnsubscribeTIC.java | 160 --- .../factory/TIC2WebSocketEventFactory.java | 28 - .../factory/TIC2WebSocketMessageFactory.java | 24 - .../factory/TIC2WebSocketRequestFactory.java | 34 - .../factory/TIC2WebSocketResponseFactory.java | 34 - .../TIC2WebSocketChannelInitializer.java | 121 -- .../service/netty/TIC2WebSocketHandler.java | 322 ----- .../service/netty/TIC2WebSocketServer.java | 190 --- .../TIC2WebSocketRequestHandler.java | 27 - .../TIC2WebSocketRequestHandlerBase.java | 341 ----- 153 files changed, 27581 deletions(-) delete mode 100644 src/main/java/enedis/lab/codec/Codec.java delete mode 100644 src/main/java/enedis/lab/codec/CodecException.java delete mode 100644 src/main/java/enedis/lab/io/PlugSubscriber.java delete mode 100644 src/main/java/enedis/lab/io/PortFinder.java delete mode 100644 src/main/java/enedis/lab/io/PortPlugNotifier.java delete mode 100644 src/main/java/enedis/lab/io/channels/Channel.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelBase.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelConfiguration.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelDirection.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelException.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelListener.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelPhysical.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelProtocol.java delete mode 100644 src/main/java/enedis/lab/io/channels/ChannelStatus.java delete mode 100644 src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java delete mode 100644 src/main/java/enedis/lab/io/channels/serialport/Parity.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataInputStream.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStream.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamBase.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamException.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamListener.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamType.java delete mode 100644 src/main/java/enedis/lab/io/datastreams/DataStreamUser.java delete mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java delete mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortFinder.java delete mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java delete mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java delete mode 100644 src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java delete mode 100644 src/main/java/enedis/lab/io/tic/TICModemType.java delete mode 100644 src/main/java/enedis/lab/io/tic/TICPortDescriptor.java delete mode 100644 src/main/java/enedis/lab/io/tic/TICPortFinder.java delete mode 100644 src/main/java/enedis/lab/io/tic/TICPortFinderBase.java delete mode 100644 src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java delete mode 100644 src/main/java/enedis/lab/io/usb/USBPortDescriptor.java delete mode 100644 src/main/java/enedis/lab/io/usb/USBPortFinder.java delete mode 100644 src/main/java/enedis/lab/io/usb/USBPortFinderBase.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/TICMode.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/TICError.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java delete mode 100644 src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java delete mode 100644 src/main/java/enedis/lab/types/BytesArray.java delete mode 100644 src/main/java/enedis/lab/types/DataArrayList.java delete mode 100644 src/main/java/enedis/lab/types/DataDictionary.java delete mode 100644 src/main/java/enedis/lab/types/DataDictionaryException.java delete mode 100644 src/main/java/enedis/lab/types/DataList.java delete mode 100644 src/main/java/enedis/lab/types/ExceptionBase.java delete mode 100644 src/main/java/enedis/lab/types/configuration/Configuration.java delete mode 100644 src/main/java/enedis/lab/types/configuration/ConfigurationBase.java delete mode 100644 src/main/java/enedis/lab/types/configuration/ConfigurationException.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java delete mode 100644 src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java delete mode 100644 src/main/java/enedis/lab/util/MinMaxChecker.java delete mode 100644 src/main/java/enedis/lab/util/SystemError.java delete mode 100644 src/main/java/enedis/lab/util/SystemLibC.java delete mode 100644 src/main/java/enedis/lab/util/message/Event.java delete mode 100644 src/main/java/enedis/lab/util/message/Message.java delete mode 100644 src/main/java/enedis/lab/util/message/MessageType.java delete mode 100644 src/main/java/enedis/lab/util/message/Request.java delete mode 100644 src/main/java/enedis/lab/util/message/Response.java delete mode 100644 src/main/java/enedis/lab/util/message/ResponseBase.java delete mode 100644 src/main/java/enedis/lab/util/message/exception/MessageException.java delete mode 100644 src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java delete mode 100644 src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java delete mode 100644 src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java delete mode 100644 src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java delete mode 100644 src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java delete mode 100644 src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/EventFactory.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/MessageFactory.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/RequestFactory.java delete mode 100644 src/main/java/enedis/lab/util/message/factory/ResponseFactory.java delete mode 100644 src/main/java/enedis/lab/util/task/FilteredNotifier.java delete mode 100644 src/main/java/enedis/lab/util/task/FilteredNotifierBase.java delete mode 100644 src/main/java/enedis/lab/util/task/Notifier.java delete mode 100644 src/main/java/enedis/lab/util/task/NotifierBase.java delete mode 100644 src/main/java/enedis/lab/util/task/Subscriber.java delete mode 100644 src/main/java/enedis/lab/util/task/Task.java delete mode 100644 src/main/java/enedis/lab/util/task/TaskBase.java delete mode 100644 src/main/java/enedis/lab/util/task/TaskPeriodic.java delete mode 100644 src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java delete mode 100644 src/main/java/enedis/lab/util/time/Time.java delete mode 100644 src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java delete mode 100644 src/main/java/enedis/tic/core/TICCore.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreBase.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreError.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreErrorCode.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreException.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreFrame.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreStream.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreStreamBase.java delete mode 100644 src/main/java/enedis/tic/core/TICCoreSubscriber.java delete mode 100644 src/main/java/enedis/tic/core/TICIdentifier.java delete mode 100644 src/main/java/enedis/tic/service/TIC2WebSocketApplication.java delete mode 100644 src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java delete mode 100644 src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java delete mode 100644 src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java delete mode 100644 src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java delete mode 100644 src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java delete mode 100644 src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java delete mode 100644 src/main/java/enedis/tic/service/endpoint/EventSender.java delete mode 100644 src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java delete mode 100644 src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java delete mode 100644 src/main/java/enedis/tic/service/message/EventOnError.java delete mode 100644 src/main/java/enedis/tic/service/message/EventOnTICData.java delete mode 100644 src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java delete mode 100644 src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java delete mode 100644 src/main/java/enedis/tic/service/message/RequestReadTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java delete mode 100644 src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java delete mode 100644 src/main/java/enedis/tic/service/message/ResponseReadTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java delete mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java delete mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java delete mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java delete mode 100644 src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java delete mode 100644 src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java delete mode 100644 src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java delete mode 100644 src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java delete mode 100644 src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java delete mode 100644 src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java diff --git a/src/main/java/enedis/lab/codec/Codec.java b/src/main/java/enedis/lab/codec/Codec.java deleted file mode 100644 index e7932f8..0000000 --- a/src/main/java/enedis/lab/codec/Codec.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.codec; - -/** - * Codec interface - * - * @param - * @param - * - */ -public interface Codec -{ - /** - * Decode type K to type T - * - * @param object - * @return instance of T - * @throws CodecException - */ - public T decode(K object) throws CodecException; - - /** - * Encode type T to type K - * - * @param object - * @return instance of K - * @throws CodecException - */ - public K encode(T object) throws CodecException; -} diff --git a/src/main/java/enedis/lab/codec/CodecException.java b/src/main/java/enedis/lab/codec/CodecException.java deleted file mode 100644 index 678524d..0000000 --- a/src/main/java/enedis/lab/codec/CodecException.java +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.codec; - -/** - * Codec exception - */ -public class CodecException extends Exception -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = 6029680699870915485L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Object data; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public CodecException() - { - super(); - } - - /** - * Constructor - * - * @param message - * @param cause - */ - public CodecException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor - * - * @param message - */ - public CodecException(String message) - { - super(message); - } - - /** - * Constructor - * - * @param message - * @param data - */ - public CodecException(String message, Object data) - { - super(message); - this.data = data; - } - - /** - * Constructor - * - * @param cause - */ - public CodecException(Throwable cause) - { - super(cause); - } - - /** - * Raise CodecException invalid value - * - * @param info - * @throws CodecException - */ - public static void raiseInvalidValue(String info) throws CodecException - { - throw new CodecException("Invalid value : " + info); - } - - /** - * Raise CodecException missing value - * - * @param value - * @throws CodecException - */ - public static void raiseMissingValue(String value) throws CodecException - { - throw new CodecException("Missing value : " + value); - } - - /** - * Raise CodecException inconsistency - * - * @param info - * @throws CodecException - */ - public static void raiseInconsistency(String info) throws CodecException - { - throw new CodecException("Inconsistency : " + info); - } - - /** - * Get data - * - * @return data - */ - public Object getData() - { - return this.data; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/io/PlugSubscriber.java b/src/main/java/enedis/lab/io/PlugSubscriber.java deleted file mode 100644 index 41f007a..0000000 --- a/src/main/java/enedis/lab/io/PlugSubscriber.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io; - -import enedis.lab.util.task.Subscriber; - -/** - * Interface used to be notified when something has been plugged or unplugged - * - * @param the information class associated with plugged or unplugged notification - */ -public interface PlugSubscriber extends Subscriber -{ - /** - * Notification when something has been plugged - * - * @param info the information on what has been plugged - */ - public void onPlugged(T info); - /** - * Notification when something has been unplugged - * - * @param info the information on what has been unplugged - */ - public void onUnplugged(T info); -} diff --git a/src/main/java/enedis/lab/io/PortFinder.java b/src/main/java/enedis/lab/io/PortFinder.java deleted file mode 100644 index eda1e69..0000000 --- a/src/main/java/enedis/lab/io/PortFinder.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io; - -import enedis.lab.types.DataList; - -/** - * Interface used to find all port descriptor - * @param the port descriptor class - */ -public interface PortFinder -{ - /** - * Find all port descriptor - * - * @return The port descriptor list found - */ - public DataList findAll(); -} diff --git a/src/main/java/enedis/lab/io/PortPlugNotifier.java b/src/main/java/enedis/lab/io/PortPlugNotifier.java deleted file mode 100644 index 9b4a8fd..0000000 --- a/src/main/java/enedis/lab/io/PortPlugNotifier.java +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io; - -import java.util.Iterator; -import java.util.concurrent.atomic.AtomicReference; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; -import enedis.lab.util.task.TaskPeriodicWithSubscribers; -import enedis.lab.util.time.Time; - -/** - * Class used to notify when a port has been plugged or unplugged - * - * @param - * the port finder interface - * @param - * the port descriptor class - */ -public class PortPlugNotifier, T> extends TaskPeriodicWithSubscribers> -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program writing on the output stream when a port has been plugged or unplugged - * - * @param - * the port finder interface - * @param - * the port descriptor class - * @param notifier - * the port plug notifier instance - * @param subscriber - * the port plus subscriber instance - */ - public static , T> void main(PortPlugNotifier notifier, PlugSubscriber subscriber) - { - /* 1. Add program shutdown hook when CTRL+C is pressed to exit program properly */ - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() - { - @Override - public void run() - { - notifier.unsubscribe(subscriber); - notifier.stop(); - } - })); - /* 4. Add a subscriber to notification service */ - notifier.subscribe(subscriber); - /* 5. Start the notification service */ - notifier.start(); - /* 6. Execute forever loop until CTRL+C is pressed */ - while (notifier.isRunning()) - { - Time.sleep(1000); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private AtomicReference finder = new AtomicReference(); - private DataList descriptors = new DataArrayList(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor with all parameters - * - * @param period - * the period (in milliseconds) used to look for plugged or unplugged serial port - * @param finder - * the serial port finder interface used to find all serial port descriptors - */ - public PortPlugNotifier(long period, F finder) - { - super(period); - this.setFinder(finder); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Set the finder - * - * @param finder - * the serial port finder interface used to find all serial port descriptors - * @throws IllegalArgumentException - * if finder is null - */ - public void setFinder(F finder) - { - if (finder == null) - { - throw new IllegalArgumentException("Cannot set null finder"); - } - this.finder.set(finder); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void process() - { - DataList newDescriptors = this.finder.get().findAll(); - - this.checkAndNotifyIfUnplugged(newDescriptors); - this.checkAndNotifyIfPlugged(newDescriptors); - this.updateDescriptors(newDescriptors); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void checkAndNotifyIfUnplugged(DataList newDescriptors) - { - Iterator it = this.descriptors.iterator(); - - while (it.hasNext()) - { - T descriptor = it.next(); - if (!newDescriptors.contains(descriptor)) - { - this.notifyOnUnplugged(descriptor); - } - } - } - - private void checkAndNotifyIfPlugged(DataList newDescriptors) - { - Iterator it = newDescriptors.iterator(); - - while (it.hasNext()) - { - T newDescriptor = it.next(); - if (!this.descriptors.contains(newDescriptor)) - { - this.notifyOnPlugged(newDescriptor); - } - } - } - - private void updateDescriptors(DataList newDescriptors) - { - synchronized (this.descriptors) - { - this.descriptors.clear(); - this.descriptors.addAll(newDescriptors); - } - } - - private void notifyOnPlugged(T info) - { - for (PlugSubscriber subscriber : this.notifier.getSubscribers()) - { - subscriber.onPlugged(info); - } - } - - private void notifyOnUnplugged(T info) - { - for (PlugSubscriber subscriber : this.notifier.getSubscribers()) - { - subscriber.onUnplugged(info); - } - } -} diff --git a/src/main/java/enedis/lab/io/channels/Channel.java b/src/main/java/enedis/lab/io/channels/Channel.java deleted file mode 100644 index 45ee2ec..0000000 --- a/src/main/java/enedis/lab/io/channels/Channel.java +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.util.task.Notifier; -import enedis.lab.util.task.Task; - -/** - * Channel interface - */ -public interface Channel extends Task, Notifier -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Setup the channel from a configuration - * - * @param configuration - * the channel configuration - * - * @throws ChannelException - */ - public void setup(ChannelConfiguration configuration) throws ChannelException; - - /** - * Read data on the channel - * - * @return bytes - * @throws ChannelException - */ - public byte[] read() throws ChannelException; - - /** - * Write data on the channel - * - * @param data - * @throws ChannelException - */ - public void write(byte[] data) throws ChannelException; - - /** - * Get the channel name - * - * @return the channel's name - */ - public String getName(); - - /** - * Get the channel protocol - * - * @return the protocol used by the channel - */ - public ChannelProtocol getProtocol(); - - /** - * Get the channel direction - * - * @return the direction of the channel - */ - public ChannelDirection getDirection(); - - /** - * Get channel status - * - * @return the channel's status - */ - public ChannelStatus getStatus(); - - /** - * Get channel configuration - * - * @return channel configuration - */ - public ChannelConfiguration getConfiguration(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // none - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // none -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelBase.java b/src/main/java/enedis/lab/io/channels/ChannelBase.java deleted file mode 100644 index 140687a..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelBase.java +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.task.TaskPeriodic; - -/** - * Channel base - */ -public abstract class ChannelBase extends TaskPeriodic implements Channel -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Logger logger; - protected ChannelConfiguration configuration; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param configuration - * configuration used to set the channel - * @throws ChannelException - */ - protected ChannelBase(ChannelConfiguration configuration) throws ChannelException - { - this.logger = LogManager.getLogger(this.getClass()); - this.setup(configuration); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Channel - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException - { - /* 1. Check configuration */ - if (configuration == null) - { - ChannelException.raiseInvalidConfiguration("null"); - } - /* 2. Copy configuration */ - ChannelConfiguration configurationBackUp = null; - try - { - if (this.configuration == null) - { - this.configuration = (ChannelConfiguration) configuration.clone(); - } - else - { - configurationBackUp = (ChannelConfiguration) this.configuration.clone(); - this.configuration.copy(configuration); - } - } - catch (DataDictionaryException exception) - { - ChannelException.raiseInvalidConfiguration(exception.getMessage()); - } - /* 3. Set up from internal configuration */ - try - { - this.setup(); - } - catch (Exception exception) - { - /* 4. Restore internal configuration */ - if (configurationBackUp != null) - { - try - { - this.configuration.copy(configurationBackUp); - } - catch (DataDictionaryException dataDictionaryException) - { - this.logger.error("", dataDictionaryException); - } - } - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setup() throws ChannelException - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java b/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java deleted file mode 100644 index 18fac19..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelConfiguration.java +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.configuration.ConfigurationBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * ChannelConfiguration class - * - * Generated - */ -public class ChannelConfiguration extends ConfigurationBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_NAME = "name"; - protected static final String KEY_PROTOCOL = "protocol"; - protected static final String KEY_DIRECTION = "direction"; - protected static final String KEY_ALIAS = "alias"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kName; - protected KeyDescriptorEnum kProtocol; - protected KeyDescriptorEnum kDirection; - protected KeyDescriptorString kAlias; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ChannelConfiguration() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ChannelConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ChannelConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public ChannelConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param protocol - * @param direction - * @param alias - * @throws DataDictionaryException - */ - public ChannelConfiguration(String name, ChannelProtocol protocol, ChannelDirection direction, String alias) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setProtocol(protocol); - this.setDirection(direction); - this.setAlias(alias); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ConfigurationBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get name - * - * @return the name - */ - public String getName() - { - return (String) this.data.get(KEY_NAME); - } - - /** - * Get protocol - * - * @return the protocol - */ - public ChannelProtocol getProtocol() - { - return (ChannelProtocol) this.data.get(KEY_PROTOCOL); - } - - /** - * Get direction - * - * @return the direction - */ - public ChannelDirection getDirection() - { - return (ChannelDirection) this.data.get(KEY_DIRECTION); - } - - /** - * Get alias - * - * @return the alias - */ - public String getAlias() - { - return (String) this.data.get(KEY_ALIAS); - } - - /** - * Set name - * - * @param name - * @throws DataDictionaryException - */ - public void setName(String name) throws DataDictionaryException - { - this.setName((Object) name); - } - - /** - * Set protocol - * - * @param protocol - * @throws DataDictionaryException - */ - public void setProtocol(ChannelProtocol protocol) throws DataDictionaryException - { - this.setProtocol((Object) protocol); - } - - /** - * Set direction - * - * @param direction - * @throws DataDictionaryException - */ - public void setDirection(ChannelDirection direction) throws DataDictionaryException - { - this.setDirection((Object) direction); - } - - /** - * Set alias - * - * @param alias - * @throws DataDictionaryException - */ - public void setAlias(String alias) throws DataDictionaryException - { - this.setAlias((Object) alias); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setName(Object name) throws DataDictionaryException - { - this.data.put(KEY_NAME, this.kName.convert(name)); - } - - protected void setProtocol(Object protocol) throws DataDictionaryException - { - this.data.put(KEY_PROTOCOL, this.kProtocol.convert(protocol)); - } - - protected void setDirection(Object direction) throws DataDictionaryException - { - this.data.put(KEY_DIRECTION, this.kDirection.convert(direction)); - } - - protected void setAlias(Object alias) throws DataDictionaryException - { - this.data.put(KEY_ALIAS, this.kAlias.convert(alias)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kName = new KeyDescriptorString(KEY_NAME, true, false); - this.keys.add(this.kName); - - this.kProtocol = new KeyDescriptorEnum(KEY_PROTOCOL, true, ChannelProtocol.class); - this.keys.add(this.kProtocol); - - this.kDirection = new KeyDescriptorEnum(KEY_DIRECTION, true, ChannelDirection.class); - this.keys.add(this.kDirection); - - this.kAlias = new KeyDescriptorString(KEY_ALIAS, false, false); - this.keys.add(this.kAlias); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelDirection.java b/src/main/java/enedis/lab/io/channels/ChannelDirection.java deleted file mode 100644 index 95398f3..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelDirection.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -/** - * Enumeration of channel direction - * - */ -public enum ChannelDirection -{ - /** Reception */ - RX, - /** Sending */ - TX, - /** Reception and sending */ - RXTX -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelException.java b/src/main/java/enedis/lab/io/channels/ChannelException.java deleted file mode 100644 index 18ff7c0..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelException.java +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.types.ExceptionBase; - -/** - * Class used for the channel exceptions - */ -public class ChannelException extends ExceptionBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -3507876436535833098L; - - /** Invalid channel error code */ - public static final int ERRCODE_INVALID_CHANNEL = -5; - /** Already exists error code */ - public static final int ERRCODE_ALREADY_EXISTS = -6; - /** Internal error error code */ - public static final int ERRCODE_INTERNAL_ERROR = -7; - /** Channel not ready error code */ - public static final int ERRCODE_CHANNEL_NOT_READY = -8; - /** Invalid configuration type error code */ - public static final int ERRCODE_INVALID_CONFIGURATION_TYPE = -9; - /** Invalid configuration error code */ - public static final int ERRCODE_INVALID_CONFIGURATION = -10; - /** Operation denied error code */ - public static final int ERRCODE_OPERATION_DENIED = -11; - /** Channel doesn't exist error code */ - public static final int ERRCODE_CHANNEL_DOESNT_EXIST = -12; - /** Unexpected error code */ - public static final int ERRCODE_UNEXPECTED = -99; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * @param channelType - * @throws ChannelException - */ - public static void raiseUnhandledChannelType(String channelType) throws ChannelException - { - throw new ChannelException(ERRCODE_INVALID_CHANNEL, channelType + " channel is not handled"); - } - - /** - * @param channelType - * @param expectedChannelType - * @throws ChannelException - */ - public static void raiseConflictingChannelType(String channelType, String expectedChannelType) throws ChannelException - { - throw new ChannelException(ERRCODE_INVALID_CHANNEL, channelType + " channel is conflicting with " + expectedChannelType); - } - - /** - * @param info - * @throws ChannelException - */ - public static void raiseInternalError(String info) throws ChannelException - { - throw new ChannelException(ERRCODE_INTERNAL_ERROR, info); - } - - /** - * @param info - * @throws ChannelException - */ - public static void raiseChannelNotReady(String info) throws ChannelException - { - throw new ChannelException(ERRCODE_CHANNEL_NOT_READY, info); - } - - /** - * @param configuration - * @param expected_configuration_name - * @throws ChannelException - */ - public static void raiseInvalidConfigurationType(ChannelConfiguration configuration, String expected_configuration_name) throws ChannelException - { - throw new ChannelException(ERRCODE_INVALID_CONFIGURATION_TYPE, - "Configuration " + configuration.getClass().getSimpleName() + " is not a valid type (" + expected_configuration_name + " expected)"); - } - - /** - * @param info - * @throws ChannelException - */ - public static void raiseInvalidConfiguration(String info) throws ChannelException - { - throw new ChannelException(ERRCODE_INVALID_CONFIGURATION, "Configuration is invalid (" + info + ")"); - } - - /** - * @param operation - * @throws ChannelException - */ - public static void raiseOperationDenied(String operation) throws ChannelException - { - throw new ChannelException(ERRCODE_OPERATION_DENIED, "Operation \'" + operation + "\' is not allowed"); - } - - /** - * @param info - * @throws ChannelException - */ - public static void raiseUnexpectedError(String info) throws ChannelException - { - throw new ChannelException(ERRCODE_UNEXPECTED, "Unexpected error occurs : " + info); - } - - /** - * @param channelName - * @throws ChannelException - */ - public static void raiseAlreadyExists(String channelName) throws ChannelException - { - throw new ChannelException(ERRCODE_ALREADY_EXISTS, "Channel " + channelName + " already exists"); - } - - /** - * @param channelName - * @throws ChannelException - */ - public static void raiseDoesntExist(String channelName) throws ChannelException - { - throw new ChannelException(ERRCODE_CHANNEL_DOESNT_EXIST, "Channel " + channelName + " doesn't exist"); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * @param code - * @param info - */ - public ChannelException(int code, String info) - { - super(code, info); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelListener.java b/src/main/java/enedis/lab/io/channels/ChannelListener.java deleted file mode 100644 index e31a37d..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelListener.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.Subscriber; - -/** - * Channel listener - */ -public interface ChannelListener extends Subscriber -{ - /** - * On data read - * - * @param channelName - * the channel name - * @param data - * the bytes read - */ - public void onDataRead(String channelName, byte[] data); - - /** - * On data written - * - * @param channelName - * the channel name - * @param data - * the bytes written - */ - public void onDataWritten(String channelName, byte[] data); - - /** - * On status changed - * - * @param channelName - * the channel name - * @param status - * the new status - */ - public void onStatusChanged(String channelName, ChannelStatus status); - - /** - * On error detected - * - * @param channelName - * the channel name - * @param errorCode - * the error code - * @param errorMessage - * the error message - */ - public void onErrorDetected(String channelName, int errorCode, String errorMessage); - - /** - * On error detected - * - * @param channelName - * the channel name - * @param errorCode - * the error code - * @param errorMessage - * the error message - * @param data - * the data associated with error message - */ - public void onErrorDetected(String channelName, int errorCode, String errorMessage, DataDictionary data); -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelPhysical.java b/src/main/java/enedis/lab/io/channels/ChannelPhysical.java deleted file mode 100644 index 5295653..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelPhysical.java +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -import java.util.Collection; - -import enedis.lab.util.task.NotifierBase; - -/** - * Channel physical - */ -public abstract class ChannelPhysical extends ChannelBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private NotifierBase notifier; - protected ChannelStatus status; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param configuration - * configuration used to set the channel - * @throws ChannelException - */ - protected ChannelPhysical(ChannelConfiguration configuration) throws ChannelException - { - super(configuration); - this.init(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Channel - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void subscribe(ChannelListener listener) - { - this.notifier.subscribe(listener); - } - - @Override - public void unsubscribe(ChannelListener listener) - { - this.notifier.unsubscribe(listener); - } - - @Override - public boolean hasSubscriber(ChannelListener subscriber) - { - return this.notifier.hasSubscriber(subscriber); - } - - @Override - public Collection getSubscribers() - { - return this.notifier.getSubscribers(); - } - - @Override - public String getName() - { - return this.configuration.getName(); - } - - @Override - public ChannelProtocol getProtocol() - { - return this.configuration.getProtocol(); - } - - @Override - public ChannelDirection getDirection() - { - return this.configuration.getDirection(); - } - - @Override - public ChannelStatus getStatus() - { - return this.status; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public ChannelConfiguration getConfiguration() - { - return this.configuration; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Set the channel status - * - * @param status - * channel status - */ - protected void setStatus(ChannelStatus status) - { - this.setStatus(status, true); - } - - /** - * Set the channel status. If 'notify' argument is false, no channel listener will be notified about a new status, - * otherwise, the will - * - * @param status - * channel status - * @param notify - * if 'true', the channel's listener will be notified, else it will not - */ - protected void setStatus(ChannelStatus status, boolean notify) - { - if (status != this.status) - { - this.status = status; - if (status == ChannelStatus.ERROR) - { - this.logger.error("Channel " + this.getName() + " new status : " + status.name()); - } - else - { - this.logger.info("Channel " + this.getName() + " new status : " + status.name()); - } - if (true == notify) - { - this.notifyOnStatusChanged(); - } - } - } - - protected void notifyOnDataRead(byte[] data) - { - if (data != null) - { - for (ChannelListener subscriber : this.notifier.getSubscribers()) - { - subscriber.onDataRead(this.getName(), data); - } - } - } - - protected void notifyOnDataWritten(byte[] data) - { - if (data != null) - { - for (ChannelListener subscriber : this.notifier.getSubscribers()) - { - subscriber.onDataWritten(this.getName(), data); - } - } - } - - protected void notifyOnStatusChanged() - { - for (ChannelListener subscriber : this.notifier.getSubscribers()) - { - subscriber.onStatusChanged(this.getName(), this.status); - } - } - - protected void notifyOnErrorDetected(int errorCode, String errorMessage) - { - for (ChannelListener subscriber : this.notifier.getSubscribers()) - { - subscriber.onErrorDetected(this.getName(), errorCode, errorMessage); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void init() - { - this.status = ChannelStatus.STOPPED; - this.notifier = new NotifierBase(); - } -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java b/src/main/java/enedis/lab/io/channels/ChannelProtocol.java deleted file mode 100644 index 82206e5..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelProtocol.java +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -/** - * Enumeration of channel protocol - * - */ -public enum ChannelProtocol -{ - /** File */ - FILE, - /** Serial port */ - SERIAL_PORT, - /** RTU modbus */ - MODBUS_RTU, - /** TCP modbus */ - MODBUS_TCP, - /** modbus stub */ - MODBUS_STUB, - /** TCP */ - TCP, - /** UDP */ - UDP, -} diff --git a/src/main/java/enedis/lab/io/channels/ChannelStatus.java b/src/main/java/enedis/lab/io/channels/ChannelStatus.java deleted file mode 100644 index acae4c5..0000000 --- a/src/main/java/enedis/lab/io/channels/ChannelStatus.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels; - -/** - * Enumeration of channel status - * - */ -public enum ChannelStatus -{ - /** Stopped */ - STOPPED, - /** Started */ - STARTED, - /** Error */ - ERROR -} diff --git a/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java b/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java deleted file mode 100644 index de7e171..0000000 --- a/src/main/java/enedis/lab/io/channels/serialport/ChannelSerialPortConfiguration.java +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.io.channels.ChannelConfiguration; -import enedis.lab.io.channels.ChannelDirection; -import enedis.lab.io.channels.ChannelProtocol; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * ChannelSerialPortConfiguration class - * - * Generated - */ -public class ChannelSerialPortConfiguration extends ChannelConfiguration -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_PORT_ID = "portId"; - protected static final String KEY_PORT_NAME = "portName"; - protected static final String KEY_BAUDRATE = "baudrate"; - protected static final String KEY_PARITY = "parity"; - protected static final String KEY_DATA_BITS = "dataBits"; - protected static final String KEY_STOP_BITS = "stopBits"; - protected static final String KEY_SYNC_READ_TIMEOUT = "syncReadTimeout"; - - private static final ChannelProtocol PROTOCOL_ACCEPTED_VALUE = ChannelProtocol.SERIAL_PORT; - private static final ChannelDirection DIRECTION_ACCEPTED_VALUE = ChannelDirection.RXTX; - private static final Number[] BAUDRATE_ACCEPTED_VALUES = { 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1843200, - 3686400 }; - private static final Number[] DATA_BITS_ACCEPTED_VALUES = { 5, 6, 7, 8 }; - private static final Number[] STOP_BITS_ACCEPTED_VALUES = { 1.0d, 1.5d, 2.0d }; - private static final Number SYNC_READ_TIMEOUT_DEFAULT_VALUE = 10000; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kPortId; - protected KeyDescriptorString kPortName; - protected KeyDescriptorNumber kBaudrate; - protected KeyDescriptorEnum kParity; - protected KeyDescriptorNumber kDataBits; - protected KeyDescriptorNumber kStopBits; - protected KeyDescriptorNumber kSyncReadTimeout; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ChannelSerialPortConfiguration() - { - super(); - this.loadKeyDescriptors(); - - this.kProtocol.setAcceptedValues(PROTOCOL_ACCEPTED_VALUE); - this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ChannelSerialPortConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ChannelSerialPortConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public ChannelSerialPortConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param alias - * @param portId - * @param portName - * @param baudrate - * @param parity - * @param dataBits - * @param stopBits - * @param syncReadTimeout - * @throws DataDictionaryException - */ - public ChannelSerialPortConfiguration(String name, String alias, String portId, String portName, Number baudrate, Parity parity, Number dataBits, Number stopBits, - Number syncReadTimeout) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setAlias(alias); - this.setPortId(portId); - this.setPortName(portName); - this.setBaudrate(baudrate); - this.setParity(parity); - this.setDataBits(dataBits); - this.setStopBits(stopBits); - this.setSyncReadTimeout(syncReadTimeout); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ChannelConfiguration - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_PORT_ID) && !this.exists(KEY_PORT_NAME)) - { - throw new DataDictionaryException("Neither " + KEY_PORT_ID + " nor " + KEY_PORT_NAME + " is defined"); - } - if (!this.exists(KEY_PROTOCOL)) - { - this.setProtocol(PROTOCOL_ACCEPTED_VALUE); - } - if (!this.exists(KEY_DIRECTION)) - { - this.setDirection(DIRECTION_ACCEPTED_VALUE); - } - if (!this.exists(KEY_SYNC_READ_TIMEOUT)) - { - this.setSyncReadTimeout(SYNC_READ_TIMEOUT_DEFAULT_VALUE); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get port id - * - * @return the port id - */ - public String getPortId() - { - return (String) this.data.get(KEY_PORT_ID); - } - - /** - * Get port name - * - * @return the port name - */ - public String getPortName() - { - return (String) this.data.get(KEY_PORT_NAME); - } - - /** - * Get baudrate - * - * @return the baudrate - */ - public Number getBaudrate() - { - return (Number) this.data.get(KEY_BAUDRATE); - } - - /** - * Get parity - * - * @return the parity - */ - public Parity getParity() - { - return (Parity) this.data.get(KEY_PARITY); - } - - /** - * Get data bits - * - * @return the data bits - */ - public Number getDataBits() - { - return (Number) this.data.get(KEY_DATA_BITS); - } - - /** - * Get stop bits - * - * @return the stop bits - */ - public Number getStopBits() - { - return (Number) this.data.get(KEY_STOP_BITS); - } - - /** - * Get sync read timeout - * - * @return the sync read timeout - */ - public Number getSyncReadTimeout() - { - return (Number) this.data.get(KEY_SYNC_READ_TIMEOUT); - } - - /** - * Set port id - * - * @param portId - * @throws DataDictionaryException - */ - public void setPortId(String portId) throws DataDictionaryException - { - this.setPortId((Object) portId); - } - - /** - * Set port name - * - * @param portName - * @throws DataDictionaryException - */ - public void setPortName(String portName) throws DataDictionaryException - { - this.setPortName((Object) portName); - } - - /** - * Set baudrate - * - * @param baudrate - * @throws DataDictionaryException - */ - public void setBaudrate(Number baudrate) throws DataDictionaryException - { - this.setBaudrate((Object) baudrate); - } - - /** - * Set parity - * - * @param parity - * @throws DataDictionaryException - */ - public void setParity(Parity parity) throws DataDictionaryException - { - this.setParity((Object) parity); - } - - /** - * Set data bits - * - * @param dataBits - * @throws DataDictionaryException - */ - public void setDataBits(Number dataBits) throws DataDictionaryException - { - this.setDataBits((Object) dataBits); - } - - /** - * Set stop bits - * - * @param stopBits - * @throws DataDictionaryException - */ - public void setStopBits(Number stopBits) throws DataDictionaryException - { - this.setStopBits((Object) stopBits); - } - - /** - * Set sync read timeout - * - * @param syncReadTimeout - * @throws DataDictionaryException - */ - public void setSyncReadTimeout(Number syncReadTimeout) throws DataDictionaryException - { - this.setSyncReadTimeout((Object) syncReadTimeout); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setPortId(Object portId) throws DataDictionaryException - { - this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); - } - - protected void setPortName(Object portName) throws DataDictionaryException - { - this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); - } - - protected void setBaudrate(Object baudrate) throws DataDictionaryException - { - this.data.put(KEY_BAUDRATE, this.kBaudrate.convert(baudrate)); - } - - protected void setParity(Object parity) throws DataDictionaryException - { - this.data.put(KEY_PARITY, this.kParity.convert(parity)); - } - - protected void setDataBits(Object dataBits) throws DataDictionaryException - { - this.data.put(KEY_DATA_BITS, this.kDataBits.convert(dataBits)); - } - - protected void setStopBits(Object stopBits) throws DataDictionaryException - { - this.data.put(KEY_STOP_BITS, this.kStopBits.convert(stopBits)); - } - - protected void setSyncReadTimeout(Object syncReadTimeout) throws DataDictionaryException - { - this.data.put(KEY_SYNC_READ_TIMEOUT, this.kSyncReadTimeout.convert(syncReadTimeout)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); - this.keys.add(this.kPortId); - - this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); - this.keys.add(this.kPortName); - - this.kBaudrate = new KeyDescriptorNumber(KEY_BAUDRATE, true); - this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); - this.keys.add(this.kBaudrate); - - this.kParity = new KeyDescriptorEnum(KEY_PARITY, true, Parity.class); - this.keys.add(this.kParity); - - this.kDataBits = new KeyDescriptorNumber(KEY_DATA_BITS, true); - this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUES); - this.keys.add(this.kDataBits); - - this.kStopBits = new KeyDescriptorNumber(KEY_STOP_BITS, true); - this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUES); - this.keys.add(this.kStopBits); - - this.kSyncReadTimeout = new KeyDescriptorNumber(KEY_SYNC_READ_TIMEOUT, false); - this.keys.add(this.kSyncReadTimeout); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/channels/serialport/Parity.java b/src/main/java/enedis/lab/io/channels/serialport/Parity.java deleted file mode 100644 index b5caaf4..0000000 --- a/src/main/java/enedis/lab/io/channels/serialport/Parity.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -/** - * Parity enum - */ -public enum Parity -{ - /** None */ - NONE, - /** Even */ - EVEN, - /** Odd */ - ODD, - /** Mark */ - MARK, - /** Space */ - SPACE -} \ No newline at end of file diff --git a/src/main/java/enedis/lab/io/datastreams/DataInputStream.java b/src/main/java/enedis/lab/io/datastreams/DataInputStream.java deleted file mode 100644 index 430d8bd..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataInputStream.java +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.types.DataDictionary; - -/** - * Data input stream - */ -public abstract class DataInputStream extends DataStreamBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor using configuration - * - * @param configuration - * @throws DataStreamException - */ - public DataInputStream(DataStreamConfiguration configuration) throws DataStreamException - { - super(configuration); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataStream - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public final void write(DataDictionary data) throws DataStreamException - { - DataStreamException.raiseOperationDenied("write"); - } - - @Override - public void setup(DataStreamConfiguration configuration) throws DataStreamException - { - super.setup(configuration); - - if (DataStreamDirection.INPUT != configuration.getDirection()) - { - DataStreamException.raiseInvalidConfiguration(DataStreamConfiguration.KEY_DIRECTION + "=" + configuration.getDirection().name() + "(expected " + DataStreamDirection.INPUT.name() + ")"); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStream.java b/src/main/java/enedis/lab/io/datastreams/DataStream.java deleted file mode 100644 index fb89041..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStream.java +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * DataStream interface - */ -public interface DataStream extends DataStreamUser -{ - /** - * Open the stream - * - * @throws DataStreamException - */ - public void open() throws DataStreamException; - - /** - * Close the stream - * - * @throws DataStreamException - */ - public void close() throws DataStreamException; - - /** - * Setup the stream from a configuration - * - * @param configuration - * configuration given for the stream - * @throws DataStreamException - * if an error occurs - */ - public void setup(DataStreamConfiguration configuration) throws DataStreamException; -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java b/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java deleted file mode 100644 index 578325f..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamBase.java +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import java.util.Collection; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.io.channels.Channel; -import enedis.lab.io.channels.ChannelListener; -import enedis.lab.io.channels.ChannelStatus; -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.NotifierBase; - -/** - * DataStream Base - */ -public abstract class DataStreamBase implements DataStream, ChannelListener -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected NotifierBase notifier; - protected DataStreamConfiguration configuration; - private DataStreamStatus status; - protected Channel channel; - protected Logger logger; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param configuration - * configuration used to set the stream - * @throws DataStreamException - */ - public DataStreamBase(DataStreamConfiguration configuration) throws DataStreamException - { - this.init(configuration); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ChannelListener - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onStatusChanged(String channelName, ChannelStatus channelStatus) - { - DataStreamStatus status_cpy = this.status; - - switch (channelStatus) - { - case STOPPED: - { - if (DataStreamStatus.CLOSED != this.status) - { - this.setStatus(DataStreamStatus.ERROR); - } - break; - } - case STARTED: - { - if (DataStreamStatus.ERROR == this.status) - { - this.setStatus(DataStreamStatus.OPENED); - } - break; - } - case ERROR: - { - this.setStatus(DataStreamStatus.ERROR); - break; - } - - default: - { - - } - } - - if (!this.notifier.getSubscribers().isEmpty() && (status_cpy != this.status)) - { - this.notifyOnStatusChanged(this.status); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataStream - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void open() throws DataStreamException - { - String channelName = this.configuration.getChannelName(); - - if (null != channelName) - { - if (null != this.channel) - { - this.channel.subscribe(this); - this.setStatus(DataStreamStatus.OPENED); - this.logger.info("Stream " + this.getName() + " start"); - } - else - { - this.setStatus(DataStreamStatus.ERROR); - DataStreamException.raiseChannelError(channelName, "channel does not exist"); - } - } - } - - @Override - public void close() throws DataStreamException - { - String channelName = this.configuration.getChannelName(); - - if (null != channelName) - { - if (null != this.channel) - { - this.channel.unsubscribe(this); - this.setStatus(DataStreamStatus.CLOSED); - this.logger.info("Service " + this.getName() + " stop"); - } - else - { - this.setStatus(DataStreamStatus.ERROR); - DataStreamException.raiseChannelError(channelName, "channel does not exist"); - } - } - } - - @Override - public void setup(DataStreamConfiguration configuration) throws DataStreamException - { - if (this.status == DataStreamStatus.OPENED) - { - DataStreamException.raiseInternalError("Cannot setup stream " + this.getName() + " (already open)"); - } - this.configuration = configuration; - } - - @Override - public void subscribe(DataStreamListener listener) - { - this.notifier.subscribe(listener); - } - - @Override - public void unsubscribe(DataStreamListener listener) - { - this.notifier.unsubscribe(listener); - } - - @Override - public boolean hasSubscriber(DataStreamListener subscriber) - { - return this.notifier.hasSubscriber(subscriber); - } - - @Override - public Collection getSubscribers() - { - return this.notifier.getSubscribers(); - } - - @Override - public String getName() - { - return this.configuration.getName(); - } - - @Override - public DataStreamConfiguration getConfiguration() - { - return (DataStreamConfiguration) this.configuration.clone(); - } - - @Override - public DataStreamDirection getDirection() - { - return this.configuration.getDirection(); - } - - @Override - public DataStreamType getType() - { - return this.configuration.getType(); - } - - @Override - public DataStreamStatus getStatus() - { - return this.status; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Set the channel reference - * - * @param channel - */ - public void setChannel(Channel channel) - { - this.channel = channel; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Notify onDataReceived listeners - * - * @param data - * new data received - */ - protected void notifyOnDataReceived(DataDictionary data) - { - if (data != null) - { - for (DataStreamListener listener : this.notifier.getSubscribers()) - { - listener.onDataReceived(this.getName(), data); - } - } - } - - /** - * Notify onDataSent listeners - * - * @param data - * new data sent - */ - protected void notifyOnDataSent(DataDictionary data) - { - if (data != null) - { - for (DataStreamListener listener : this.notifier.getSubscribers()) - { - listener.onDataSent(this.getName(), data); - } - } - } - - /** - * Notify onStatusChanged listeners - * - * @param newStatus - * new status - */ - protected void notifyOnStatusChanged(DataStreamStatus newStatus) - { - if (newStatus != null) - { - for (DataStreamListener listener : this.notifier.getSubscribers()) - { - listener.onStatusChanged(this.getName(), newStatus); - } - } - } - - /** - * Notify onErrorDetected listeners - * - * @param errorCode - * the error code - * @param errorMessage - * the error message - * @param data - * the data associated with the error - */ - protected void notifyOnErrorDetected(int errorCode, String errorMessage, DataDictionary data) - { - for (DataStreamListener listener : this.notifier.getSubscribers()) - { - listener.onErrorDetected(this.getName(), errorCode, errorMessage, data); - } - } - - /** - * Set the channel status - * - * @param status - * channel status - */ - protected void setStatus(DataStreamStatus status) - { - this.setStatus(status, true); - } - - /** - * Set the channel status. If 'notify' argument is false, no channel listener will be notified about a new status, - * otherwise, the will - * - * @param status - * channel status - * @param notify - * if 'true', the channel's listener will be notified, else it will not - */ - protected void setStatus(DataStreamStatus status, boolean notify) - { - if (status != this.status) - { - this.status = status; - - if (true == notify) - { - this.notifyOnStatusChanged(this.status); - } - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void init(DataStreamConfiguration configuration) throws DataStreamException - { - this.notifier = new NotifierBase(); - this.status = DataStreamStatus.UNKNOWN; - this.configuration = configuration; - this.logger = LogManager.getLogger(this.getClass()); - this.setup(configuration); - } -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java b/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java deleted file mode 100644 index ddd6069..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamConfiguration.java +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.configuration.ConfigurationBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * DataStreamConfiguration class - * - * Generated - */ -public class DataStreamConfiguration extends ConfigurationBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_NAME = "name"; - protected static final String KEY_TYPE = "type"; - protected static final String KEY_DIRECTION = "direction"; - protected static final String KEY_CHANNEL_NAME = "channelName"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kName; - protected KeyDescriptorEnum kType; - protected KeyDescriptorEnum kDirection; - protected KeyDescriptorString kChannelName; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected DataStreamConfiguration() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public DataStreamConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public DataStreamConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public DataStreamConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param type - * @param direction - * @param channelName - * @throws DataDictionaryException - */ - public DataStreamConfiguration(String name, DataStreamType type, DataStreamDirection direction, String channelName) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setType(type); - this.setDirection(direction); - this.setChannelName(channelName); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ConfigurationBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get name - * - * @return the name - */ - public String getName() - { - return (String) this.data.get(KEY_NAME); - } - - /** - * Get type - * - * @return the type - */ - public DataStreamType getType() - { - return (DataStreamType) this.data.get(KEY_TYPE); - } - - /** - * Get direction - * - * @return the direction - */ - public DataStreamDirection getDirection() - { - return (DataStreamDirection) this.data.get(KEY_DIRECTION); - } - - /** - * Get channel name - * - * @return the channel name - */ - public String getChannelName() - { - return (String) this.data.get(KEY_CHANNEL_NAME); - } - - /** - * Set name - * - * @param name - * @throws DataDictionaryException - */ - public void setName(String name) throws DataDictionaryException - { - this.setName((Object) name); - } - - /** - * Set type - * - * @param type - * @throws DataDictionaryException - */ - public void setType(DataStreamType type) throws DataDictionaryException - { - this.setType((Object) type); - } - - /** - * Set direction - * - * @param direction - * @throws DataDictionaryException - */ - public void setDirection(DataStreamDirection direction) throws DataDictionaryException - { - this.setDirection((Object) direction); - } - - /** - * Set channel name - * - * @param channelName - * @throws DataDictionaryException - */ - public void setChannelName(String channelName) throws DataDictionaryException - { - this.setChannelName((Object) channelName); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setName(Object name) throws DataDictionaryException - { - this.data.put(KEY_NAME, this.kName.convert(name)); - } - - protected void setType(Object type) throws DataDictionaryException - { - this.data.put(KEY_TYPE, this.kType.convert(type)); - } - - protected void setDirection(Object direction) throws DataDictionaryException - { - this.data.put(KEY_DIRECTION, this.kDirection.convert(direction)); - } - - protected void setChannelName(Object channelName) throws DataDictionaryException - { - this.data.put(KEY_CHANNEL_NAME, this.kChannelName.convert(channelName)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kName = new KeyDescriptorString(KEY_NAME, true, false); - this.keys.add(this.kName); - - this.kType = new KeyDescriptorEnum(KEY_TYPE, true, DataStreamType.class); - this.keys.add(this.kType); - - this.kDirection = new KeyDescriptorEnum(KEY_DIRECTION, true, DataStreamDirection.class); - this.keys.add(this.kDirection); - - this.kChannelName = new KeyDescriptorString(KEY_CHANNEL_NAME, false, false); - this.keys.add(this.kChannelName); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java b/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java deleted file mode 100644 index 66e83e7..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamDirection.java +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * DataStream direction - */ -public enum DataStreamDirection -{ - /** Input dataStream */ - INPUT, - /** Output dataStream */ - OUTPUT, - /** Inoutput dataStream */ - INOUTPUT; -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamException.java b/src/main/java/enedis/lab/io/datastreams/DataStreamException.java deleted file mode 100644 index 1410e0f..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamException.java +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * Specific exception for data stream - */ -public class DataStreamException extends Exception -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = 4079445021346677107L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Raise channel error exception - * - * @param channelName - * @param info - * @throws DataStreamException - */ - public static void raiseChannelError(String channelName, String info) throws DataStreamException - { - throw new DataStreamException("Error occurs on channel " + channelName + " (" + info + ")"); - } - - /** - * Raise unhandle type exception - * - * @param type - * @throws DataStreamException - */ - public static void raiseUnhandledType(String type) throws DataStreamException - { - throw new DataStreamException(type + " datastream is not handled"); - } - - /** - * Raise internal error exception - * - * @param info - * @throws DataStreamException - */ - public static void raiseInternalError(String info) throws DataStreamException - { - throw new DataStreamException(info); - } - - /** - * Raise invalid configuration exception - * - * @param configuration - * @param expected_configuration_name - * @throws DataStreamException - */ - public static void raiseInvalidConfiguration(DataStreamConfiguration configuration, String expected_configuration_name) throws DataStreamException - { - throw new DataStreamException("Configuration " + configuration.getClass().getSimpleName() + " is not valid (" + expected_configuration_name + " expected)"); - } - - /** - * Raise invalid configuration exceotion - * - * @param info - * @throws DataStreamException - */ - public static void raiseInvalidConfiguration(String info) throws DataStreamException - { - throw new DataStreamException(info); - } - - /** - * Raise operation denied excpetion - * - * @param operation - * @throws DataStreamException - */ - public static void raiseOperationDenied(String operation) throws DataStreamException - { - throw new DataStreamException("Operation \'" + operation + "\' is not allowed"); - } - - /** - * Raise unexpected error exception - * - * @param info - * @throws DataStreamException - */ - public static void raiseUnexpectedError(String info) throws DataStreamException - { - throw new DataStreamException("Unexpected error occurs : " + info); - } - - /** - * Raise already exists exception - * - * @param dataStream - * @throws DataStreamException - */ - public static void raiseAlreadyExists(String dataStream) throws DataStreamException - { - throw new DataStreamException("DataStream " + dataStream + " already exists"); - } - - /** - * Raise creation failed exception - * - * @param dataStream - * @param info - * @throws DataStreamException - */ - public static void raiseCreationFailed(String dataStream, String info) throws DataStreamException - { - throw new DataStreamException("Creation of dataStream " + dataStream + " creation failed : " + info); - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Super class constructor - * - * @param message - * @param cause - */ - public DataStreamException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Super class constructor - * - * @param message - */ - public DataStreamException(String message) - { - super(message); - } - - /** - * Super class constructor - * - * @param cause - */ - public DataStreamException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java b/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java deleted file mode 100644 index f9094ef..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamListener.java +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.Subscriber; - -/** - * Data stream listener - */ -public interface DataStreamListener extends Subscriber -{ - /** - * Function call when data has been received - * - * @param dataStreamName - * the stream name - * @param data - * the data received - */ - public void onDataReceived(String dataStreamName, DataDictionary data); - - /** - * Function call when data has been sent - * - * @param dataStreamName - * the stream name - * @param data - * the data sent - */ - public void onDataSent(String dataStreamName, DataDictionary data); - - /** - * Function call when data stream status changed - * - * @param dataStreamName - * the stream name - * @param status - * the new status - */ - public void onStatusChanged(String dataStreamName, DataStreamStatus status); - - /** - * On error detected - * - * @param dataStreamName - * the stream name - * @param errorCode - * the error code - * @param errorMessage - * the error message - * @param data - * the data associated with error - */ - public void onErrorDetected(String dataStreamName, int errorCode, String errorMessage, DataDictionary data); -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java b/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java deleted file mode 100644 index 282be74..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamStatus.java +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * DataStream status - */ -public enum DataStreamStatus -{ - /** Unknown, default status */ - UNKNOWN, - /** Closed */ - CLOSED, - /** Opened */ - OPENED, - /** Error */ - ERROR -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamType.java b/src/main/java/enedis/lab/io/datastreams/DataStreamType.java deleted file mode 100644 index 9d778d7..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamType.java +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -/** - * DataStream type - */ -public enum DataStreamType -{ - /** raw stream */ - RAW, - /** TIC stream */ - TIC, - /** IEC61850 stream */ - IEC61850, - /** Slave modbus stream */ - MODBUS_SLAVE, - /** Master modbus stream */ - MODBUS_MASTER, - /** LEM DC product */ - LEM_DC; -} diff --git a/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java b/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java deleted file mode 100644 index 6e0e865..0000000 --- a/src/main/java/enedis/lab/io/datastreams/DataStreamUser.java +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.datastreams; - -import enedis.lab.types.DataDictionary; -import enedis.lab.util.task.Notifier; - -/** - * DataStream interface - */ -public interface DataStreamUser extends Notifier -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * read data on the stream - * - * @return data read - * @throws DataStreamException - */ - public DataDictionary read() throws DataStreamException; - - /** - * write data on the stream - * - * @param data - * @throws DataStreamException - */ - public void write(DataDictionary data) throws DataStreamException; - - /** - * Get data stream name - * - * @return the stream name - */ - public String getName(); - - /** - * Get stream configuration - * - * @return stream configuration - */ - public DataStreamConfiguration getConfiguration(); - - /** - * Get stream direction - * - * @return stream direction - */ - public DataStreamDirection getDirection(); - - /** - * Get stream type - * - * @return stream type - */ - public DataStreamType getType(); - - /** - * Get stream status - * - * @return stream status - */ - public DataStreamStatus getStatus(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java b/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java deleted file mode 100644 index 3c30eee..0000000 --- a/src/main/java/enedis/lab/io/serialport/SerialPortDescriptor.java +++ /dev/null @@ -1,455 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.serialport; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorNumberMinMax; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * Class used to get the following serial port informations:
    - * - port unique identifier
    - * - port name used to open serial port
    - * - port description
    - * - USB device product identifier
    - * - USB device vendor identifier
    - * - USB device product name
    - * - USB device manufacturer
    - * - USB device serial number - */ -public class SerialPortDescriptor extends DataDictionaryBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_PORT_ID = "portId"; - protected static final String KEY_PORT_NAME = "portName"; - protected static final String KEY_DESCRIPTION = "description"; - protected static final String KEY_PRODUCT_ID = "productId"; - protected static final String KEY_VENDOR_ID = "vendorId"; - protected static final String KEY_PRODUCT_NAME = "productName"; - protected static final String KEY_MANUFACTURER = "manufacturer"; - protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - - private static final Number PRODUCT_ID_MIN = 0; - private static final Number PRODUCT_ID_MAX = 65535; - private static final Number VENDOR_ID_MIN = 0; - private static final Number VENDOR_ID_MAX = 65535; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kPortId; - protected KeyDescriptorString kPortName; - protected KeyDescriptorString kDescription; - protected KeyDescriptorNumberMinMax kProductId; - protected KeyDescriptorNumberMinMax kVendorId; - protected KeyDescriptorString kProductName; - protected KeyDescriptorString kManufacturer; - protected KeyDescriptorString kSerialNumber; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Empty constructor - */ - public SerialPortDescriptor() - { - super(); - this.loadKeyDescriptors(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public SerialPortDescriptor(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using data dictionary - * - * @param other - * @throws DataDictionaryException - */ - public SerialPortDescriptor(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param portId - * the port unique identifier - * @param portName - * the port name used to open serial port - * @param description - * the port description - * @param productId - * the USB device product identifier - * @param vendorId - * the USB device vendor identifier - * @param productName - * the USB device product name - * @param manufacturer - * the USB device manufacturer - * @param serialNumber - * the USB device serial number - * @throws DataDictionaryException - * if any parameters is invalid - */ - public SerialPortDescriptor(String portId, String portName, String description, Number productId, Number vendorId, String productName, String manufacturer, String serialNumber) - throws DataDictionaryException - { - this(); - - this.setPortId(portId); - this.setPortName(portName); - this.setDescription(description); - this.setProductId(productId); - this.setVendorId(vendorId); - this.setProductName(productName); - this.setManufacturer(manufacturer); - this.setSerialNumber(serialNumber); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get port id - * - * @return the port unique identifier - */ - public String getPortId() - { - return (String) this.data.get(KEY_PORT_ID); - } - - /** - * Get port name - * - * @return the port name used to open serial port - */ - public String getPortName() - { - return (String) this.data.get(KEY_PORT_NAME); - } - - /** - * Get description - * - * @return the port description - */ - public String getDescription() - { - return (String) this.data.get(KEY_DESCRIPTION); - } - - /** - * Get product id - * - * @return the USB device product identifier - */ - public Number getProductId() - { - return (Number) this.data.get(KEY_PRODUCT_ID); - } - - /** - * Get vendor id - * - * @return the USB device vendor identifier - */ - public Number getVendorId() - { - return (Number) this.data.get(KEY_VENDOR_ID); - } - - /** - * Get product name - * - * @return the USB device product name - */ - public String getProductName() - { - return (String) this.data.get(KEY_PRODUCT_NAME); - } - - /** - * Get manufacturer - * - * @return the USB device manufacturer - */ - public String getManufacturer() - { - return (String) this.data.get(KEY_MANUFACTURER); - } - - /** - * Get serial number - * - * @return the USB device serial number - */ - public String getSerialNumber() - { - return (String) this.data.get(KEY_SERIAL_NUMBER); - } - - /** - * Set port id - * - * @param portId - * the port unique identifier - * @throws DataDictionaryException - * if portId is empty - */ - public void setPortId(String portId) throws DataDictionaryException - { - this.setPortId((Object) portId); - } - - /** - * Set port name - * - * @param portName - * the port name used to open serial port - * @throws DataDictionaryException - * if portName is empty - */ - public void setPortName(String portName) throws DataDictionaryException - { - this.setPortName((Object) portName); - } - - /** - * Set description - * - * @param description - * the port description - * @throws DataDictionaryException - * if description is empty - */ - public void setDescription(String description) throws DataDictionaryException - { - this.setDescription((Object) description); - } - - /** - * Set product id - * - * @param productId - * the USB device product identifier - * @throws DataDictionaryException - * if productId is out of range [0-65535] - */ - public void setProductId(Number productId) throws DataDictionaryException - { - this.setProductId((Object) productId); - } - - /** - * Set vendor id - * - * @param vendorId - * the USB device vendor identifier - * @throws DataDictionaryException - * if vendorId is out of range [0-65535] - */ - public void setVendorId(Number vendorId) throws DataDictionaryException - { - this.setVendorId((Object) vendorId); - } - - /** - * Set product name - * - * @param productName - * the USB device product name - * @throws DataDictionaryException - * if productName is empty - */ - public void setProductName(String productName) throws DataDictionaryException - { - this.setProductName((Object) productName); - } - - /** - * Set manufacturer - * - * @param manufacturer - * the USB device manufacturer - * @throws DataDictionaryException - * if manufacturer is empty - */ - public void setManufacturer(String manufacturer) throws DataDictionaryException - { - this.setManufacturer((Object) manufacturer); - } - - /** - * Set serial number - * - * @param serialNumber - * the USB device serial number - * @throws DataDictionaryException - * if serialNumber is empty - */ - public void setSerialNumber(String serialNumber) throws DataDictionaryException - { - this.setSerialNumber((Object) serialNumber); - } - - /** - * Check if serial port is native (not USB) - * - * @return true if native port, false otherwise - */ - public boolean isNative() - { - return this.getPortName() != null && !this.getPortName().isEmpty() && this.getPortId() == null; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setPortId(Object portId) throws DataDictionaryException - { - this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); - } - - protected void setPortName(Object portName) throws DataDictionaryException - { - this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); - } - - protected void setDescription(Object description) throws DataDictionaryException - { - this.data.put(KEY_DESCRIPTION, this.kDescription.convert(description)); - } - - protected void setProductId(Object productId) throws DataDictionaryException - { - this.data.put(KEY_PRODUCT_ID, this.kProductId.convert(productId)); - } - - protected void setVendorId(Object vendorId) throws DataDictionaryException - { - this.data.put(KEY_VENDOR_ID, this.kVendorId.convert(vendorId)); - } - - protected void setProductName(Object productName) throws DataDictionaryException - { - this.data.put(KEY_PRODUCT_NAME, this.kProductName.convert(productName)); - } - - protected void setManufacturer(Object manufacturer) throws DataDictionaryException - { - this.data.put(KEY_MANUFACTURER, this.kManufacturer.convert(manufacturer)); - } - - protected void setSerialNumber(Object serialNumber) throws DataDictionaryException - { - this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); - this.keys.add(this.kPortId); - - this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); - this.keys.add(this.kPortName); - - this.kDescription = new KeyDescriptorString(KEY_DESCRIPTION, false, false); - this.keys.add(this.kDescription); - - this.kProductId = new KeyDescriptorNumberMinMax(KEY_PRODUCT_ID, false, PRODUCT_ID_MIN, PRODUCT_ID_MAX); - this.keys.add(this.kProductId); - - this.kVendorId = new KeyDescriptorNumberMinMax(KEY_VENDOR_ID, false, VENDOR_ID_MIN, VENDOR_ID_MAX); - this.keys.add(this.kVendorId); - - this.kProductName = new KeyDescriptorString(KEY_PRODUCT_NAME, false, false); - this.keys.add(this.kProductName); - - this.kManufacturer = new KeyDescriptorString(KEY_MANUFACTURER, false, false); - this.keys.add(this.kManufacturer); - - this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, false); - this.keys.add(this.kSerialNumber); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java deleted file mode 100644 index 4090a4b..0000000 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinder.java +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.serialport; - -import enedis.lab.io.PortFinder; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; - -/** - * Interface used to find all serial port descriptor - */ -public interface SerialPortFinder extends PortFinder -{ - /** - * Find serial port descriptor matching with port id - * - * @param portId - * the unique port identifier desired - * - * @return Serial port descriptor found, or null if nothing matches with portId - */ - public default SerialPortDescriptor findByPortId(String portId) - { - return this.findAll().stream().filter(k -> (k.getPortId() != null) ? k.getPortId().equals(portId) : portId == null).findFirst().orElse(null); - } - - /** - * Find serial port descriptor matching with port name - * - * @param portName - * the port name desired - * - * @return Serial port descriptor found, or null if nothing matches with portName - */ - public default SerialPortDescriptor findByPortName(String portName) - { - return this.findAll().stream().filter(k -> (k.getPortName() != null) ? k.getPortName().equals(portName) : portName == null).findFirst().orElse(null); - } - - /** - * Find native serial port (not USB) descriptor matching with port name - * - * @param portName - * the port name desired - * - * @return Serial port descriptor found, or null if nothing matches with portName - */ - public default SerialPortDescriptor findNative(String portName) - { - return this.findAll().stream().filter(k -> k.isNative() && k.getPortName().equals(portName)).findFirst().orElse(null); - } - - /** - * Find serial port descriptor matching with port id (having priority) or port name - * - * @param portId - * the unique port identifier desired - * @param portName - * the port name desired - * - * @return Serial port descriptor found, or null if nothing matches with portName - */ - public default SerialPortDescriptor findByPortIdOrPortName(String portId, String portName) - { - SerialPortDescriptor descriptor; - - if (portId != null) - { - descriptor = this.findByPortId(portId); - } - else if (portName != null) - { - descriptor = this.findByPortName(portName); - } - else - { - descriptor = null; - } - - return descriptor; - } - - /** - * Find serial port descriptor matching with pid/vid - * - * @param productId - * the USB device product identifier - * @param vendorId - * the USB device vendor identifier - * - * @return Serial port descriptor list found - */ - public default DataList findByProductIdAndVendorId(Number productId, Number vendorId) - { - DataList descriptorList = new DataArrayList(); - - for (SerialPortDescriptor descriptor : this.findAll()) - { - if (descriptor.getProductId() == null && productId != null) - { - continue; - } - if (descriptor.getVendorId() == null && vendorId != null) - { - continue; - } - if (descriptor.getProductId().equals(productId) && descriptor.getVendorId().equals(vendorId)) - { - descriptorList.add(descriptor); - } - } - - return descriptorList; - } -} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java deleted file mode 100644 index 9b9ba0f..0000000 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinderBase.java +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.serialport; - -import org.apache.commons.lang3.SystemUtils; - -import enedis.lab.types.DataList; - -/** - * Class used to find all serial port descriptor for any operating system - */ -public class SerialPortFinderBase implements SerialPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program writing the serial port descriptor list (JSON format) on the output stream - * - * @param args - * not used - */ - public static void main(String[] args) - { - DataList descriptors = getInstance().findAll(); - - System.out.println(descriptors.toString(2)); - } - - /** - * Get instance - * - * @return Unique instance - */ - public static SerialPortFinder getInstance() - { - if (instance == null) - { - if (SystemUtils.IS_OS_WINDOWS) - { - instance = SerialPortFinderForWindows.getInstance(); - } - else if (SystemUtils.IS_OS_LINUX) - { - instance = SerialPortFinderForLinux.getInstance(); - } - else - { - throw new RuntimeException("Operating system " + SystemUtils.OS_NAME + " is not handled by " + SerialPortFinderBase.class.getName()); - } - } - - return instance; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static SerialPortFinder instance; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private SerialPortFinderBase() - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortFinder - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataList findAll() - { - return instance.findAll(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java deleted file mode 100644 index f8c774a..0000000 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForLinux.java +++ /dev/null @@ -1,703 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.serialport; - -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.sun.jna.platform.linux.Udev; -import com.sun.jna.platform.linux.Udev.UdevContext; -import com.sun.jna.platform.linux.Udev.UdevDevice; -import com.sun.jna.platform.linux.Udev.UdevEnumerate; -import com.sun.jna.platform.linux.Udev.UdevListEntry; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * Class used to find all serial port descriptor for Linux - */ -public class SerialPortFinderForLinux implements SerialPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static String MOXA_IDENTIFIER = "ttyr"; - private static Pattern MOXA_PATTERN = Pattern.compile("(.*)" + MOXA_IDENTIFIER + "(\\d+)"); - private static String MOXA_FORMAT = MOXA_IDENTIFIER + "%02d"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get instance - * - * @return Unique instance - */ - public static SerialPortFinderForLinux getInstance() - { - if (instance == null) - { - instance = new SerialPortFinderForLinux(); - } - - return instance; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static SerialPortFinderForLinux instance; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private SerialPortFinderForLinux() - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortFinder - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataList findAll() - { - DataList serialPortDescriptorList = new DataArrayList(); - - serialPortDescriptorList = availablePortsByUdev(); - if (serialPortDescriptorList.isEmpty()) - { - serialPortDescriptorList = availablePortsBySysfs(); - } - if (serialPortDescriptorList.isEmpty()) - { - serialPortDescriptorList = availablePortsByFiltersOfDevices(); - } - - return serialPortDescriptorList; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static DataList availablePortsByUdev() - { - UdevContext udev = Udev.INSTANCE.udev_new(); - UdevEnumerate enumerate = Udev.INSTANCE.udev_enumerate_new(udev); - Udev.INSTANCE.udev_enumerate_add_match_subsystem(enumerate, "tty"); - Udev.INSTANCE.udev_enumerate_scan_devices(enumerate); - - DataList serialPortDescriptorList = new DataArrayList(); - - for (UdevListEntry dev_list_entry = Udev.INSTANCE.udev_enumerate_get_list_entry(enumerate); dev_list_entry != null; dev_list_entry = dev_list_entry.getNext()) - { - UdevDevice device = Udev.INSTANCE.udev_device_new_from_syspath(udev, Udev.INSTANCE.udev_list_entry_get_name(dev_list_entry)); - if (device == null) - { - break; - } - SerialPortDescriptor serialPortDescriptor; - - String location = deviceLocation(device); - String portName = deviceName(device); - - UdevDevice parentdev = Udev.INSTANCE.udev_device_get_parent(device); - - if (parentdev != null) - { - String driverName = deviceDriver(parentdev); - if (isSerial8250Driver(driverName) && !isValidSerial8250(location)) - { - Udev.INSTANCE.udev_device_unref(device); - continue; - } - String description = deviceDescription(device); - String portId = devicePortId(device); - String manufacturer = deviceManufacturer(device); - String serialNumber = deviceSerialNumber(device); - Number vendorIdentifier = deviceVendorIdentifier(device); - Number productIdentifier = deviceProductIdentifier(device); - String productName = deviceProductName(device); - try - { - serialPortDescriptor = new SerialPortDescriptor(portId, location, description, productIdentifier, vendorIdentifier, productName, manufacturer, serialNumber); - } - catch (DataDictionaryException e) - { - e.printStackTrace(); - Udev.INSTANCE.udev_device_unref(device); - continue; - } - } - else - { - if (!isRfcommDevice(portName) && !isVirtualNullModemDevice(portName) && !isGadgetDevice(portName) && !isMoxaDevice(portName)) - { - Udev.INSTANCE.udev_device_unref(device); - continue; - } - try - { - serialPortDescriptor = new SerialPortDescriptor(null, location, null, null, null, null, null, null); - } - catch (DataDictionaryException e) - { - e.printStackTrace(); - Udev.INSTANCE.udev_device_unref(device); - continue; - } - } - serialPortDescriptorList.add(serialPortDescriptor); - Udev.INSTANCE.udev_device_unref(device); - } - Udev.INSTANCE.udev_enumerate_unref(enumerate); - Udev.INSTANCE.udev_unref(udev); - - return serialPortDescriptorList; - } - - private static DataList availablePortsBySysfs() - { - DataList serialPortDescriptorList = new DataArrayList(); - File ttySysClassDir = new File("/sys/class/tty"); - - if (!(ttySysClassDir.exists() && ttySysClassDir.canRead())) - { - return serialPortDescriptorList; - } - FileFilter filter = new FileFilter() - { - @Override - public boolean accept(File pathname) - { - - return pathname.isDirectory() && !pathname.getName().equals(".") && !pathname.getName().equals(".."); - } - }; - File[] fileInfos = ttySysClassDir.listFiles(filter); - for (File fileInfo : fileInfos) - { - if (!Files.isSymbolicLink(fileInfo.toPath())) - { - continue; - } - File targetDir = fileInfo.getAbsoluteFile(); - SerialPortDescriptor serialPortDescriptor; - - String portName = deviceName(targetDir); - if (portName == null) - { - continue; - } - String driverName = deviceDriver(targetDir); - if (driverName == null) - { - if (!isRfcommDevice(portName) && !isVirtualNullModemDevice(portName) && !isGadgetDevice(portName) && !isMoxaDevice(portName)) - { - continue; - } - } - - String device = portNameToSystemLocation(portName); - if (isSerial8250Driver(driverName) && !isValidSerial8250(device)) - { - continue; - } - String description = null; - String manufacturer = null; - String serialNumber = null; - Number vendorIdentifier = null; - Number productIdentifier = null; - String productName = null; - do - { - if (description == null) - { - description = deviceDescription(targetDir); - } - if (manufacturer == null) - { - manufacturer = deviceManufacturer(targetDir); - } - if (serialNumber == null) - { - serialNumber = deviceSerialNumber(targetDir); - } - if (vendorIdentifier == null) - { - vendorIdentifier = deviceVendorIdentifier(targetDir); - } - if (productIdentifier == null) - { - productIdentifier = deviceProductIdentifier(targetDir); - } - if (productName == null) - { - productName = deviceProductName(targetDir); - } - if (description != null || manufacturer != null || serialNumber != null || vendorIdentifier != null || productIdentifier != null || productName != null) - { - break; - } - targetDir = targetDir.getParentFile(); - } while (targetDir != null); - try - { - serialPortDescriptor = new SerialPortDescriptor(null, portName, description, productIdentifier, vendorIdentifier, productName, manufacturer, serialNumber); - } - catch (DataDictionaryException e) - { - e.printStackTrace(); - continue; - } - serialPortDescriptorList.add(serialPortDescriptor); - } - - return serialPortDescriptorList; - } - - private static DataList availablePortsByFiltersOfDevices() - { - DataList serialPortDescriptorList = new DataArrayList(); - - List deviceFilePaths = filteredDeviceFilePaths(); - for (String deviceFilePath : deviceFilePaths) - { - SerialPortDescriptor serialPortDescriptor; - String portName = portNameFromSystemLocation(deviceFilePath); - try - { - serialPortDescriptor = new SerialPortDescriptor(null, portName, null, null, null, null, null, null); - } - catch (DataDictionaryException e) - { - e.printStackTrace(); - continue; - } - serialPortDescriptorList.add(serialPortDescriptor); - } - - return serialPortDescriptorList; - } - - private static List filteredDeviceFilePaths() - { - List deviceFileNameFilterList = new ArrayList(); - - // Linux - deviceFileNameFilterList.add("ttyS*"); // Standard UART 8250 and etc. - deviceFileNameFilterList.add("ttyO*"); // OMAP UART 8250 and etc. - deviceFileNameFilterList.add("ttyUSB*"); // Usb/serial converters PL2303 and etc. - deviceFileNameFilterList.add("ttyACM*"); // CDC_ACM converters (i.e. Mobile Phones). - deviceFileNameFilterList.add("ttyGS*"); // Gadget serial device (i.e. Mobile Phones with gadget serial driver). - deviceFileNameFilterList.add("ttyMI*"); // MOXA pci/serial converters. - deviceFileNameFilterList.add("ttymxc*"); // Motorola IMX serial ports (i.e. Freescale i.MX). - deviceFileNameFilterList.add("ttyAMA*"); // AMBA serial device for embedded platform on ARM (i.e. Raspberry Pi). - deviceFileNameFilterList.add("ttyTHS*"); // Serial device for embedded platform on ARM (i.e. Tegra Jetson TK1). - deviceFileNameFilterList.add("rfcomm*"); // Bluetooth serial device. - deviceFileNameFilterList.add("ircomm*"); // IrDA serial device. - deviceFileNameFilterList.add("tnt*"); // Virtual tty0tty serial device. - deviceFileNameFilterList.add(MOXA_IDENTIFIER + "*"); // MOXA ethernet device. - // FreeBSD - deviceFileNameFilterList.add("cu*"); - // QNX - deviceFileNameFilterList.add("ser*"); - - List result = new ArrayList(); - - File deviceDir = new File("/dev"); - if (deviceDir.exists()) - { - FileFilter deviceFileNameFilter = new FileFilter() - { - @Override - public boolean accept(File pathname) - { - Path path = pathname.toPath(); - - return deviceFileNameFilterList.contains(pathname.getName()) && pathname.isFile() && !Files.isSymbolicLink(path); - } - }; - File[] deviceFileInfos = deviceDir.listFiles(deviceFileNameFilter); - for (File deviceFileInfo : deviceFileInfos) - { - String deviceAbsoluteFilePath = deviceFileInfo.getAbsolutePath(); - // it is a quick workaround to skip the non-serial devices (FreeBSD) - if (deviceAbsoluteFilePath.endsWith(".init") || deviceAbsoluteFilePath.endsWith(".lock")) - { - continue; - } - if (!result.contains(deviceAbsoluteFilePath)) - { - result.add(deviceAbsoluteFilePath); - } - } - } - - return result; - } - - private static boolean isSerial8250Driver(String driverName) - { - return (driverName == "serial8250"); - } - - private static boolean isValidSerial8250(String systemLocation) - { - return false; - } - - private static boolean isRfcommDevice(String portName) - { - if (!portName.startsWith("rfcomm")) - { - return false; - } - try - { - String number = portName.substring(6); - int portNumber = Integer.parseInt(number); - if (portNumber < 0 || portNumber > 255) - { - return false; - } - } - catch (Exception e) - { - return false; - } - return true; - } - - // provided by the tty0tty driver - private static boolean isVirtualNullModemDevice(String portName) - { - return portName.startsWith("tnt"); - } - - // provided by the g_serial driver - private static boolean isGadgetDevice(String portName) - { - return portName.startsWith("ttyGS"); - } - - // provided by the MOXA driver - private static boolean isMoxaDevice(String portName) - { - return portName.startsWith(MOXA_IDENTIFIER); - } - - private static String devicePortId(UdevDevice dev) - { - return deviceProperty(dev, "ID_PATH"); - } - - private static String deviceDescription(UdevDevice dev) - { - String description = deviceProperty(dev, "ID_MODEL_FROM_DATABASE"); - - return (description != null) ? description.replace('_', ' ') : null; - } - - private static String deviceDescription(File targetDir) - { - return deviceProperty(new File(targetDir, "product").getAbsolutePath()); - } - - private static String deviceManufacturer(UdevDevice dev) - { - String manufacturer = deviceProperty(dev, "ID_VENDOR"); - - return (manufacturer != null) ? manufacturer.replace('_', ' ') : null; - } - - private static String deviceManufacturer(File targetDir) - { - return deviceProperty(new File(targetDir, "manufacturer").getAbsolutePath()); - } - - private static Number deviceProductIdentifier(UdevDevice dev) - { - Number identifierValue; - try - { - String indentifierText = deviceProperty(dev, "ID_MODEL_ID"); - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; - } - catch (Exception e) - { - identifierValue = null; - } - return identifierValue; - } - - private static String deviceProductName(UdevDevice dev) - { - return deviceProperty(dev, "ID_MODEL"); - } - - private static String deviceProductName(File targetDir) - { - return deviceProperty(new File(targetDir, "product").getAbsolutePath()); - } - - private static Number deviceProductIdentifier(File targetDir) - { - String indentifierText = deviceProperty(new File(targetDir, "idProduct").getAbsolutePath()); - - if (indentifierText == null) - { - indentifierText = deviceProperty(new File(targetDir, "device").getAbsolutePath()); - } - Number identifierValue; - try - { - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; - } - catch (Exception e) - { - identifierValue = null; - } - return identifierValue; - } - - private static Number deviceVendorIdentifier(File targetDir) - { - String indentifierText = deviceProperty(new File(targetDir, "idVendor").getAbsolutePath()); - - if (indentifierText == null) - { - indentifierText = deviceProperty(new File(targetDir, "vendor").getAbsolutePath()); - } - Number identifierValue; - try - { - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; - } - catch (Exception e) - { - identifierValue = null; - } - return identifierValue; - } - - private static String deviceSerialNumber(File targetDir) - { - return deviceProperty(new File(targetDir, "serial").getAbsolutePath()); - } - - private static Number deviceVendorIdentifier(UdevDevice dev) - { - Number identifierValue; - try - { - String indentifierText = deviceProperty(dev, "ID_VENDOR_ID"); - identifierValue = (indentifierText != null) ? Integer.parseInt(indentifierText, 16) : null; - } - catch (Exception e) - { - identifierValue = null; - } - return identifierValue; - } - - private static String deviceSerialNumber(UdevDevice dev) - { - return deviceProperty(dev, "ID_SERIAL_SHORT"); - } - - private static String deviceProperty(UdevDevice dev, String name) - { - return Udev.INSTANCE.udev_device_get_property_value(dev, name); - } - - private static String deviceProperty(String targetFilePath) - { - FileInputStream f; - - try - { - f = new FileInputStream(targetFilePath); - } - catch (FileNotFoundException e) - { - e.printStackTrace(); - return null; - } - String text; - try - { - int available = f.available(); - byte[] buffer = new byte[available]; - f.read(buffer); - text = new String(buffer); - } - catch (IOException e) - { - e.printStackTrace(); - return null; - } - finally - { - try - { - f.close(); - } - catch (IOException e) - { - e.printStackTrace(); - } - } - - return text.trim(); - } - - private static String deviceDriver(UdevDevice dev) - { - return null; - } - - private static String deviceDriver(File targetDir) - { - File deviceDir = new File(targetDir, "device"); - - return ueventProperty(deviceDir, "DRIVER="); - } - - private static String deviceName(UdevDevice dev) - { - return Udev.INSTANCE.udev_device_get_sysname(dev); - } - - static String deviceName(File targetDir) - { - return ueventProperty(targetDir, "DEVNAME="); - } - - private static String deviceLocation(UdevDevice dev) - { - String location = Udev.INSTANCE.udev_device_get_devnode(dev); - // Patch for MOXA device - // Udev prints location as /dev/ttyrN but only /dev/ttyr0N exists (with N belongs to [1-4]) - // So we are renaming location from /dev/ttyrN to /dev/ttyr0N - Matcher matcher = MOXA_PATTERN.matcher(location); - - if (matcher.matches()) - { - String prefix = ""; - String indexValue; - if (matcher.groupCount() == 2) - { - prefix = matcher.group(1); - indexValue = matcher.group(2); - } - else - { - indexValue = matcher.group(2); - } - int index = Integer.parseUnsignedInt(indexValue); - location = String.format("%s" + MOXA_FORMAT, prefix, index); - } - - return location; - } - - private static String portNameToSystemLocation(String source) - { - return (source.startsWith("/") || source.startsWith("./") || source.startsWith("../")) ? source : (("/dev/") + source); - } - - private static String portNameFromSystemLocation(String source) - { - return source.startsWith("/dev/") ? source.substring(5) : source; - } - - @SuppressWarnings("resource") - private static String ueventProperty(File targetDir, String pattern) - { - FileInputStream f; - - try - { - f = new FileInputStream(new File(targetDir, "uevent")); - } - catch (FileNotFoundException e) - { - e.printStackTrace(); - return null; - } - String content; - try - { - int available = f.available(); - byte[] buffer = new byte[available]; - f.read(buffer); - content = new String(buffer); - } - catch (IOException e) - { - e.printStackTrace(); - return null; - } - int firstbound = content.indexOf(pattern); - if (firstbound == -1) - { - return null; - } - int lastbound = content.indexOf('\n', firstbound); - - return content.substring(firstbound, lastbound); - } -} diff --git a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java b/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java deleted file mode 100644 index 6f66a58..0000000 --- a/src/main/java/enedis/lab/io/serialport/SerialPortFinderForWindows.java +++ /dev/null @@ -1,455 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.serialport; - -import static com.sun.jna.platform.win32.SetupApi.DICS_FLAG_GLOBAL; -import static com.sun.jna.platform.win32.SetupApi.DIGCF_DEVICEINTERFACE; -import static com.sun.jna.platform.win32.SetupApi.DIGCF_PRESENT; -import static com.sun.jna.platform.win32.SetupApi.DIREG_DEV; -import static com.sun.jna.platform.win32.SetupApi.GUID_DEVINTERFACE_COMPORT; -import static com.sun.jna.platform.win32.SetupApi.SPDRP_DEVICEDESC; -import static com.sun.jna.platform.win32.WinBase.INVALID_HANDLE_VALUE; -import static com.sun.jna.platform.win32.WinDef.MAX_PATH; -import static com.sun.jna.platform.win32.WinError.ERROR_SUCCESS; -import static com.sun.jna.platform.win32.WinNT.KEY_QUERY_VALUE; -import static com.sun.jna.platform.win32.WinNT.KEY_READ; -import static com.sun.jna.platform.win32.WinNT.REG_DWORD; -import static com.sun.jna.platform.win32.WinNT.REG_SZ; -import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE; - -import java.util.ArrayList; -import java.util.List; - -import com.sun.jna.Memory; -import com.sun.jna.Pointer; -import com.sun.jna.platform.win32.Advapi32; -import com.sun.jna.platform.win32.Advapi32Util; -import com.sun.jna.platform.win32.Cfgmgr32; -import com.sun.jna.platform.win32.Cfgmgr32Util; -import com.sun.jna.platform.win32.Guid.GUID; -import com.sun.jna.platform.win32.SetupApi; -import com.sun.jna.platform.win32.SetupApi.SP_DEVINFO_DATA; -import com.sun.jna.platform.win32.Win32Exception; -import com.sun.jna.platform.win32.WinNT.HANDLE; -import com.sun.jna.platform.win32.WinReg.HKEY; -import com.sun.jna.ptr.IntByReference; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * Class used to find all serial port descriptor for Windows - */ -public class SerialPortFinderForWindows implements SerialPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final int SPDRP_MFG = 0x0000000B; - private static final int SPDRP_ADDRESS = 0x0000001C; - private static final int PROPERTY_MAX_SIZE = 1024; - - private static final GUID GUID_DEVCLASS_PORTS = new GUID("{4d36e978-e325-11ce-bfc1-08002be10318}"); - private static final GUID GUID_DEVCLASS_MODEM = new GUID("{4d36e96d-e325-11ce-bfc1-08002be10318}"); - private static final GUID GUID_DEVINTERFACE_MODEM = new GUID("{2c7089aa-2e0e-11d1-b114-00c04fc2aae4}"); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get instance - * - * @return Unique instance - */ - public static SerialPortFinderForWindows getInstance() - { - if(instance == null) - { - instance = new SerialPortFinderForWindows(); - } - - return instance; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static SerialPortFinderForWindows instance; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private SerialPortFinderForWindows() - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortFinder - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataList findAll() - { - final class SetupToken - { - public GUID guid; - public int flags; - - public SetupToken(GUID guid, int flags) - { - super(); - this.guid = guid; - this.flags = flags; - } - } - - SetupToken[] setupTokens = new SetupToken[] { new SetupToken(GUID_DEVCLASS_PORTS, DIGCF_PRESENT), new SetupToken(GUID_DEVCLASS_MODEM, DIGCF_PRESENT), - new SetupToken(GUID_DEVINTERFACE_COMPORT, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE), new SetupToken(GUID_DEVINTERFACE_MODEM, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE) }; - - DataList serialPortDescriptorList = new DataArrayList(); - - for (int i = 0; i < setupTokens.length; ++i) - { - HANDLE deviceInfoSet = SetupApi.INSTANCE.SetupDiGetClassDevs(setupTokens[i].guid, null, null, setupTokens[i].flags); - if (deviceInfoSet == INVALID_HANDLE_VALUE) - { - return serialPortDescriptorList; - } - SP_DEVINFO_DATA deviceInfoData = new SP_DEVINFO_DATA(); - int index = 0; - while (SetupApi.INSTANCE.SetupDiEnumDeviceInfo(deviceInfoSet, index++, deviceInfoData)) - { - SerialPortDescriptor serialPortDescriptor; - - String portName = devicePortName(deviceInfoSet, deviceInfoData); - if (portName == null || portName.isEmpty() || portName.contains("LPT")) - { - continue; - } - if (anyOfPorts(serialPortDescriptorList, portName)) - { - continue; - } - String description = deviceDescription(deviceInfoSet, deviceInfoData); - String address = deviceAddress(deviceInfoSet, deviceInfoData); - String manufacturer = deviceManufacturer(deviceInfoSet, deviceInfoData); - String instanceIdentifier = deviceInstanceIdentifier(deviceInfoData.DevInst); - String serialNumber = deviceSerialNumber(instanceIdentifier, deviceInfoData.DevInst); - Number vendorIdentifier = deviceVendorIdentifier(instanceIdentifier); - Number productIdentifier = deviceProductIdentifier(instanceIdentifier); - try - { - serialPortDescriptor = new SerialPortDescriptor(address, portName, description, productIdentifier, vendorIdentifier, null, manufacturer, serialNumber); - } - catch (DataDictionaryException e) - { - e.printStackTrace(System.err); - continue; - } - serialPortDescriptorList.add(serialPortDescriptor); - - } - SetupApi.INSTANCE.SetupDiDestroyDeviceInfoList(deviceInfoSet); - } - - List portNames = portNamesFromHardwareDeviceMap(); - for (String portName : portNames) - { - if (!anyOfPorts(serialPortDescriptorList, portName)) - { - try - { - SerialPortDescriptor serialPortDescriptor = new SerialPortDescriptor(null, portName, null, null, null, null, null, null); - serialPortDescriptorList.add(serialPortDescriptor); - } - catch (DataDictionaryException e) - { - e.printStackTrace(System.err); - } - } - } - - return serialPortDescriptorList; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static boolean anyOfPorts(DataList descriptors, String portName) - { - if (descriptors == null) - { - return false; - } - for (SerialPortDescriptor descriptor : descriptors) - { - if (descriptor.getPortName() == null) - { - if (portName != null) - { - continue; - } - else - { - return true; - } - } - if (descriptor.getPortName().equals(portName)) - { - return true; - } - } - - return false; - } - - private static String devicePortName(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) - { - HKEY key = SetupApi.INSTANCE.SetupDiOpenDevRegKey(deviceInfoSet, deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); - - if (key == INVALID_HANDLE_VALUE) - { - return null; - } - String[] keyTokens = { "PortName", "PortNumber" }; - String portName = null; - for (int i = 0; i < keyTokens.length && portName == null; i++) - { - portName = Advapi32Util.registryGetStringValue(key, keyTokens[i]); - } - - return portName; - } - - private static String deviceDescription(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) - { - return (String) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_DEVICEDESC); - } - - private static String deviceManufacturer(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) - { - return (String) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_MFG); - } - - private static String deviceAddress(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData) - { - Integer address = (Integer) deviceRegistryProperty(deviceInfoSet, deviceInfoData, SPDRP_ADDRESS); - return (address != null) ? address.toString() : null; - } - - private static Number deviceVendorIdentifier(String instanceIdentifier) - { - final int vendorIdentifierSize = 4; - - Number result = parseDeviceIdentifier(instanceIdentifier, "VID_", vendorIdentifierSize); - if (result == null) - { - result = parseDeviceIdentifier(instanceIdentifier, "VEN_", vendorIdentifierSize); - } - - return result; - } - - private static Number deviceProductIdentifier(String instanceIdentifier) - { - final int productIdentifierSize = 4; - - Number result = parseDeviceIdentifier(instanceIdentifier, "PID_", productIdentifierSize); - if (result == null) - { - result = parseDeviceIdentifier(instanceIdentifier, "DEV_", productIdentifierSize); - } - - return result; - } - - private static String deviceInstanceIdentifier(int deviceInstanceNumber) - { - return Cfgmgr32Util.CM_Get_Device_ID(deviceInstanceNumber); - } - - private static String deviceSerialNumber(String instanceIdentifier, int deviceInstanceNumber) - { - for (;;) - { - String serialNumber = parseDeviceSerialNumber(instanceIdentifier); - if (serialNumber != null) - { - return serialNumber; - } - deviceInstanceNumber = parentDeviceInstanceNumber(deviceInstanceNumber); - if (deviceInstanceNumber == 0) - { - break; - } - instanceIdentifier = deviceInstanceIdentifier(deviceInstanceNumber); - if (instanceIdentifier.isEmpty()) - { - break; - } - } - - return null; - } - - private static Object deviceRegistryProperty(HANDLE deviceInfoSet, SP_DEVINFO_DATA deviceInfoData, int property) - { - Object propertyValue = null; - IntByReference dataType = new IntByReference(0); - Pointer outputBuffer = new Memory(PROPERTY_MAX_SIZE); - IntByReference bytesRequired = new IntByReference(PROPERTY_MAX_SIZE); - - if (SetupApi.INSTANCE.SetupDiGetDeviceRegistryProperty(deviceInfoSet, deviceInfoData, property, dataType, outputBuffer, bytesRequired.getValue(), bytesRequired)) - { - switch (dataType.getValue()) - { - case REG_DWORD: - propertyValue = outputBuffer.getInt(0); - break; - case REG_SZ: - propertyValue = outputBuffer.getWideString(0); - break; - } - } - - return propertyValue; - } - - private static int parentDeviceInstanceNumber(int childDeviceInstanceNumber) - { - IntByReference parentInstanceNumber = new IntByReference(0); - if (Cfgmgr32.INSTANCE.CM_Get_Parent(parentInstanceNumber, childDeviceInstanceNumber, 0) != Cfgmgr32.CR_SUCCESS) - { - return 0; - } - return parentInstanceNumber.getValue(); - } - - private static String parseDeviceSerialNumber(String instanceIdentifier) - { - int firstbound = instanceIdentifier.lastIndexOf('\\'); - int lastbound = instanceIdentifier.indexOf('_', firstbound); - if (instanceIdentifier.startsWith("USB\\")) - { - if (lastbound != instanceIdentifier.length() - 3) - lastbound = instanceIdentifier.length(); - int ampersand = instanceIdentifier.indexOf('&', firstbound); - if (ampersand != -1 && ampersand < lastbound) - return null; - } - else if (instanceIdentifier.startsWith("FTDIBUS\\")) - { - firstbound = instanceIdentifier.lastIndexOf('+'); - lastbound = instanceIdentifier.indexOf('\\', firstbound); - if (lastbound == -1) - return null; - } - else - { - return null; - } - - return instanceIdentifier.substring(firstbound + 1, lastbound); - } - - private static Number parseDeviceIdentifier(String instanceIdentifier, String identifierPrefix, int identifierSize) - { - int index = instanceIdentifier.indexOf(identifierPrefix); - - if (index == -1) - { - return null; - } - String indentifierText = instanceIdentifier.substring(index + identifierPrefix.length(), index + identifierPrefix.length() + identifierSize); - Number identifierValue; - try - { - identifierValue = Integer.parseInt(indentifierText, 16); - } - catch (Exception e) - { - identifierValue = null; - } - - return identifierValue; - } - - private static List portNamesFromHardwareDeviceMap() - { - List result = new ArrayList(); - HKEY hKey = null; - try - { - hKey = Advapi32Util.registryGetKey(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM", KEY_QUERY_VALUE).getValue(); - } - catch(Win32Exception exception) - { - return result; - } - int index = 0; - for (;;) - { - // This is a maximum length of value name, see: - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724872%28v=vs.85%29.aspx - IntByReference requiredValueNameChars = new IntByReference(16383); - char[] outputValueName = new char[requiredValueNameChars.getValue()]; - Pointer outputBuffer = new Memory(MAX_PATH); - IntByReference bytesRequired = new IntByReference(MAX_PATH); - int ret = Advapi32.INSTANCE.RegEnumValue(hKey, index, outputValueName, requiredValueNameChars, null, null, outputBuffer, bytesRequired); - if (ret == ERROR_SUCCESS) - { - result.add(outputBuffer.getWideString(0)); - ++index; - } - else - { - break; - } - } - Advapi32.INSTANCE.RegCloseKey(hKey); - - return result; - } -} diff --git a/src/main/java/enedis/lab/io/tic/TICModemType.java b/src/main/java/enedis/lab/io/tic/TICModemType.java deleted file mode 100644 index b1f3688..0000000 --- a/src/main/java/enedis/lab/io/tic/TICModemType.java +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -/** - * TIC Modem type - */ -public enum TICModemType -{ - /** Modem michaud */ - MICHAUD(0x6001, 0x0403), - /** Télé info */ - TELEINFO(0x6015, 0x0403); - - private int productId; - private int vendorId; - - TICModemType(int productId, int vendorId) - { - this.productId = productId; - this.vendorId = vendorId; - } - - /** - * Get product id - * - * @return product id - */ - public int getProductId() - { - return this.productId; - } - - /** - * Get vendor id - * - * @return vendor id - */ - public int getVendorId() - { - return this.vendorId; - } -} diff --git a/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java b/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java deleted file mode 100644 index 675a073..0000000 --- a/src/main/java/enedis/lab/io/tic/TICPortDescriptor.java +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.io.serialport.SerialPortDescriptor; -import enedis.lab.io.usb.USBPortDescriptor; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; - -/** - * TICPortDescriptor class - * - * Generated - */ -public class TICPortDescriptor extends SerialPortDescriptor -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_MODEM_TYPE = "modemType"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kModemType; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected TICPortDescriptor() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICPortDescriptor(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICPortDescriptor(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param portId - * @param portName - * @param description - * @param productName - * @param manufacturer - * @param serialNumber - * @param modemType - * @throws DataDictionaryException - */ - public TICPortDescriptor(String portId, String portName, String description, String productName, String manufacturer, String serialNumber, TICModemType modemType) - throws DataDictionaryException - { - this(); - - this.setPortId(portId); - this.setPortName(portName); - this.setDescription(description); - this.setProductName(productName); - this.setManufacturer(manufacturer); - this.setSerialNumber(serialNumber); - this.setModemType(modemType); - - this.checkAndUpdate(); - } - - /** - * Constructor setting parameters to specific values - * - * @param serialPortDescriptor - * @param modemType - * @throws DataDictionaryException - */ - public TICPortDescriptor(SerialPortDescriptor serialPortDescriptor, TICModemType modemType) throws DataDictionaryException - { - this(); - - this.checkProductId(serialPortDescriptor.getProductId(), modemType); - this.checkVendorId(serialPortDescriptor.getVendorId(), modemType); - - this.setPortId(serialPortDescriptor.getPortId()); - this.setPortName(serialPortDescriptor.getPortName()); - this.setDescription(serialPortDescriptor.getDescription()); - this.setProductId(serialPortDescriptor.getProductId()); - this.setVendorId(serialPortDescriptor.getVendorId()); - this.setProductName(serialPortDescriptor.getProductName()); - this.setManufacturer(serialPortDescriptor.getManufacturer()); - this.setSerialNumber(serialPortDescriptor.getSerialNumber()); - this.setModemType(modemType); - - this.checkAndUpdate(); - } - - /** - * Constructor setting parameters to specific values - * - * @param usbPortDescriptor - * @param modemType - * @throws DataDictionaryException - */ - public TICPortDescriptor(USBPortDescriptor usbPortDescriptor, TICModemType modemType) throws DataDictionaryException - { - this(); - - this.checkProductId(usbPortDescriptor.getIdProduct(), modemType); - this.checkVendorId(usbPortDescriptor.getIdVendor(), modemType); - - this.setProductId(usbPortDescriptor.getIdProduct()); - this.setVendorId(usbPortDescriptor.getIdVendor()); - this.setProductName(usbPortDescriptor.getProduct()); - this.setManufacturer(usbPortDescriptor.getManufacturer()); - this.setSerialNumber(usbPortDescriptor.getSerialNumber()); - this.setModemType(modemType); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (this.exists(KEY_MODEM_TYPE)) - { - if (this.getModemType() != null) - { - this.setProductId(this.getModemType().getProductId()); - this.setVendorId(this.getModemType().getVendorId()); - } - else - { - this.setProductId(null); - this.setVendorId(null); - } - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get modem type - * - * @return the modem type - */ - public TICModemType getModemType() - { - return (TICModemType) this.data.get(KEY_MODEM_TYPE); - } - - /** - * Set modem type - * - * @param modemType - * @throws DataDictionaryException - */ - public void setModemType(TICModemType modemType) throws DataDictionaryException - { - this.setModemType((Object) modemType); - if (modemType != null) - { - this.setProductId(modemType.getProductId()); - this.setVendorId(modemType.getVendorId()); - } - else - { - this.setProductId(null); - this.setVendorId(null); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setModemType(Object modemType) throws DataDictionaryException - { - if (modemType == null) - { - this.data.put(KEY_MODEM_TYPE, null); - } - else - { - this.data.put(KEY_MODEM_TYPE, this.kModemType.convert(modemType)); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void checkProductId(Number productId, TICModemType modemType) throws DataDictionaryException - { - if (modemType != null && productId != null && productId.intValue() != modemType.getProductId()) - { - throw new DataDictionaryException("TIC modem productId is inconsistent with the given one"); - } - } - - private void checkVendorId(Number vendorId, TICModemType modemType) throws DataDictionaryException - { - if (modemType != null && vendorId != null && vendorId.intValue() != modemType.getVendorId()) - { - throw new DataDictionaryException("TIC modem vendorId is inconsistent with the given one"); - } - } - - private void loadKeyDescriptors() - { - try - { - this.kModemType = new KeyDescriptorEnum(KEY_MODEM_TYPE, false, TICModemType.class); - this.keys.add(this.kModemType); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/tic/TICPortFinder.java b/src/main/java/enedis/lab/io/tic/TICPortFinder.java deleted file mode 100644 index 370121b..0000000 --- a/src/main/java/enedis/lab/io/tic/TICPortFinder.java +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import enedis.lab.io.PortFinder; - -/** - * Interface used to find all TIC port descriptor - */ -public interface TICPortFinder extends PortFinder -{ - /** - * Find TIC port descriptor matching with port id - * - * @param portId - * the unique port identifier desired - * - * @return TIC port descriptor found, or null if nothing matches with portId - */ - public default TICPortDescriptor findByPortId(String portId) - { - return this.findAll().stream().filter(p -> (p.getPortId() != null) ? p.getPortId().equals(portId) : portId == null).findFirst().orElse(null); - } - - /** - * Find TIC port descriptor matching with port name - * - * @param portName - * the port name desired - * - * @return TIC port descriptor found, or null if nothing matches with portName - */ - public default TICPortDescriptor findByPortName(String portName) - { - return this.findAll().stream().filter(p -> (p.getPortName() != null) ? p.getPortName().equals(portName) : portName == null).findFirst().orElse(null); - } - - /** - * Find native TIC port (not USB) descriptor matching with port name - * - * @param portName - * the port name desired - * - * @return TIC port descriptor found, or null if nothing matches with portName - */ - public TICPortDescriptor findNative(String portName); - - /** - * Find TIC port descriptor matching with port id or port name - * - * @param portId - * the unique port identifier desired - * @param portName - * the port name desired - * - * @return TIC port descriptor found, or null if nothing matches with portName - */ - public default TICPortDescriptor findByPortIdOrPortName(String portId, String portName) - { - TICPortDescriptor descriptor = this.findByPortId(portId); - - if (descriptor == null) - { - descriptor = this.findByPortName(portId); - } - - return descriptor; - } -} diff --git a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java b/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java deleted file mode 100644 index d4777c1..0000000 --- a/src/main/java/enedis/lab/io/tic/TICPortFinderBase.java +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import java.util.List; - -import enedis.lab.io.serialport.SerialPortDescriptor; -import enedis.lab.io.serialport.SerialPortFinder; -import enedis.lab.io.serialport.SerialPortFinderBase; -import enedis.lab.io.usb.USBPortDescriptor; -import enedis.lab.io.usb.USBPortFinder; -import enedis.lab.io.usb.USBPortFinderBase; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * Class used to find all TIC port descriptor - */ -public class TICPortFinderBase implements TICPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program writing the TIC port descriptor list (JSON format) on the output stream - * - * @param args - * not used - */ - public static void main(String[] args) - { - DataList descriptors = getInstance().findAll(); - - System.out.println(descriptors.toString(2)); - } - - /** - * Get instance - * - * @return Unique instance - */ - public static TICPortFinderBase getInstance() - { - if (instance == null) - { - instance = new TICPortFinderBase(); - } - - return instance; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static TICPortFinderBase instance; - - private SerialPortFinder serialPortFinder; - private USBPortFinder usbPortFinder; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICPortFinderBase() - { - this(SerialPortFinderBase.getInstance(), USBPortFinderBase.getInstance()); - } - - /** - * Constructor with finder parameters - * - * @param serialPortFinder - * the serial port finder interface - * @param usbPortFinder - * the USB port finder interface - */ - public TICPortFinderBase(SerialPortFinder serialPortFinder, USBPortFinder usbPortFinder) - { - if (serialPortFinder == null) - { - throw new IllegalArgumentException("Cannot set null serial port finder"); - } - this.serialPortFinder = serialPortFinder; - if (usbPortFinder == null) - { - throw new IllegalArgumentException("Cannot set null USB port finder"); - } - this.usbPortFinder = usbPortFinder; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICPortFinder - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataList findAll() - { - DataList ticSerialPort = new DataArrayList(); - - for (TICModemType modemType : TICModemType.values()) - { - List tmpSerialPort = this.serialPortFinder.findByProductIdAndVendorId(modemType.getProductId(), modemType.getVendorId()); - - if (tmpSerialPort.isEmpty()) - { - List tmpUSBPort = this.usbPortFinder.findByProductIdAndVendorId(modemType.getProductId(), modemType.getVendorId()); - - for (USBPortDescriptor upd : tmpUSBPort) - { - try - { - TICPortDescriptor tic = new TICPortDescriptor(upd, modemType); - ticSerialPort.add(tic); - } - catch (DataDictionaryException e) - { - } - } - } - else - { - for (SerialPortDescriptor spd : tmpSerialPort) - { - try - { - TICPortDescriptor tic = new TICPortDescriptor(spd, modemType); - ticSerialPort.add(tic); - } - catch (DataDictionaryException e) - { - } - } - } - } - - return ticSerialPort; - } - - @Override - public TICPortDescriptor findNative(String portName) - { - TICPortDescriptor ticPortDescriptor = null; - SerialPortDescriptor serialPortDescriptor = this.serialPortFinder.findNative(portName); - - if (serialPortDescriptor != null) - { - try - { - ticPortDescriptor = new TICPortDescriptor(serialPortDescriptor, null); - } - catch (DataDictionaryException e) - { - } - } - - return ticPortDescriptor; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java b/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java deleted file mode 100644 index d16edc8..0000000 --- a/src/main/java/enedis/lab/io/tic/TICPortPlugNotifier.java +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import org.json.JSONObject; - -import enedis.lab.io.PlugSubscriber; -import enedis.lab.io.PortPlugNotifier; - -/** - * Class used to notify when a TIC port has been plugged or unplugged - */ -public class TICPortPlugNotifier extends PortPlugNotifier -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final int DEFAULT_JSON_INDENTATION = 2; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program writing on the output stream when an TIC port has been plugged or unplugged - * - * @param args - * not used - */ - public static void main(String[] args) - { - /* 1. Create notification service */ - TICPortPlugNotifier notifier = new TICPortPlugNotifier(); - /* 2. Create subscriber to print when an TIC port has been plugged or unplugged */ - PlugSubscriber subscriber = new PlugSubscriber() - { - @Override - public void onPlugged(TICPortDescriptor descriptor) - { - JSONObject jsonObject = new JSONObject(); - jsonObject.put(notifier.getClass().getSimpleName() + ".onPlugged", descriptor.toJSON()); - System.out.println(jsonObject.toString(DEFAULT_JSON_INDENTATION) + "\n"); - - } - @Override - public void onUnplugged(TICPortDescriptor descriptor) - { - JSONObject jsonObject = new JSONObject(); - jsonObject.put(notifier.getClass().getSimpleName() + ".onUnplugged", descriptor.toJSON()); - System.out.println(jsonObject.toString(DEFAULT_JSON_INDENTATION) + "\n"); - } - }; - /* 3. Run program printing when an TIC port has been plugged or unplugged until CTRL+C is pressed */ - PortPlugNotifier.main(notifier, subscriber); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor with default parameter - * - * @see #DEFAULT_PERIOD - * @see TICPortFinderBase - */ - public TICPortPlugNotifier() - { - this(DEFAULT_PERIOD,TICPortFinderBase.getInstance()); - } - - /** - * Constructor with all parameters - * - * @param period the period (in milliseconds) used to look for plugged or unplugged TIC port - * @param finder the TIC port finder interface used to find all TIC port descriptors - */ - public TICPortPlugNotifier(long period,TICPortFinder finder) - { - super(period,finder); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Runnable - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java b/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java deleted file mode 100644 index b3f71fb..0000000 --- a/src/main/java/enedis/lab/io/usb/USBPortDescriptor.java +++ /dev/null @@ -1,711 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.usb; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * USBPortDescriptor class - * - * Generated - */ -public class USBPortDescriptor extends DataDictionaryBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_BCD_DEVICE = "bcdDevice"; - protected static final String KEY_BCD_USB = "bcdUSB"; - protected static final String KEY_B_DESCRIPTOR_TYPE = "bDescriptorType"; - protected static final String KEY_B_DEVICE_CLASS = "bDeviceClass"; - protected static final String KEY_B_DEVICE_PROTOCOL = "bDeviceProtocol"; - protected static final String KEY_B_DEVICE_SUB_CLASS = "bDeviceSubClass"; - protected static final String KEY_B_LENGTH = "bLength"; - protected static final String KEY_B_MAX_PACKET_SIZE0 = "bMaxPacketSize0"; - protected static final String KEY_B_NUM_CONFIGURATIONS = "bNumConfigurations"; - protected static final String KEY_ID_PRODUCT = "idProduct"; - protected static final String KEY_ID_VENDOR = "idVendor"; - protected static final String KEY_I_MANUFACTURER = "iManufacturer"; - protected static final String KEY_I_PRODUCT = "iProduct"; - protected static final String KEY_I_SERIAL_NUMBER = "iSerialNumber"; - protected static final String KEY_MANUFACTURER = "manufacturer"; - protected static final String KEY_PRODUCT = "product"; - protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorNumber kBcdDevice; - protected KeyDescriptorNumber kBcdUSB; - protected KeyDescriptorNumber kBDescriptorType; - protected KeyDescriptorNumber kBDeviceClass; - protected KeyDescriptorNumber kBDeviceProtocol; - protected KeyDescriptorNumber kBDeviceSubClass; - protected KeyDescriptorNumber kBLength; - protected KeyDescriptorNumber kBMaxPacketSize0; - protected KeyDescriptorNumber kBNumConfigurations; - protected KeyDescriptorNumber kIdProduct; - protected KeyDescriptorNumber kIdVendor; - protected KeyDescriptorNumber kIManufacturer; - protected KeyDescriptorNumber kIProduct; - protected KeyDescriptorNumber kISerialNumber; - protected KeyDescriptorString kManufacturer; - protected KeyDescriptorString kProduct; - protected KeyDescriptorString kSerialNumber; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected USBPortDescriptor() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public USBPortDescriptor(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public USBPortDescriptor(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param bcdDevice - * @param bcdUSB - * @param bDescriptorType - * @param bDeviceClass - * @param bDeviceProtocol - * @param bDeviceSubClass - * @param bLength - * @param bMaxPacketSize0 - * @param bNumConfigurations - * @param idProduct - * @param idVendor - * @param iManufacturer - * @param iProduct - * @param iSerialNumber - * @param manufacturer - * @param product - * @param serialNumber - * @throws DataDictionaryException - */ - public USBPortDescriptor(Number bcdDevice, Number bcdUSB, Number bDescriptorType, Number bDeviceClass, Number bDeviceProtocol, Number bDeviceSubClass, Number bLength, - Number bMaxPacketSize0, Number bNumConfigurations, Number idProduct, Number idVendor, Number iManufacturer, Number iProduct, Number iSerialNumber, String manufacturer, - String product, String serialNumber) throws DataDictionaryException - { - this(); - - this.setBcdDevice(bcdDevice); - this.setBcdUSB(bcdUSB); - this.setBDescriptorType(bDescriptorType); - this.setBDeviceClass(bDeviceClass); - this.setBDeviceProtocol(bDeviceProtocol); - this.setBDeviceSubClass(bDeviceSubClass); - this.setBLength(bLength); - this.setBMaxPacketSize0(bMaxPacketSize0); - this.setBNumConfigurations(bNumConfigurations); - this.setIdProduct(idProduct); - this.setIdVendor(idVendor); - this.setIManufacturer(iManufacturer); - this.setIProduct(iProduct); - this.setISerialNumber(iSerialNumber); - this.setManufacturer(manufacturer); - this.setProduct(product); - this.setSerialNumber(serialNumber); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get bcd device - * - * @return the bcd device - */ - public Number getBcdDevice() - { - return (Number) this.data.get(KEY_BCD_DEVICE); - } - - /** - * Get bcd u s b - * - * @return the bcd u s b - */ - public Number getBcdUSB() - { - return (Number) this.data.get(KEY_BCD_USB); - } - - /** - * Get b descriptor type - * - * @return the b descriptor type - */ - public Number getBDescriptorType() - { - return (Number) this.data.get(KEY_B_DESCRIPTOR_TYPE); - } - - /** - * Get b device class - * - * @return the b device class - */ - public Number getBDeviceClass() - { - return (Number) this.data.get(KEY_B_DEVICE_CLASS); - } - - /** - * Get b device protocol - * - * @return the b device protocol - */ - public Number getBDeviceProtocol() - { - return (Number) this.data.get(KEY_B_DEVICE_PROTOCOL); - } - - /** - * Get b device sub class - * - * @return the b device sub class - */ - public Number getBDeviceSubClass() - { - return (Number) this.data.get(KEY_B_DEVICE_SUB_CLASS); - } - - /** - * Get b length - * - * @return the b length - */ - public Number getBLength() - { - return (Number) this.data.get(KEY_B_LENGTH); - } - - /** - * Get b max packet size0 - * - * @return the b max packet size0 - */ - public Number getBMaxPacketSize0() - { - return (Number) this.data.get(KEY_B_MAX_PACKET_SIZE0); - } - - /** - * Get b num configurations - * - * @return the b num configurations - */ - public Number getBNumConfigurations() - { - return (Number) this.data.get(KEY_B_NUM_CONFIGURATIONS); - } - - /** - * Get id product - * - * @return the id product - */ - public Number getIdProduct() - { - return (Number) this.data.get(KEY_ID_PRODUCT); - } - - /** - * Get id vendor - * - * @return the id vendor - */ - public Number getIdVendor() - { - return (Number) this.data.get(KEY_ID_VENDOR); - } - - /** - * Get i manufacturer - * - * @return the i manufacturer - */ - public Number getIManufacturer() - { - return (Number) this.data.get(KEY_I_MANUFACTURER); - } - - /** - * Get i product - * - * @return the i product - */ - public Number getIProduct() - { - return (Number) this.data.get(KEY_I_PRODUCT); - } - - /** - * Get i serial number - * - * @return the i serial number - */ - public Number getISerialNumber() - { - return (Number) this.data.get(KEY_I_SERIAL_NUMBER); - } - - /** - * Get manufacturer - * - * @return the manufacturer - */ - public String getManufacturer() - { - return (String) this.data.get(KEY_MANUFACTURER); - } - - /** - * Get product - * - * @return the product - */ - public String getProduct() - { - return (String) this.data.get(KEY_PRODUCT); - } - - /** - * Get serial number - * - * @return the serial number - */ - public String getSerialNumber() - { - return (String) this.data.get(KEY_SERIAL_NUMBER); - } - - /** - * Set bcd device - * - * @param bcdDevice - * @throws DataDictionaryException - */ - public void setBcdDevice(Number bcdDevice) throws DataDictionaryException - { - this.setBcdDevice((Object) bcdDevice); - } - - /** - * Set bcd u s b - * - * @param bcdUSB - * @throws DataDictionaryException - */ - public void setBcdUSB(Number bcdUSB) throws DataDictionaryException - { - this.setBcdUSB((Object) bcdUSB); - } - - /** - * Set b descriptor type - * - * @param bDescriptorType - * @throws DataDictionaryException - */ - public void setBDescriptorType(Number bDescriptorType) throws DataDictionaryException - { - this.setBDescriptorType((Object) bDescriptorType); - } - - /** - * Set b device class - * - * @param bDeviceClass - * @throws DataDictionaryException - */ - public void setBDeviceClass(Number bDeviceClass) throws DataDictionaryException - { - this.setBDeviceClass((Object) bDeviceClass); - } - - /** - * Set b device protocol - * - * @param bDeviceProtocol - * @throws DataDictionaryException - */ - public void setBDeviceProtocol(Number bDeviceProtocol) throws DataDictionaryException - { - this.setBDeviceProtocol((Object) bDeviceProtocol); - } - - /** - * Set b device sub class - * - * @param bDeviceSubClass - * @throws DataDictionaryException - */ - public void setBDeviceSubClass(Number bDeviceSubClass) throws DataDictionaryException - { - this.setBDeviceSubClass((Object) bDeviceSubClass); - } - - /** - * Set b length - * - * @param bLength - * @throws DataDictionaryException - */ - public void setBLength(Number bLength) throws DataDictionaryException - { - this.setBLength((Object) bLength); - } - - /** - * Set b max packet size0 - * - * @param bMaxPacketSize0 - * @throws DataDictionaryException - */ - public void setBMaxPacketSize0(Number bMaxPacketSize0) throws DataDictionaryException - { - this.setBMaxPacketSize0((Object) bMaxPacketSize0); - } - - /** - * Set b num configurations - * - * @param bNumConfigurations - * @throws DataDictionaryException - */ - public void setBNumConfigurations(Number bNumConfigurations) throws DataDictionaryException - { - this.setBNumConfigurations((Object) bNumConfigurations); - } - - /** - * Set id product - * - * @param idProduct - * @throws DataDictionaryException - */ - public void setIdProduct(Number idProduct) throws DataDictionaryException - { - this.setIdProduct((Object) idProduct); - } - - /** - * Set id vendor - * - * @param idVendor - * @throws DataDictionaryException - */ - public void setIdVendor(Number idVendor) throws DataDictionaryException - { - this.setIdVendor((Object) idVendor); - } - - /** - * Set i manufacturer - * - * @param iManufacturer - * @throws DataDictionaryException - */ - public void setIManufacturer(Number iManufacturer) throws DataDictionaryException - { - this.setIManufacturer((Object) iManufacturer); - } - - /** - * Set i product - * - * @param iProduct - * @throws DataDictionaryException - */ - public void setIProduct(Number iProduct) throws DataDictionaryException - { - this.setIProduct((Object) iProduct); - } - - /** - * Set i serial number - * - * @param iSerialNumber - * @throws DataDictionaryException - */ - public void setISerialNumber(Number iSerialNumber) throws DataDictionaryException - { - this.setISerialNumber((Object) iSerialNumber); - } - - /** - * Set manufacturer - * - * @param manufacturer - * @throws DataDictionaryException - */ - public void setManufacturer(String manufacturer) throws DataDictionaryException - { - this.setManufacturer((Object) manufacturer); - } - - /** - * Set product - * - * @param product - * @throws DataDictionaryException - */ - public void setProduct(String product) throws DataDictionaryException - { - this.setProduct((Object) product); - } - - /** - * Set serial number - * - * @param serialNumber - * @throws DataDictionaryException - */ - public void setSerialNumber(String serialNumber) throws DataDictionaryException - { - this.setSerialNumber((Object) serialNumber); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setBcdDevice(Object bcdDevice) throws DataDictionaryException - { - this.data.put(KEY_BCD_DEVICE, this.kBcdDevice.convert(bcdDevice)); - } - - protected void setBcdUSB(Object bcdUSB) throws DataDictionaryException - { - this.data.put(KEY_BCD_USB, this.kBcdUSB.convert(bcdUSB)); - } - - protected void setBDescriptorType(Object bDescriptorType) throws DataDictionaryException - { - this.data.put(KEY_B_DESCRIPTOR_TYPE, this.kBDescriptorType.convert(bDescriptorType)); - } - - protected void setBDeviceClass(Object bDeviceClass) throws DataDictionaryException - { - this.data.put(KEY_B_DEVICE_CLASS, this.kBDeviceClass.convert(bDeviceClass)); - } - - protected void setBDeviceProtocol(Object bDeviceProtocol) throws DataDictionaryException - { - this.data.put(KEY_B_DEVICE_PROTOCOL, this.kBDeviceProtocol.convert(bDeviceProtocol)); - } - - protected void setBDeviceSubClass(Object bDeviceSubClass) throws DataDictionaryException - { - this.data.put(KEY_B_DEVICE_SUB_CLASS, this.kBDeviceSubClass.convert(bDeviceSubClass)); - } - - protected void setBLength(Object bLength) throws DataDictionaryException - { - this.data.put(KEY_B_LENGTH, this.kBLength.convert(bLength)); - } - - protected void setBMaxPacketSize0(Object bMaxPacketSize0) throws DataDictionaryException - { - this.data.put(KEY_B_MAX_PACKET_SIZE0, this.kBMaxPacketSize0.convert(bMaxPacketSize0)); - } - - protected void setBNumConfigurations(Object bNumConfigurations) throws DataDictionaryException - { - this.data.put(KEY_B_NUM_CONFIGURATIONS, this.kBNumConfigurations.convert(bNumConfigurations)); - } - - protected void setIdProduct(Object idProduct) throws DataDictionaryException - { - this.data.put(KEY_ID_PRODUCT, this.kIdProduct.convert(idProduct)); - } - - protected void setIdVendor(Object idVendor) throws DataDictionaryException - { - this.data.put(KEY_ID_VENDOR, this.kIdVendor.convert(idVendor)); - } - - protected void setIManufacturer(Object iManufacturer) throws DataDictionaryException - { - this.data.put(KEY_I_MANUFACTURER, this.kIManufacturer.convert(iManufacturer)); - } - - protected void setIProduct(Object iProduct) throws DataDictionaryException - { - this.data.put(KEY_I_PRODUCT, this.kIProduct.convert(iProduct)); - } - - protected void setISerialNumber(Object iSerialNumber) throws DataDictionaryException - { - this.data.put(KEY_I_SERIAL_NUMBER, this.kISerialNumber.convert(iSerialNumber)); - } - - protected void setManufacturer(Object manufacturer) throws DataDictionaryException - { - this.data.put(KEY_MANUFACTURER, this.kManufacturer.convert(manufacturer)); - } - - protected void setProduct(Object product) throws DataDictionaryException - { - this.data.put(KEY_PRODUCT, this.kProduct.convert(product)); - } - - protected void setSerialNumber(Object serialNumber) throws DataDictionaryException - { - this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kBcdDevice = new KeyDescriptorNumber(KEY_BCD_DEVICE, true); - this.keys.add(this.kBcdDevice); - - this.kBcdUSB = new KeyDescriptorNumber(KEY_BCD_USB, true); - this.keys.add(this.kBcdUSB); - - this.kBDescriptorType = new KeyDescriptorNumber(KEY_B_DESCRIPTOR_TYPE, true); - this.keys.add(this.kBDescriptorType); - - this.kBDeviceClass = new KeyDescriptorNumber(KEY_B_DEVICE_CLASS, true); - this.keys.add(this.kBDeviceClass); - - this.kBDeviceProtocol = new KeyDescriptorNumber(KEY_B_DEVICE_PROTOCOL, true); - this.keys.add(this.kBDeviceProtocol); - - this.kBDeviceSubClass = new KeyDescriptorNumber(KEY_B_DEVICE_SUB_CLASS, true); - this.keys.add(this.kBDeviceSubClass); - - this.kBLength = new KeyDescriptorNumber(KEY_B_LENGTH, true); - this.keys.add(this.kBLength); - - this.kBMaxPacketSize0 = new KeyDescriptorNumber(KEY_B_MAX_PACKET_SIZE0, true); - this.keys.add(this.kBMaxPacketSize0); - - this.kBNumConfigurations = new KeyDescriptorNumber(KEY_B_NUM_CONFIGURATIONS, true); - this.keys.add(this.kBNumConfigurations); - - this.kIdProduct = new KeyDescriptorNumber(KEY_ID_PRODUCT, true); - this.keys.add(this.kIdProduct); - - this.kIdVendor = new KeyDescriptorNumber(KEY_ID_VENDOR, true); - this.keys.add(this.kIdVendor); - - this.kIManufacturer = new KeyDescriptorNumber(KEY_I_MANUFACTURER, true); - this.keys.add(this.kIManufacturer); - - this.kIProduct = new KeyDescriptorNumber(KEY_I_PRODUCT, true); - this.keys.add(this.kIProduct); - - this.kISerialNumber = new KeyDescriptorNumber(KEY_I_SERIAL_NUMBER, true); - this.keys.add(this.kISerialNumber); - - this.kManufacturer = new KeyDescriptorString(KEY_MANUFACTURER, false, true); - this.keys.add(this.kManufacturer); - - this.kProduct = new KeyDescriptorString(KEY_PRODUCT, false, true); - this.keys.add(this.kProduct); - - this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, true); - this.keys.add(this.kSerialNumber); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/io/usb/USBPortFinder.java b/src/main/java/enedis/lab/io/usb/USBPortFinder.java deleted file mode 100644 index 391e3e2..0000000 --- a/src/main/java/enedis/lab/io/usb/USBPortFinder.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.usb; - -import java.util.stream.Collectors; - -import enedis.lab.io.PortFinder; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; - -/** - * Interface used to find all USB port descriptor - */ -public interface USBPortFinder extends PortFinder -{ - /** - * Find USB device with the given product id - * - * @param idProduct the USB product identifier - * @return the USB descriptor data list of all USB device connected with the given product id - */ - public default DataList findByProductId(int idProduct) - { - DataList descriptors = new DataArrayList(); - // @formatter:off - descriptors.addAll(this.findAll().stream() - .filter(d -> d.getIdProduct().intValue() == idProduct) - .collect(Collectors.toList())); - // @formatter:on - return descriptors; - } - - /** - * Find USB device with the given vendor id - * - * @param idVendor the USB vendor identifier - * @return the USB descriptor data list of all USB device connected with the given vendor id - */ - public default DataList findByVendorId(int idVendor) - { - DataList descriptors = new DataArrayList(); - // @formatter:off - descriptors.addAll(this.findAll().stream() - .filter(d -> d.getIdVendor().intValue() == idVendor) - .collect(Collectors.toList())); - // @formatter:on - return descriptors; - } - - /** - * Find USB device with the given product id and vendor id - * - * @param idProduct the USB product identifier - * @param idVendor the USB vendor identifier - * @return the USB descriptor data list of all USB device connected with the given product id and vendor id - */ - public default DataList findByProductIdAndVendorId(int idProduct, int idVendor) - { - DataList descriptors = new DataArrayList(); - // @formatter:off - descriptors.addAll(this.findAll().stream() - .filter(d -> d.getIdProduct().intValue() == idProduct) - .filter(d -> d.getIdVendor().intValue() == idVendor) - .collect(Collectors.toList())); - // @formatter:on - return descriptors; - } -} diff --git a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java b/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java deleted file mode 100644 index 712626c..0000000 --- a/src/main/java/enedis/lab/io/usb/USBPortFinderBase.java +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.usb; - -import org.usb4java.Context; -import org.usb4java.Device; -import org.usb4java.DeviceDescriptor; -import org.usb4java.DeviceHandle; -import org.usb4java.DeviceList; -import org.usb4java.LibUsb; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * Class used to find all USB port descriptor - */ -public class USBPortFinderBase implements USBPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program writing the USB port descriptor list (JSON format) on the output stream - * - * @param args - * not used - */ - public static void main(String[] args) - { - DataList descriptors = getInstance().findAll(); - - System.out.println(descriptors.toString(2)); - } - - /** - * Get instance - * - * @return Unique instance - */ - public static USBPortFinderBase getInstance() - { - if (instance == null) - { - instance = new USBPortFinderBase(); - } - - return instance; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static USBPortFinderBase instance; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private USBPortFinderBase() - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// USBPortFinder - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataList findAll() - { - DataList usbPortList = new DataArrayList(); - - Context context = new Context(); - DeviceList deviceList = new DeviceList(); - - // Initialize USB library access - int result = LibUsb.init(context); - if (result != LibUsb.SUCCESS) - { - return usbPortList; - } - - // Get available USB device list - result = LibUsb.getDeviceList(context, deviceList); - if (result < 0) - { - return usbPortList; - } - - // For each USB device - for (Device device : deviceList) - { - DeviceDescriptor descriptor = new DeviceDescriptor(); - - // Get USB device descriptor - result = LibUsb.getDeviceDescriptor(device, descriptor); - if (result != LibUsb.SUCCESS) - { - continue; - } - String productName = null; - String manufacturer = null; - String serialNumber = null; - // If USB device has string descriptors - if (descriptor.idProduct() != 0) - { - // Get string descriptors for USB device - DeviceHandle handle = new DeviceHandle(); - result = LibUsb.open(device, handle); - if (result == LibUsb.SUCCESS) - { - productName = LibUsb.getStringDescriptor(handle, descriptor.iProduct()); - manufacturer = LibUsb.getStringDescriptor(handle, descriptor.iManufacturer()); - serialNumber = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber()); - LibUsb.close(handle); - } - } - - try - { - // @formatter:off - USBPortDescriptor usbPort = new USBPortDescriptor( - descriptor.bcdDevice() & 0xFFFF, - descriptor.bcdUSB() & 0xFFFF, - descriptor.bDescriptorType() & 0xFF, - descriptor.bDeviceClass() & 0xFF, - descriptor.bDeviceProtocol() & 0xFF, - descriptor.bDeviceSubClass() & 0xFF, - descriptor.bLength() & 0xFF, - descriptor.bMaxPacketSize0() & 0xFF, - descriptor.bNumConfigurations() & 0xFF, - descriptor.idProduct() & 0xFFFF, - descriptor.idVendor() & 0xFFFF, - descriptor.iManufacturer() & 0xFF, - descriptor.iProduct() & 0xFF, - descriptor.iSerialNumber() & 0xFF, - manufacturer, - productName, - serialNumber); - // @formatter:on - usbPortList.add(usbPort); - } - catch (DataDictionaryException e) - { - } - } - - // Finalize USB library access - LibUsb.freeDeviceList(deviceList, true); - LibUsb.exit(context); - return usbPortList; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/protocol/tic/TICMode.java b/src/main/java/enedis/lab/protocol/tic/TICMode.java deleted file mode 100644 index 5bbc299..0000000 --- a/src/main/java/enedis/lab/protocol/tic/TICMode.java +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic; - -import java.util.Arrays; - -/** - * TIC mode used available - */ - -import enedis.lab.protocol.tic.frame.TICError; -import enedis.lab.protocol.tic.frame.standard.TICException; - -/** - * TIC Mode - * - */ -public enum TICMode -{ - /** Unknown mode */ - UNKNOWN, - /** Standard mode */ - STANDARD, - /** Historic mode */ - HISTORIC, - /** Auto mode */ - AUTO; - - /** Historic separator */ - public static final char HISTORIC_SEPARATOR = ' '; - /** Standard separator */ - public static final char STANDARD_SEPARATOR = '\t'; - /** Historic buffer start */ - public static final byte[] HISTORIC_BUFFER_START = { 2, 10, 65, 68, 67, 79 }; - /** Standard buffer start */ - public static final byte[] STANDARD_BUFFER_START = { 2, 10, 65, 68, 83, 67 }; - - /** - * Find mode from Frame Buffer - * - * @param frameBuffer - * @return TICMode - * @throws TICException - */ - public static TICMode findModeFromFrameBuffer(byte[] frameBuffer) throws TICException - { - byte[] frameBufferStart = new byte[HISTORIC_BUFFER_START.length]; - if (frameBuffer.length < frameBufferStart.length) - { - throw new TICException("Tic frame read 0x" + bytesToHex(frameBuffer) + " too short to determine TIC Mode !", - TICError.TIC_READER_FRAME_DECODE_FAILED); - } - System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); - if (Arrays.equals(frameBufferStart, HISTORIC_BUFFER_START)) - { - return HISTORIC; - } - else - { - if (STANDARD_BUFFER_START.length != HISTORIC_BUFFER_START.length) - { - frameBufferStart = new byte[STANDARD_BUFFER_START.length]; - System.arraycopy(frameBuffer, 0, frameBufferStart, 0, frameBufferStart.length); - } - if (Arrays.equals(frameBufferStart, STANDARD_BUFFER_START)) - { - return STANDARD; - } - return null; - } - } - - /** - * Find Mode frome Group Buffer - * - * @param groupBuffer - * @return TICMode - */ - public static TICMode findModeFromGroupBuffer(byte[] groupBuffer) - { - for (int i = 0; i < groupBuffer.length; i++) - { - if (groupBuffer[i] == HISTORIC_SEPARATOR) - { - return HISTORIC; - } - else if (groupBuffer[i] == STANDARD_SEPARATOR) - { - return STANDARD; - } - } - return null; - } - - /** - * Convert byte array to hexadecimal string (replacement for - * DatatypeConverter.printHexBinary) - * - * @param bytes the byte array to convert - * @return hexadecimal string representation - */ - private static String bytesToHex(byte[] bytes) { - StringBuilder result = new StringBuilder(); - for (byte b : bytes) { - result.append(String.format("%02X", b)); - } - return result.toString(); - } - -} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java deleted file mode 100644 index 468c112..0000000 --- a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPort.java +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.channels; - -import java.util.Arrays; - -import enedis.lab.io.channels.ChannelConfiguration; -import enedis.lab.io.channels.ChannelException; -import enedis.lab.io.channels.serialport.ChannelSerialPort; -import enedis.lab.io.channels.serialport.ChannelSerialPortErrorCode; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.BytesArray; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.time.Time; - -/** - * Channel TIC SerialPort - */ -public class ChannelTICSerialPort extends ChannelSerialPort -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final byte START_PATTERN = (byte) 0x02; - private static final byte END_PATTERN = (byte) 0x03; - private static final int RECEIVE_DATA_POLLING_PERIOD = 100; - private static final byte[] HISTORIC_BUFFER_START = { 2, 10, 65, 68, 67, 79 }; - private static final byte[] STANDARD_BUFFER_START = { 2, 10, 65, 68, 83, 67 }; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICMode currentMode; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor: create an instance from a SerialPortConfiguration - * - * @param configuration - * @throws ChannelException - */ - public ChannelTICSerialPort(ChannelTICSerialPortConfiguration configuration) throws ChannelException - { - super(configuration); - this.currentMode = null; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Channel - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException - { - if (configuration == null) - { - ChannelException.raiseInvalidConfiguration("null"); - } - if (!(configuration instanceof ChannelTICSerialPortConfiguration)) - { - ChannelException.raiseInvalidConfigurationType(configuration, ChannelTICSerialPortConfiguration.class.getSimpleName()); - } - super.setup(configuration); - } - - @Override - public ChannelTICSerialPortConfiguration getConfiguration() - { - return (ChannelTICSerialPortConfiguration) this.configuration; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get mode - * - * @return TIC Mode - */ - public TICMode getMode() - { - return (this.getCurrentMode() != null) ? this.getCurrentMode() : this.getSelectedMode(); - } - - /** - * Get selected mode - * - * @return selected Mode - */ - public TICMode getSelectedMode() - { - return this.getConfiguration().getTicMode(); - } - - /** - * Get current mode - * - * @return current Mode - */ - public TICMode getCurrentMode() - { - return this.currentMode; - } - - @Override - public byte[] read() throws ChannelException - { - long beginTime = System.currentTimeMillis(); - long elapsedTime = 0; - long timeout = this.getSyncReadTimeout(); - BytesArray buffer = new BytesArray(); - byte[] ticFrame = null; - int startOfFrame = -1, endOfFrame = -1; - - while (elapsedTime < timeout || timeout == 0) - { - if (this.available() > 0) - { - buffer.addAll(super.read(1)); - if (startOfFrame == -1) - { - startOfFrame = buffer.indexOf(START_PATTERN); - if (startOfFrame != -1) - { - endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); - if (endOfFrame != -1) - { - ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); - break; - } - } - } - else - { - endOfFrame = buffer.indexOf(END_PATTERN, startOfFrame + 1); - if (endOfFrame != -1) - { - ticFrame = buffer.subList(startOfFrame, endOfFrame).getBytes(); - break; - } - } - } - else - { - Time.sleep(RECEIVE_DATA_POLLING_PERIOD); - elapsedTime = (System.currentTimeMillis() - beginTime); - } - } - - return ticFrame; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void receiveData() - { - if (this.currentMode == null) - { - if (this.autoDetectMode() == null) - { - this.onReadTimeout(); - return; - } - } - try - { - byte[] ticFrame = this.read(); - - if (ticFrame == null) - { - this.onReadTimeout(); - } - else - { - this.notifyOnDataRead(ticFrame); - } - } - catch (ChannelException e) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), "TIC read failed: " + e.getErrorInfo()); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void onReadTimeout() - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_TIMEOUT.getCode(ERROR_CODE_OFFSET), "TIC read timeout"); - this.configurePort(); - this.currentMode = null; - } - - private void configurePort() - { - this.closePort(); - this.openPort(); - try - { - this.flush(); - } - catch (ChannelException e) - { - } - } - - private void setSelectedMode(TICMode ticMode) - { - try - { - this.getConfiguration().setTicMode(ticMode); - } - catch (DataDictionaryException e) - { - this.logger.error("Cannot set TIC mode " + ticMode, e); - } - } - - private boolean checkAndUpdateMode(TICMode ticMode) - { - boolean result = false; - this.setSelectedMode(ticMode); - this.configurePort(); - try - { - byte[] ticFrame = this.read(); - if (ticFrame != null && ticFrame.length > HISTORIC_BUFFER_START.length) - { - byte[] ticFrameStart = new byte[HISTORIC_BUFFER_START.length]; - System.arraycopy(ticFrame, 0, ticFrameStart, 0, ticFrameStart.length); - if (ticMode == TICMode.HISTORIC) - { - if (Arrays.equals(ticFrameStart, HISTORIC_BUFFER_START)) - { - result = true; - } - } - else if (ticMode == TICMode.STANDARD) - { - if (Arrays.equals(ticFrameStart, STANDARD_BUFFER_START)) - { - result = true; - } - } - } - } - catch (ChannelException e) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), "TIC read failed: " + e.getErrorInfo()); - } - if (result) - { - this.currentMode = ticMode; - } - this.setSelectedMode(TICMode.AUTO); - - return result; - } - - private TICMode autoDetectMode() - { - if (this.getSelectedMode() != TICMode.AUTO) - { - this.currentMode = this.getSelectedMode(); - return this.getSelectedMode(); - } - this.logger.debug("Auto detecting TIC Mode"); - if (!this.checkAndUpdateMode(TICMode.HISTORIC)) - { - if (!this.checkAndUpdateMode(TICMode.STANDARD)) - { - this.logger.debug("TIC Mode not detected"); - - return null; - } - this.logger.debug("TIC Mode STANDARD detected"); - - return TICMode.STANDARD; - } - this.logger.debug("TIC Mode HISTORIC detected"); - - return TICMode.HISTORIC; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java b/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java deleted file mode 100644 index ecfeff6..0000000 --- a/src/main/java/enedis/lab/protocol/tic/channels/ChannelTICSerialPortConfiguration.java +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.channels; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.io.channels.serialport.ChannelSerialPortConfiguration; -import enedis.lab.io.channels.serialport.Parity; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; - -/** - * ChannelTICSerialPortConfiguration class - * - * Generated - */ -public class ChannelTICSerialPortConfiguration extends ChannelSerialPortConfiguration -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_TIC_MODE = "ticMode"; - - protected static final Number BAUDRATE_HISTORIC = 1200; - protected static final Number BAUDRATE_STANDARD = 9600; - protected static final Number[] BAUDRATE_ACCEPTED_VALUES = { BAUDRATE_HISTORIC, BAUDRATE_STANDARD }; - protected static final Parity PARITY_ACCEPTED_VALUE = Parity.EVEN; - protected static final Number DATA_BITS_ACCEPTED_VALUE = 7; - protected static final Number STOP_BITS_ACCEPTED_VALUE = 1.0d; - protected static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kTicMode; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ChannelTICSerialPortConfiguration() - { - super(); - this.loadKeyDescriptors(); - - this.kBaudrate.setAcceptedValues(BAUDRATE_ACCEPTED_VALUES); - this.kParity.setAcceptedValues(PARITY_ACCEPTED_VALUE); - this.kDataBits.setAcceptedValues(DATA_BITS_ACCEPTED_VALUE); - this.kStopBits.setAcceptedValues(STOP_BITS_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ChannelTICSerialPortConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ChannelTICSerialPortConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public ChannelTICSerialPortConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param portName - * @param ticMode - * @throws DataDictionaryException - */ - public ChannelTICSerialPortConfiguration(String name, String portName, TICMode ticMode) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setPortName(portName); - this.setTicMode(ticMode); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ChannelSerialPortConfiguration - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TIC_MODE)) - { - this.setTicMode(TIC_MODE_DEFAULT_VALUE); - } - switch (this.getTicMode()) - { - case HISTORIC: - { - this.setBaudrate(BAUDRATE_HISTORIC); - this.setParity(PARITY_ACCEPTED_VALUE); - this.setDataBits(DATA_BITS_ACCEPTED_VALUE); - this.setStopBits(STOP_BITS_ACCEPTED_VALUE); - break; - } - case STANDARD: - case AUTO: - default: - { - this.setBaudrate(BAUDRATE_STANDARD); - this.setParity(PARITY_ACCEPTED_VALUE); - this.setDataBits(DATA_BITS_ACCEPTED_VALUE); - this.setStopBits(STOP_BITS_ACCEPTED_VALUE); - } - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get tic mode - * - * @return the tic mode - */ - public TICMode getTicMode() - { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Set tic mode - * - * @param ticMode - * @throws DataDictionaryException - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException - { - this.setTicMode((Object) ticMode); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setTicMode(Object ticMode) throws DataDictionaryException - { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - switch (this.getTicMode()) - { - case HISTORIC: - this.setBaudrate(BAUDRATE_HISTORIC); - break; - case STANDARD: - case AUTO: - this.setBaudrate(BAUDRATE_STANDARD); - break; - default: - break; - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); - this.keys.add(this.kTicMode); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java deleted file mode 100644 index 5a47500..0000000 --- a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPort.java +++ /dev/null @@ -1,823 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -import org.apache.commons.lang3.SystemUtils; - -import enedis.lab.io.channels.ChannelConfiguration; -import enedis.lab.io.channels.ChannelException; -import enedis.lab.io.channels.ChannelPhysical; -import enedis.lab.io.channels.ChannelStatus; -import enedis.lab.io.serialport.SerialPortDescriptor; -import enedis.lab.io.serialport.SerialPortFinderBase; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.SystemError; -import jssc.SerialPort; -import jssc.SerialPortEvent; -import jssc.SerialPortEventListener; -import jssc.SerialPortException; -import jssc.SerialPortTimeoutException; - -/** - * Channel serial port - */ -public class ChannelSerialPort extends ChannelPhysical implements SerialPortEventListener -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final int ERROR_CODE_OFFSET = 1000; - private static final int DEFAULT_DELAY = 500; - private static final int DELAY_REOPEN = DEFAULT_DELAY * 10; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Check if the port is found - * - * @param portId - * @param portName - * @return true if the specified port is found - */ - public static boolean isPortFound(String portId, String portName) - { - return SerialPortFinderBase.getInstance().findByPortIdOrPortName(portId, portName) != null; - } - - /** - * Find the port name corresponding to the given portId or portName. portId has a higher priority - * - * @param portId - * @param portName - * @return port name - */ - public static String findPortName(String portId, String portName) - { - SerialPortDescriptor descriptor = null; - if (portId != null) - { - descriptor = SerialPortFinderBase.getInstance().findByPortId(portId); - } - else - { - descriptor = SerialPortFinderBase.getInstance().findByPortName(portName); - } - - return (descriptor != null) ? descriptor.getPortName() : null; - } - - /** - * Convert the parity provided by the SerialPortConfiguration to a value usable by the channel - * - * @param parity - * @return parity - */ - public static int parityFromConfiguration(Parity parity) - { - int retVal = -1; - - switch (parity) - { - case EVEN: - retVal = SerialPort.PARITY_EVEN; - break; - case MARK: - retVal = SerialPort.PARITY_MARK; - break; - case NONE: - retVal = SerialPort.PARITY_NONE; - break; - case ODD: - retVal = SerialPort.PARITY_ODD; - break; - case SPACE: - retVal = SerialPort.PARITY_SPACE; - break; - default: /* Cas non atteignable */ - } - - return retVal; - } - - /** - * Convert the stopBits parameter provided by the SerialPortConfiguration to a value usable by the channel - * - * @param stop_bits - * @return stopBits - */ - public static int stopBitsFromConfiguration(float stop_bits) - { - int retVal = -1; - - if (1 == stop_bits) - { - retVal = SerialPort.STOPBITS_1; - } - - else if (1.5 == stop_bits) - { - retVal = SerialPort.STOPBITS_1_5; - } - - else if (2 == stop_bits) - { - retVal = SerialPort.STOPBITS_2; - } - - return retVal; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private SerialPort portHandler = null; - - private String previousPortName; - private String currentPortName; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor: create an instance from a ChannelConfiguration - * - * @param configuration - * @throws ChannelException - */ - public ChannelSerialPort(ChannelConfiguration configuration) throws ChannelException - { - super(configuration); - this.setPeriod(DEFAULT_DELAY); - - this.currentPortName = this.findPortName(); - this.previousPortName = this.currentPortName; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Channel - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void start() - { - this.logger.info("Channel " + this.getName() + " start (" + this.findPortName() + ")"); - this.openPort(); - super.start(); - } - - @Override - public void stop() - { - this.logger.info("Channel " + this.getName() + " stop (" + this.findPortName() + ")"); - super.stop(); - this.closePort(); - } - - @Override - public void setup(ChannelConfiguration configuration) throws ChannelException - { - if (configuration == null) - { - ChannelException.raiseInvalidConfiguration("null"); - } - if (!(configuration instanceof ChannelSerialPortConfiguration)) - { - ChannelException.raiseInvalidConfigurationType(configuration, ChannelSerialPortConfiguration.class.getSimpleName()); - } - super.setup(configuration); - } - - @Override - public byte[] read() throws ChannelException - { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - buffer = this.portHandler.readBytes(); - } - catch (SerialPortException exception) - { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - @Override - public void write(byte[] data) throws ChannelException - { - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - this.portHandler.writeBytes(data); - } - catch (SerialPortException exception) - { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot write bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - } - - @Override - public ChannelSerialPortConfiguration getConfiguration() - { - return (ChannelSerialPortConfiguration) this.configuration.clone(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ChannelBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void setup() throws ChannelException - { - /* 1. Save current state */ - ChannelStatus currentStatus = this.status; - /* 2. Stop serial port */ - this.stop(); - /* 3. update path if symbolic link */ - this.updateRealPath(); - /* 4. Create serial port handler */ - if ((null == this.portHandler) || (false == this.getPortName().equals(this.portHandler.getPortName()))) - { - this.portHandler = new SerialPort(this.getPortName()); - } - /* 5. Restore current state */ - if (ChannelStatus.STARTED == currentStatus) - { - this.start(); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// SerialPortEventListener - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void serialEvent(SerialPortEvent event) - { - switch (event.getEventType()) - { - case SerialPortEvent.RXCHAR: - this.receiveData(); - break; - default: /* Aucune action */ - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Runnable - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void process() - { - int delay = DEFAULT_DELAY; - - if (this.status == ChannelStatus.STARTED) - { - if (!this.isPortFound()) - { - this.onPortNotFound(); - } - else if (this.hasPortChanged()) - { - this.onPortNameChanged(); - } - else if (!this.hasEventListener()) - { - this.receiveData(); - } - } - else - { - if (this.isPortFound()) - { - this.openPort(); - if (this.status == ChannelStatus.ERROR) - { - delay = DELAY_REOPEN; - } - } - } - this.setPeriod(delay); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Blocking Read - * - * @param bytesCount - * @return read bytes - * @throws ChannelException - */ - public byte[] read(int bytesCount) throws ChannelException - { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - buffer = this.portHandler.readBytes(bytesCount); - } - catch (SerialPortException exception) - { - this.setStatus(ChannelStatus.ERROR); - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - /** - * Blocking read with timeout - * - * @param bytesCount - * @param timeout - * @return read bytes - * @throws ChannelException - */ - public byte[] read(int bytesCount, int timeout) throws ChannelException - { - byte[] buffer = null; - - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - buffer = this.portHandler.readBytes(bytesCount, timeout); - } - catch (SerialPortException exception) - { - this.logger.error("Cannot read bytes", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - catch (SerialPortTimeoutException exception) - { - ChannelException.raiseInternalError(exception.getMessage()); - } - - return buffer; - } - - /** - * Get available bytes count - * - * @return Available bytes count - * @throws ChannelException - * if serial port not open or internal error occurs - */ - public int available() throws ChannelException - { - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - return this.portHandler.getInputBufferBytesCount(); - } - catch (SerialPortException exception) - { - this.logger.error("Cannot get bytes available", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - - return 0; - } - - /** - * Flush port - * - * @throws ChannelException - */ - public void flush() throws ChannelException - { - if (this.portHandler == null || !this.portHandler.isOpened()) - { - ChannelException.raiseChannelNotReady(this.getPortName()); - } - try - { - int mask = SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_RXABORT | SerialPort.PURGE_TXCLEAR | SerialPort.PURGE_TXABORT; - if (!this.portHandler.purgePort(mask)) - { - this.logger.error("Channel " + this.getName() + " failed to purge port " + this.getPortName() + " (" + SystemError.getMessage() + ") "); - } - } - catch (SerialPortException exception) - { - this.logger.error("Cannot purge port", exception); - ChannelException.raiseInternalError(exception.getMessage()); - } - } - - /** - * - * @return the identifier of the port. If the parameter has not been set, 'null' is returned. - */ - public String getPortId() - { - return this.getChannelConfiguration().getPortId(); - } - - /** - * - * @return the name of the port. If the parameter has not been set, 'null' is returned. - */ - public String getPortName() - { - return this.getChannelConfiguration().getPortName(); - } - - /** - * - * @return the name of the port opened. If the port has not been opened, 'null' is returned. - */ - public String getPortNameOpened() - { - return (this.portHandler != null) ? this.portHandler.getPortName() : null; - } - - /** - * Find the port name associated with configuration - * - * @return Port name found or null if nothing found - */ - public String findPortName() - { - return findPortName(this.getPortId(), this.getPortName()); - } - - /** - * @return the current baudrate value. If the Baudrate has not been set (port has not been used), -1 is returned - */ - public int getBaudrate() - { - return this.getChannelConfiguration().getBaudrate().intValue(); - } - - /** - * @return the number of bits used for data (5, 6, 7, 8). If the parameter does not exist in channel configuration - * or is not consistent, the value '-1' is returned. - */ - public int getDataBits() - { - return this.getChannelConfiguration().getDataBits().intValue(); - } - - /** - * @return the current number of bits used for stop (1.0 , 1.5 , 2.0). - * - * NB : If the port has been used, the method returns -1. - */ - public float getStopBits() - { - return this.getChannelConfiguration().getStopBits().floatValue(); - } - - /** - * @return the current parity used by the port - * - * NB : If the port has been used, the method returns null. - */ - public Parity getParity() - { - return this.getChannelConfiguration().getParity(); - } - - /** - * @return the timeout used for a synchronous read operation - */ - public int getSyncReadTimeout() - { - return this.getChannelConfiguration().getSyncReadTimeout().intValue(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected boolean hasEventListener() - { - return false; - } - - protected boolean addEventListener() - { - if (this.portHandler == null) - { - return false; - } - int mask = SerialPort.MASK_RXCHAR + SerialPort.MASK_CTS + SerialPort.MASK_DSR; - try - { - if (!this.portHandler.setEventsMask(mask)) - { - this.logger.error("Channel " + this.getName() + " failed to set event mask on port " + this.getPortName() + " (" + SystemError.getMessage() + ") "); - return false; - } - } - catch (SerialPortException exception) - { - this.logger.error("Channel " + this.getName() + " failed to set event mask on port " + this.getPortName() + " : " + exception.getMessage()); - return false; - } - try - { - this.portHandler.addEventListener(this); - } - catch (SerialPortException e) - { - this.logger.error("Channel " + this.getName() + " failed to add listener :" + e.getMessage(), e); - return false; - } - - return true; - } - - protected boolean removeEventListener() - { - if (this.portHandler == null) - { - return false; - } - try - { - if (!this.portHandler.removeEventListener()) - { - this.logger.error("Channel " + this.getName() + " failed to remove listener"); - return false; - } - } - catch (SerialPortException e) - { - this.logger.error("Channel " + this.getName() + " failed to remove listener :" + e.getMessage(), e); - return false; - } - - return true; - } - - protected void receiveData() - { - try - { - if (this.available() > 0) - { - byte[] buffer = this.read(); - this.notifyOnDataRead(buffer); - } - } - catch (ChannelException exception) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.READ_FAILED.getCode(ERROR_CODE_OFFSET), exception.getMessage()); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private boolean hasPortChanged() - { - boolean portChanged = false; - - this.currentPortName = this.findPortName(); - - if (this.currentPortName != null && !this.currentPortName.equalsIgnoreCase(this.previousPortName)) - { - portChanged = true; - } - - return portChanged; - } - - private void onPortNameChanged() - { - String errorMessage = "Port name has changed ( " + this.previousPortName + " --> " + this.currentPortName + " )"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_NAME_HAS_CHANGED.getCode(ERROR_CODE_OFFSET), errorMessage); - this.previousPortName = this.currentPortName; - } - - private void updateRealPath() throws ChannelException - { - if (SystemUtils.IS_OS_LINUX) - { - String realPortName; - try - { - Process p = Runtime.getRuntime().exec("realpath " + this.getPortName()); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream())); - realPortName = stdInput.readLine(); - this.getChannelConfiguration().setPortName(realPortName); - } - catch (IOException | DataDictionaryException e) - { - throw new ChannelException(ChannelException.ERRCODE_INVALID_CONFIGURATION, "Cannot resolve realpath (" + e.getMessage() + ")"); - } - } - } - - private ChannelSerialPortConfiguration getChannelConfiguration() - { - return (ChannelSerialPortConfiguration) this.configuration; - } - - protected void openPort() - { - /* 1. Check if serial port is already opened */ - if (this.portHandler != null && this.portHandler.isOpened()) - { - return; - } - /* 2. Open serial port */ - String portNameFound = this.findPortName(); - try - { - this.logger.info("Channel " + this.getName() + " opening port " + portNameFound); - if (this.portHandler == null) - { - this.portHandler = new SerialPort(portNameFound); - } - if (!this.portHandler.openPort()) - { - this.logger.error("Channel " + this.getName() + " failed to open port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); - return; - } - this.logger.info("Channel " + this.getName() + " configuring port " + portNameFound); - if (!this.portHandler.setParams(this.getBaudrate(), this.getDataBits(), stopBitsFromConfiguration(this.getStopBits()), parityFromConfiguration(this.getParity()))) - { - this.logger.error("Channel " + this.getName() + " failed to configure port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); - return; - } - } - catch (SerialPortException exception) - { - String errorMessage = "Channel " + this.getName() + " failed to open port " + this.getPortNameOpened() + " : " + exception.getMessage(); - this.logger.error(errorMessage); - if (exception.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) - { - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_BUSY.getCode(ERROR_CODE_OFFSET), errorMessage); - } - this.closePortForced(); - return; - } - /* 3. Add serial event listener */ - if (this.hasEventListener()) - { - if (!this.addEventListener()) - { - return; - } - } - this.setStatus(ChannelStatus.STARTED); - } - - protected void closePort() - { - /* 1. Check if serial port is already closed */ - if (this.portHandler == null || !this.portHandler.isOpened()) - { - return; - } - /* 2. Remove serial event listener */ - if (this.hasEventListener()) - { - this.removeEventListener(); - } - /* 3. Close serial port */ - try - { - this.logger.info("Channel " + this.getName() + " closing port " + this.getPortNameOpened()); - if (!this.portHandler.closePort()) - { - this.logger.error("Channel " + this.getName() + " failed to close port " + this.getPortNameOpened() + " (" + SystemError.getMessage() + ") "); - return; - } - } - catch (SerialPortException exception) - { - this.logger.error("Channel " + this.getName() + " failed to close port " + this.getPortNameOpened() + " : " + exception.getMessage()); - return; - } - this.setStatus(ChannelStatus.STOPPED); - } - - protected void closePortForced() - { - try - { - this.portHandler.closePort(); - } - catch (SerialPortException exception) - { - this.logger.error("Channel " + this.getName() + " fail on close : close " + this.getPortNameOpened() + " failed due to " + exception.getMessage()); - } - this.portHandler = new SerialPort(this.findPortName()); - } - - protected boolean isPortFound() - { - return isPortFound(this.getPortId(), this.getPortName()); - } - - protected boolean isPortOpenedFound() - { - String portNameOpened = this.getPortNameOpened(); - if (portNameOpened == null) - { - return false; - } - String portNameFound = this.findPortName(); - - return portNameOpened.equals(portNameFound); - } - - protected void onPortNotFound() - { - String errorMessage = "Channel " + this.getName() + " : serial port " + this.findPortName() + " not found"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); - } - - protected void onPortOpenedNotFound() - { - String errorMessage = "Channel " + this.getName() + " : serial port " + this.getPortNameOpened() + " opened not found"; - this.logger.error(errorMessage); - this.setStatus(ChannelStatus.ERROR); - this.closePortForced(); - this.notifyOnErrorDetected(ChannelSerialPortErrorCode.PORT_OPENED_NOT_FOUND.getCode(ERROR_CODE_OFFSET), errorMessage); - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java b/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java deleted file mode 100644 index 29647b4..0000000 --- a/src/main/java/enedis/lab/protocol/tic/channels/serialport/ChannelSerialPortErrorCode.java +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.channels.serialport; - -/** - * Channel serial port error code - */ -public enum ChannelSerialPortErrorCode -{ - /** Port not found */ - PORT_NOT_FOUND(1), - - /** Port opened not found */ - PORT_OPENED_NOT_FOUND(2), - - /** Port name has changed */ - PORT_NAME_HAS_CHANGED(3), - - /** Port busy */ - PORT_BUSY(4), - - /** Read operation failed */ - READ_FAILED(5), - - /** Read operation timed out */ - READ_TIMEOUT(6); - - private int code; - - private ChannelSerialPortErrorCode(int code) - { - this.code = code; - } - - /** - * Get error code without offset - * - * @return error code - */ - public int getCode() - { - return this.code; - } - - /** - * Get error code with offset - * - * @param offset - * - * @return error code - */ - public int getCode(int offset) - { - return offset + this.code; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java deleted file mode 100644 index 726f099..0000000 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoric.java +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import java.util.List; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; -import enedis.lab.types.BytesArray; - -/** - * Codec TIC Frame Historic - * - */ -public class CodecTICFrameHistoric implements Codec -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Separator */ - public static final byte SEPARATOR = 0x20; // SP - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Codec - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICFrameHistoric decode(byte[] bytesBuffer) throws CodecException - { - String errorMessage = ""; - BytesArray rawDataSet = null; - TICFrameHistoric ticFrame = null; - CodecTICFrameHistoricDataSet codecTICFrameHistoricDataSet = new CodecTICFrameHistoricDataSet(); - - BytesArray bytesArray = new BytesArray(bytesBuffer); - - if ((true == bytesArray.startsWith(TICFrame.BEGINNING_PATTERN)) && (true == bytesArray.endsWith(TICFrame.END_PATTERN)) && (bytesArray.contains(TICFrame.EOT) == false)) - { - bytesArray.remove(0); - bytesArray.remove(bytesArray.size() - 1); - - ticFrame = new TICFrameHistoric(); - - List datasetList = bytesArray.slice(TICFrameDataSet.BEGINNING_PATTERN, TICFrameDataSet.END_PATTERN, BytesArray.CONTIGUOUS); - - if (datasetList.isEmpty() == false) - { - for (int i = 0; i < datasetList.size(); i++) - { - TICFrameHistoricDataSet dataSet = null; - rawDataSet = datasetList.get(i); - byte[] rawDataSetByte = rawDataSet.getBytes(); - - try - { - dataSet = codecTICFrameHistoricDataSet.decode(rawDataSetByte); - ticFrame.addDataSet(dataSet); - } - catch (CodecException exception) - { - errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; - } - } - } - } - - if (errorMessage.isEmpty()) - { - return ticFrame; - } - else - { - throw new CodecException(errorMessage, ticFrame); - } - - } - - @Override - public byte[] encode(TICFrameHistoric ticFrameHistoric) throws CodecException - { - String errorMessage = ""; - CodecTICFrameHistoricDataSet codec = new CodecTICFrameHistoricDataSet(); - BytesArray dataSet = new BytesArray(); - - List ticFrameHistoricList = ticFrameHistoric.getDataSetList(); - - if (ticFrameHistoric != null && !ticFrameHistoricList.isEmpty()) - { - List groups = ticFrameHistoric.getDataSetList(); - dataSet.add(TICFrame.BEGINNING_PATTERN); - for (int i = 0; i < groups.size(); i++) - { - try - { - byte[] buff = codec.encode((TICFrameHistoricDataSet) groups.get(i)); - dataSet.addAll(buff); - } - catch (CodecException e) - { - errorMessage += e.getMessage() + " : " + new String(ticFrameHistoric.getBytes()) + "\n"; - } - } - dataSet.add(TICFrame.END_PATTERN); - } - - else - { - return null; - } - - if (errorMessage.isEmpty()) - { - return dataSet.getBytes(); - } - else - { - throw new CodecException(errorMessage, dataSet.getBytes()); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} \ No newline at end of file diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java deleted file mode 100644 index c275a52..0000000 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameHistoricDataSet.java +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import java.util.List; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoricDataSet; -import enedis.lab.types.BytesArray; - -/** - * Codec TIC Frame Historic Data Set - * - */ -public class CodecTICFrameHistoricDataSet implements Codec -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Codec - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public byte[] encode(TICFrameHistoricDataSet ticFrameHistoricDataSet) throws CodecException - { - BytesArray dataSet = new BytesArray(); - - if ((ticFrameHistoricDataSet.getLabel() != null) && (ticFrameHistoricDataSet.getData() != null)) - { - dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); - dataSet.addAll(ticFrameHistoricDataSet.getLabel().getBytes()); - - dataSet.add(TICFrameHistoricDataSet.SEPARATOR); - dataSet.addAll(ticFrameHistoricDataSet.getData().getBytes()); - - dataSet.add(TICFrameHistoricDataSet.SEPARATOR); - - if (!ticFrameHistoricDataSet.isValid()) - { - throw new CodecException("Invalid Checksum value"); - } - dataSet.add(ticFrameHistoricDataSet.getChecksum()); - dataSet.add(TICFrameDataSet.END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public TICFrameHistoricDataSet decode(byte[] bytes) throws CodecException - { - BytesArray bytesArray = new BytesArray(bytes); - TICFrameHistoricDataSet dataSet = null; - - if (this.isStartStopDelimiterPresent(bytesArray)) - { - this.removeStartStopDelimiter(bytesArray); - - } - - List parts = this.splitFrame(bytesArray); - - // Structure "LABEL/DATA/CHECKSUM" : - if (this.isStructureLabelDataChecksum(parts)) - { - - if (this.isChecksumInOneByte(parts.get(2))) - - { - dataSet = new TICFrameHistoricDataSet(); - dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); - - } - else - { - throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); - } - } - else - { - throw new CodecException("Invalid format of TICFrameHistoricDataSet"); - } - - if (dataSet.isValid() == false) - { - throw new CodecException("Invalid Checksum value - Historic"); - } - - return dataSet; - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private boolean isStartStopDelimiterPresent(BytesArray bytes) - { - return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); - } - - private void removeStartStopDelimiter(BytesArray bytes) - - { - bytes.remove(0); - bytes.remove(bytes.size() - 1); - } - - private TICFrameHistoricDataSet createDataSetLabelDataChecksum(List parts, TICFrameHistoricDataSet dataSet) - { - - dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(2).get(0)); - return dataSet; - } - - private boolean isChecksumInOneByte(BytesArray part) - { - return part.size() == 1; - } - - private boolean isStructureLabelDataChecksum(List parts) - { - return parts.size() == 3; - } - - private List splitFrame(BytesArray bytesArray) throws CodecException - { - List parts; - - if (bytesArray.size() < 5) - { - throw new CodecException("Not enough bytes in TICFrameHistoricDataSet"); - } - if (bytesArray.get(bytesArray.size() - 1) == TICFrameHistoricDataSet.SEPARATOR) - { - BytesArray subList = bytesArray.subList(0, bytesArray.size() - 2); - parts = subList.split(TICFrameHistoricDataSet.SEPARATOR); - if (parts.size() < 1) - { - throw new CodecException("Invalid format of TICFrameHistoricDataSet"); - } - BytesArray checksum = parts.get(parts.size() - 1); - checksum.addAll(new byte[] { TICFrameHistoricDataSet.SEPARATOR }); - } - else - { - parts = bytesArray.split(TICFrameHistoricDataSet.SEPARATOR); - } - - return parts; - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java deleted file mode 100644 index 3e98acb..0000000 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandard.java +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import java.util.List; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; -import enedis.lab.types.BytesArray; - -/** - * Codec TIC Frame Standard - * - */ -public class CodecTICFrameStandard implements Codec -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Codec - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICFrameStandard decode(byte[] bytes) throws CodecException - { - String errorMessage = ""; - BytesArray rawDataSet = null; - TICFrameStandard ticFrame = null; - CodecTICFrameStandardDataSet codecTICFrameStandardDataSet = new CodecTICFrameStandardDataSet(); - - BytesArray bytesArray = new BytesArray(bytes); - - // Extract the body of the frame - // NB: the presence of an EOT character makes the content of a frame invalid - if ((bytesArray.startsWith(TICFrame.BEGINNING_PATTERN) == true) && (bytesArray.endsWith(TICFrame.END_PATTERN) == true) && (bytesArray.contains(TICFrame.EOT) == false)) - { - bytesArray.remove(0); - bytesArray.remove(bytesArray.size() - 1); - - ticFrame = new TICFrameStandard(); - - // Isolate each memory area supposed to correspond to an Information Group - // (DataSet) - List datasetList = bytesArray.slice(TICFrameDataSet.BEGINNING_PATTERN, TICFrameDataSet.END_PATTERN, BytesArray.CONTIGUOUS); - - // Analyze each memory area to extract the controls of the Group of information - // If the format of a zone is invalid, then the browse is interrupted and the whole frame is invalid - // (null) - if (datasetList.isEmpty() == false) - { - for (int i = 0; i < datasetList.size(); i++) - { - TICFrameStandardDataSet dataSet = null; - rawDataSet = datasetList.get(i); - byte[] rawDataSetByte = rawDataSet.getBytes(); - try - { - dataSet = codecTICFrameStandardDataSet.decode(rawDataSetByte); - ticFrame.addDataSet(dataSet); - } - catch (CodecException exception) - { - errorMessage += exception.getMessage() + " : " + new String(rawDataSet.getBytes()) + "\n"; - } - } - } - } - - if (errorMessage.isEmpty()) - { - return ticFrame; - } - else - { - throw new CodecException(errorMessage, ticFrame); - } - } - - @Override - public byte[] encode(TICFrameStandard ticFrameStandard) throws CodecException - { - CodecTICFrameStandardDataSet codec = new CodecTICFrameStandardDataSet(); - BytesArray dataSet = new BytesArray(); - - List ticFrameStandardList = ticFrameStandard.getDataSetList(); - - if (ticFrameStandard != null && !ticFrameStandardList.isEmpty()) - { - List groups = ticFrameStandard.getDataSetList(); - dataSet.add(TICFrame.BEGINNING_PATTERN); - for (int i = 0; i < groups.size(); i++) - { - byte[] buff = codec.encode((TICFrameStandardDataSet) groups.get(i)); - dataSet.addAll(buff); - } - dataSet.add(TICFrame.END_PATTERN); - return dataSet.getBytes(); - } - else - { - return null; - } - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java deleted file mode 100644 index 9e89e85..0000000 --- a/src/main/java/enedis/lab/protocol/tic/codec/CodecTICFrameStandardDataSet.java +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import java.util.List; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandardDataSet; -import enedis.lab.types.BytesArray; - -/** - * Codec TIC Frame Standard Data Set - * - */ -public class CodecTICFrameStandardDataSet implements Codec -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Codec - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public byte[] encode(TICFrameStandardDataSet ticFrameStandardDataSet) throws CodecException - { - BytesArray dataSet = new BytesArray(); - - if ((null != ticFrameStandardDataSet.getLabel()) && (null != ticFrameStandardDataSet.getData())) - { - dataSet.add(TICFrameDataSet.BEGINNING_PATTERN); - dataSet.addAll(ticFrameStandardDataSet.getLabel().getBytes()); - - if (ticFrameStandardDataSet.checkDateTime() == true) - { - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - dataSet.addAll(ticFrameStandardDataSet.getDateTime().getBytes()); - } - - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - dataSet.addAll(ticFrameStandardDataSet.getData().getBytes()); - - dataSet.add(TICFrameStandardDataSet.SEPARATOR); - if (!ticFrameStandardDataSet.isValid()) - { - throw new CodecException("Invalid Checksum value"); - } - - dataSet.add(ticFrameStandardDataSet.getChecksum()); - dataSet.add(TICFrameDataSet.END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public TICFrameStandardDataSet decode(byte[] bytes) throws CodecException - { - BytesArray bytesArray = new BytesArray(bytes); - TICFrameStandardDataSet dataSet = null; - - if (this.isStartStopDelimiterPresent(bytesArray)) - { - this.removeStartStopDelimiter(bytesArray); - } - - List parts = bytesArray.split(TICFrameStandardDataSet.SEPARATOR); - - // Structure "LABEL/DATA/CHECKSUM" : - if (this.isStructureLabelDataChecksum(parts)) - { - if (this.isChecksumInOneByte(parts.get(2))) - { - dataSet = new TICFrameStandardDataSet(); - dataSet = this.createDataSetLabelDataChecksum(parts, dataSet); - } - else - { - throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); - } - } - // Structure "LABEL/DATETIME/DATA/CHECKSUM" : - else if (this.isStructureLabelDateTimeDataChecksum(parts)) - { - if (this.isChecksumInOneByte(parts.get(3))) - { - dataSet = new TICFrameStandardDataSet(); - dataSet = this.createDataSetLabelDatetimeDataChecksum(parts, dataSet); - } - else - { - throw new CodecException("Invalid Checksum value : The part to the Checksum must occupy one and only one byte\r\n" + "The Label part must not exceed 8 bytes "); - } - } - else - { - throw new CodecException("Invalid bytes for decode - Standard"); - } - - if (dataSet.isValid() == false) - { - throw new CodecException("Invalid Checksum value"); - } - - return dataSet; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private boolean isStructureLabelDateTimeDataChecksum(List parts) - { - return parts.size() == 4; - } - - private boolean isStructureLabelDataChecksum(List parts) - { - return parts.size() == 3; - } - - private boolean isChecksumInOneByte(BytesArray part) - { - return part.size() == 1; - } - - private void removeStartStopDelimiter(BytesArray bytes) - { - bytes.remove(0); - bytes.remove(bytes.size() - 1); - } - - private boolean isStartStopDelimiterPresent(BytesArray bytes) - { - return bytes.startsWith(TICFrameDataSet.BEGINNING_PATTERN) && bytes.endsWith(TICFrameDataSet.END_PATTERN); - } - - private TICFrameStandardDataSet createDataSetLabelDataChecksum(List parts, TICFrameStandardDataSet dataSet) - { - dataSet.setup(parts.get(0).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(2).get(0)); - return dataSet; - } - - private TICFrameStandardDataSet createDataSetLabelDatetimeDataChecksum(List parts, TICFrameStandardDataSet dataSet) - { - dataSet.setup(parts.get(0).getBytes(), parts.get(2).getBytes(), parts.get(1).getBytes()); - dataSet.setChecksum(parts.get(3).get(0)); - return dataSet; - } - -} \ No newline at end of file diff --git a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java b/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java deleted file mode 100644 index 2649dfb..0000000 --- a/src/main/java/enedis/lab/protocol/tic/codec/TICCodec.java +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.codec; - -import enedis.lab.codec.Codec; -import enedis.lab.codec.CodecException; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; -import enedis.lab.protocol.tic.frame.standard.TICException; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; -import enedis.lab.types.BytesArray; - -/** - * Codec TIC - */ -public class TICCodec implements Codec -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final TICMode DEFAULT_MODE = TICMode.STANDARD; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private BytesArray Buffer; - private TICMode mode = TICMode.UNKNOWN; - private TICMode currentMode = TICMode.UNKNOWN; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TICCodec() - { - this.mode = TICMode.UNKNOWN; - this.currentMode = TICMode.UNKNOWN; - this.Buffer = new BytesArray(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Codec - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICFrame decode(byte[] newData) throws CodecException - { - CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); - CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); - TICFrameStandard ticFrameStandard = null; - TICFrameHistoric ticFrameHistoric = null; - - TICFrame ticFrame = null; - TICMode currentTICMode = null; - - try - { - switch (this.currentMode) - { - case STANDARD: - { - ticFrameStandard = codecStandard.decode(newData); - ticFrame = ticFrameStandard; - break; - } - case HISTORIC: - { - ticFrameHistoric = codecHistoric.decode(newData); - ticFrame = ticFrameHistoric; - break; - } - - case AUTO: - { - try - { - currentTICMode = TICMode.findModeFromFrameBuffer(newData); - } - catch (TICException exception) - { - throw new CodecException("can't determinated TIC Mode"); - } - - if (currentTICMode == TICMode.STANDARD) - { - ticFrameStandard = codecStandard.decode(newData); - ticFrame = ticFrameStandard; - break; - } - else if (currentTICMode == TICMode.HISTORIC) - { - ticFrameHistoric = codecHistoric.decode(newData); - ticFrame = ticFrameHistoric; - } - else - { - throw new CodecException("can't decode TIC, unable to find TIC Modem"); - } - - } - - default: - { - /**/ - } - } - } - catch (CodecException exception) - { - throw new CodecException(exception.getMessage(), exception.getData()); - } - return ticFrame; - } - - @Override - public byte[] encode(TICFrame ticFrame) throws CodecException - { - CodecTICFrameStandard codecStandard = new CodecTICFrameStandard(); - CodecTICFrameHistoric codecHistoric = new CodecTICFrameHistoric(); - - byte[] bytesBuffer = new byte[0]; - - try - { - switch (ticFrame.getMode()) - { - case STANDARD: - { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } - case HISTORIC: - { - bytesBuffer = codecHistoric.encode((TICFrameHistoric) ticFrame); - break; - } - default: - { - bytesBuffer = codecStandard.encode((TICFrameStandard) ticFrame); - break; - } - } - } - catch (CodecException exception) - { - throw new CodecException("Can't encode TICFrame" + exception.getMessage()); - } - return bytesBuffer; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Reset buffer - */ - public void reset() - { - this.Buffer.clear(); - } - - /** - * Append data to buffer - * - * @param data - */ - public void append(byte[] data) - { - this.Buffer.addAll(data); - } - - /** - * Append data to buffer - * - * @param data - */ - public void append(byte data) - { - this.Buffer.add(data); - } - - /** - * Append data to buffer - * - * @param data - */ - public void append(BytesArray data) - { - this.Buffer.addAll(data.getBytes()); - } - - /** - * Get mode - * - * @return mode - */ - public TICMode getMode() - { - return this.mode; - } - - /** - * Set mode - * - * @param mode - */ - public void setMode(TICMode mode) - { - if (this.mode != mode) - { - this.mode = mode; - - if (TICMode.AUTO != mode) - { - this.setCurrentMode(mode); - } - - else - { - this.setCurrentMode(DEFAULT_MODE); - } - } - } - - /** - * Get current mode - * - * @return current mode - */ - public TICMode getCurrentMode() - { - return this.currentMode; - } - - /** - * Set current mode - * - * @param currentMode - */ - public void setCurrentMode(TICMode currentMode) - { - if (currentMode != this.currentMode) - { - this.currentMode = currentMode; - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java deleted file mode 100644 index ec4ebf3..0000000 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICInputStream.java +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.datastreams; - -import enedis.lab.codec.CodecException; -import enedis.lab.io.datastreams.DataInputStream; -import enedis.lab.io.datastreams.DataStreamException; -import enedis.lab.io.datastreams.DataStreamType; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.channels.ChannelTICSerialPort; -import enedis.lab.protocol.tic.codec.TICCodec; -import enedis.lab.protocol.tic.frame.TICError; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; - -/** - * TIC input stream - */ -public class TICInputStream extends DataInputStream -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Key timestamp */ - public static final String KEY_TIMESTAMP = "timestamp"; - /** Key channel */ - public static final String KEY_CHANNEL = "channel"; - /** Key data */ - public static final String KEY_DATA = "data"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected TICCodec codec; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor using configuration - * - * @param configuration - * @throws DataStreamException - */ - public TICInputStream(TICStreamConfiguration configuration) throws DataStreamException - { - super(configuration); - this.codec = new TICCodec(); - this.codec.setCurrentMode(configuration.getTicMode()); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataInputStream - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public DataDictionary read() throws DataStreamException - { - return null; - } - - @Override - public DataStreamType getType() - { - return DataStreamType.TIC; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// ChannelListener - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onDataRead(String channelName, byte[] data) - { - if (!this.notifier.getSubscribers().isEmpty()) - { - DataDictionary ticFrame = null; - try - { - ticFrame = this.decodeTICFrame(data); - if (ticFrame != null) - { - this.notifyOnDataReceived(ticFrame); - } - } - catch (DataDictionaryException exception) - { - this.logger.error(exception.getMessage(), exception); - } - catch (CodecException exception) - { - DataDictionaryBase errorDataDictionary = new DataDictionaryBase(); - - String KEY_PARTIAL_FRAME = "partialTICFrame"; - - errorDataDictionary.addKey(KEY_PARTIAL_FRAME); - - try - { - Object exceptionData = exception.getData(); - if (exceptionData != null && exceptionData instanceof TICFrame) - { - try - { - DataDictionary frameDataDictionary = ((TICFrame) exceptionData).getDataDictionary(); - if (frameDataDictionary != null) - { - errorDataDictionary.set(KEY_PARTIAL_FRAME, frameDataDictionary); - } - else - { - this.logger.warn("Frame data dictionary is null"); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } - catch (DataDictionaryException frameDataDictionaryException) - { - this.logger.warn("Can't get TICFrame data dictionary: " + frameDataDictionaryException.getMessage()); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } - else - { - this.logger.error("No frame data available"); - errorDataDictionary.set(KEY_PARTIAL_FRAME, ""); - } - } - catch (DataDictionaryException dataDictionaryException) - { - this.logger.error("Can't convert TICFrame to DataDictonary" + dataDictionaryException.getMessage()); - } - - this.logger.error(exception.getMessage() + errorDataDictionary.toString()); - this.onErrorDetected(channelName, TICError.TIC_READER_READ_FRAME_CHECKSUM_INVALID.getValue(), exception.getMessage(), errorDataDictionary); - } - } - } - - @Override - public void onDataWritten(String channelName, byte[] data) - { - // Not used - } - - @Override - public void onErrorDetected(String channelName, int errorCode, String errorMessage) - { - this.notifyOnErrorDetected(errorCode, errorMessage, null); - } - - @Override - public void onErrorDetected(String channelName, int errorCode, String errorMessage, DataDictionary errorData) - { - this.notifyOnErrorDetected(errorCode, errorMessage, errorData); - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get Mode - * - * @return TIC Mode - */ - public TICMode getMode() - { - ChannelTICSerialPort channelTIC = (ChannelTICSerialPort) this.channel; - - return channelTIC.getMode(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected DataDictionary decodeTICFrame(byte[] data) throws DataDictionaryException, CodecException - { - TICFrame ticFrame; - try - { - ticFrame = this.codec.decode(data); - } - catch (CodecException exception) - { - throw new CodecException(exception.getMessage(), exception.getData()); - } - - long timestamp = System.currentTimeMillis(); - - DataDictionaryBase decodedTICFrame = new DataDictionaryBase(); - - decodedTICFrame.set(KEY_TIMESTAMP, timestamp); - decodedTICFrame.set(KEY_CHANNEL, this.getConfiguration().getChannelName()); - decodedTICFrame.set(KEY_DATA, ticFrame); - - return decodedTICFrame; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java b/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java deleted file mode 100644 index 4003b4b..0000000 --- a/src/main/java/enedis/lab/protocol/tic/datastreams/TICStreamConfiguration.java +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.datastreams; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.io.datastreams.DataStreamConfiguration; -import enedis.lab.io.datastreams.DataStreamDirection; -import enedis.lab.io.datastreams.DataStreamType; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; - -/** - * TICStreamConfiguration class - * - * Generated - */ -public class TICStreamConfiguration extends DataStreamConfiguration -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_TIC_MODE = "ticMode"; - - private static final DataStreamType TYPE_ACCEPTED_VALUE = DataStreamType.TIC; - private static final DataStreamDirection[] DIRECTION_ACCEPTED_VALUES = { DataStreamDirection.OUTPUT, DataStreamDirection.INPUT }; - private static final TICMode TIC_MODE_DEFAULT_VALUE = TICMode.AUTO; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kTicMode; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected TICStreamConfiguration() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - this.kDirection.setAcceptedValues(DIRECTION_ACCEPTED_VALUES); - this.kChannelName.setMandatory(true); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICStreamConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICStreamConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public TICStreamConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param direction - * @param channelName - * @param ticMode - * @throws DataDictionaryException - */ - public TICStreamConfiguration(String name, DataStreamDirection direction, String channelName, TICMode ticMode) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setDirection(direction); - this.setChannelName(channelName); - this.setTicMode(ticMode); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataStreamConfiguration - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - if (!this.exists(KEY_TIC_MODE)) - { - this.setTicMode(TIC_MODE_DEFAULT_VALUE); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get tic mode - * - * @return the tic mode - */ - public TICMode getTicMode() - { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Set tic mode - * - * @param ticMode - * @throws DataDictionaryException - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException - { - this.setTicMode((Object) ticMode); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setTicMode(Object ticMode) throws DataDictionaryException - { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, true, TICMode.class); - this.keys.add(this.kTicMode); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java b/src/main/java/enedis/lab/protocol/tic/frame/TICError.java deleted file mode 100644 index db44f6e..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICError.java +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame; - -import jssc.SerialPortException; - -/** - * TICClient errors definition - */ -public enum TICError -{ - /** No error */ - NO_ERROR(0), - /** Serial port not found */ - SERIAL_PORT_NOT_FOUND(17), - /** Serial port not found */ - SERIAL_PORT_INCORRECT_SERIAL_PORT(18), - /** Serial port not found */ - SERIAL_PORT_NULL_NOT_PERMITTED(19), - /** Serial port not found */ - SERIAL_PORT_ALREADY_OPENED(20), - /** Serial port not found */ - SERIAL_PORT_SERIAL_PORT_BUSY(21), - /** Serial port not found */ - SERIAL_PORT_CONFIGURATION_FAILURE(22), - /** TIC reader default error */ - TIC_READER_DEFAULT_ERROR(23), - /** TIC reader label name not found */ - TIC_READER_LABEL_NAME_NOT_FOUND(24), - /** TIC reader frame decode failed */ - TIC_READER_FRAME_DECODE_FAILED(25), - /** TIC reader read frame timeout */ - TIC_READER_READ_FRAME_TIMEOUT(26), - /** TIC reader read frame with checksum invalid */ - TIC_READER_READ_FRAME_CHECKSUM_INVALID(27),; - - private int value; - - private TICError(int value) - { - this.value = value; - } - - /** - * Get error value - * - * @return the value - */ - public int getValue() - { - return this.value; - } - - /** - * Get serial port error - * - * @param serialPortException - * @return TICError - */ - public static TICError getSerialPortError(SerialPortException serialPortException) - { - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_BUSY)) - { - return TICError.SERIAL_PORT_SERIAL_PORT_BUSY; - } - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_INCORRECT_SERIAL_PORT)) - { - return TICError.SERIAL_PORT_INCORRECT_SERIAL_PORT; - } - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_NULL_NOT_PERMITTED)) - { - return TICError.SERIAL_PORT_NULL_NOT_PERMITTED; - } - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_NOT_FOUND)) - { - return TICError.SERIAL_PORT_NOT_FOUND; - } - if (serialPortException.getExceptionType().equals(SerialPortException.TYPE_PORT_ALREADY_OPENED)) - { - return TICError.SERIAL_PORT_ALREADY_OPENED; - } - return null; - } - -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java deleted file mode 100644 index cd89e18..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrame.java +++ /dev/null @@ -1,522 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import org.json.JSONObject; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.BytesArray; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; - -/** - * TICFrame - */ -public abstract class TICFrame -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Beginning pattern */ - public static final byte BEGINNING_PATTERN = 0x02; // STX - /** End pattern */ - public static final byte END_PATTERN = 0x03; // ETX - /** End of trame pattern */ - public static final byte EOT = 0x04; // EOT - - // Options - /** No specific options */ - public static final int EXPLICIT = 0x0000; - /** No checksum options */ - public static final int NOCHECKSUM = 0x0001; - /** No date time options */ - public static final int NODATETIME = 0x0002; - /** No tic mode options */ - public static final int NOTICMODE = 0x0004; - /** No invalid options */ - public static final int NOINVALID = 0x0008; - /** Trimmed options */ - public static final int TRIMMED = NOCHECKSUM | NODATETIME | NOINVALID; - - /** Key tic frame */ - public static final String KEY_TICFRAME = "ticFrame"; - /** Key tic mode */ - public static final String KEY_TICMODE = "ticMode"; - /** Key label */ - public static final String KEY_LABEL = "label"; - /** Key data */ - public static final String KEY_DATA = "data"; - /** Key checksum */ - public static final String KEY_CHECKSUM = "checksum"; - /** Key date time */ - public static final String KEY_DATETIME = "dateTime"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - protected List DataSetList; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TICFrame() - { - this.DataSetList = new ArrayList(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Object - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public int hashCode() - { - return Objects.hash(this.DataSetList); - } - - @Override - public boolean equals(Object obj) - { - if (this == obj) - return true; - if (obj == null) - return false; - if (this.getClass() != obj.getClass()) - return false; - TICFrame other = (TICFrame) obj; - return Objects.equals(this.DataSetList, other.DataSetList); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Return the list of DataSet - * - * @return the list of DataSet - */ - public List getDataSetList() - { - return this.DataSetList; - } - - /** - * Set the list of DataSet - * - * @param dataSetList - */ - public void setDataSetList(List dataSetList) - { - this.DataSetList = dataSetList; - } - - /** - * Return the index of the given DataSet - * - * @param label - * Label of the DataSet - * @return the index of the given DataSet or '-1' if it does not exist - */ - public int indexOf(String label) - { - int index = -1; - - for (int i = 0; i < this.DataSetList.size(); i++) - { - if (this.DataSetList.get(i).getLabel().equals(label)) - { - index = i; - break; - } - } - - return index; - } - - /** - * Return the given DataSet - * - * @param label - * Label of the DataSet - * @return the given DataSet or 'null' if it does not exist - */ - public TICFrameDataSet getDataSet(String label) - { - TICFrameDataSet dataSet = null; - - for (int i = 0; i < this.DataSetList.size(); i++) - { - if (this.DataSetList.get(i).getLabel().equals(label)) - { - dataSet = this.DataSetList.get(i); - break; - } - } - - return dataSet; - } - - /** - * Return the data field of the given DataSet - * - * @param label - * Label of the DataSet - * @return the data field of the given DataSet - */ - public String getData(String label) - { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) - { - return dataSet.getData(); - } - - else - { - return null; - } - } - - /** - * Add the dataSet at the end of the list. If the dataSet already existed, its data and checksum are just set - * - * @param dataSet - */ - public void addDataSet(TICFrameDataSet dataSet) - { - TICFrameDataSet oldDataSet = this.getDataSet(dataSet.getLabel()); - - if (oldDataSet == null) - { - this.DataSetList.add(dataSet); - } - - else - { - oldDataSet.setData(dataSet.getData()); - oldDataSet.setChecksum(dataSet.getChecksum()); - } - } - - /** - * Add the dataSet at the given index of the list. If the dataSet already existed, its data and checksum are set and - * it is moved at the given index - * - * @param index - * @param dataSet - */ - public void addDataSet(int index, TICFrameDataSet dataSet) - { - int dataSetIndex = this.indexOf(dataSet.getLabel()); - - if (dataSetIndex < 0) - { - this.DataSetList.add(index, dataSet); - } - - else - { - if (dataSetIndex != index) - { - this.removeDataSet(dataSetIndex); - this.DataSetList.add(index, dataSet); - } - - else - { - this.DataSetList.get(index).setData(dataSet.getData()); - this.DataSetList.get(index).setChecksum(dataSet.getChecksum()); - } - } - } - - /** - * Add data set - * - * @param label - * @param data - * @return tic frame data set - */ - public abstract TICFrameDataSet addDataSet(String label, String data); - - /** - * Add data set - * - * @param index - * @param label - * @param data - * @return tic frame data set - */ - public abstract TICFrameDataSet addDataSet(int index, String label, String data); - - /** - * Move the DataSet at the given index. - * - * @param label - * @param index - * @return true if the operation has been done, else return false - */ - public boolean moveDataSet(String label, int index) - { - int previous_index = this.indexOf(label); - - // Si l'ensemble des conditions suivantes sont vérifiées : - // - le Groupe d'Information existe, - // - l'index donné est cohérent - // - l'index donné diffère de l'index actuel - if ((previous_index >= 0) && (index >= 0) && (index < this.DataSetList.size()) && (index != previous_index)) - { - // Alors déplacer le Groupe d'Information à l'index donné: - - TICFrameDataSet dataSet = this.DataSetList.remove(previous_index); - - if (index < this.DataSetList.size()) - { - this.DataSetList.add(index, dataSet); - } - - else - { - this.DataSetList.add(dataSet); - } - - return true; - } - - else - { - // Sinon, l'opération n'est pas effectuée - return false; - } - } - - /** - * Set data set - * - * @param label - * @param data - * @return true if succeed - */ - public boolean setDataSet(String label, String data) - { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) - { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - return true; - } - - else - { - return false; - } - } - - /** - * Set data set - * - * @param label - * @param data - * @param checksum - * @return true if succeed - */ - public boolean setDataSet(String label, String data, byte checksum) - { - TICFrameDataSet dataSet = this.getDataSet(label); - - if (dataSet != null) - { - dataSet.setData(data); - dataSet.setChecksum(checksum); - - return true; - } - - else - { - return false; - } - } - - /** - * Remove data set - * - * @param label - * @return tic frame data set - */ - public TICFrameDataSet removeDataSet(String label) - { - TICFrameDataSet dataSet = null; - - int index = this.indexOf(label); - - if (index >= 0) - { - dataSet = this.DataSetList.remove(index); - } - - return dataSet; - } - - /** - * Remove data set - * - * @param index - * @return tic frame data set - */ - public TICFrameDataSet removeDataSet(int index) - { - TICFrameDataSet dataSet = null; - - if ((index >= 0) && (index < this.DataSetList.size())) - { - dataSet = this.DataSetList.remove(index); - } - - return dataSet; - } - - /** - * Get bytes - * - * @return bytes - */ - public byte[] getBytes() - { - BytesArray bytesFrame = new BytesArray(); - - bytesFrame.add(BEGINNING_PATTERN); - - for (int i = 0; i < this.DataSetList.size(); i++) - { - bytesFrame.addAll(this.DataSetList.get(i).getBytes()); - } - - bytesFrame.add(END_PATTERN); - - return bytesFrame.getBytes(); - } - - /** - * Get data dictionary - * - * @return data dictionary - * @throws DataDictionaryException - */ - public DataDictionary getDataDictionary() throws DataDictionaryException - { - return this.getDataDictionary(0); - } - - /** - * Return a DataDictionary object corresponding to the TIC frame - * - * @param options - * options to DataDictionary render (use like bits fields) : - EXPLICIT : frame is detailed - NOCHECKSUM - * : DataSet checksum are hidden - NODATETIME : DataSet dateTime fields are hidden - NOTICMODE : Tic mode - * is hidden; - TRIMMED : NOCHECKSUM | NODATETIME - FILTER_INVALID : Invalid DataSet are filtered - * - * - * @return data dictionary - * @throws DataDictionaryException - */ - public DataDictionary getDataDictionary(int options) throws DataDictionaryException - { - DataDictionaryBase dictionary = new DataDictionaryBase(); - - JSONObject jsonObject = new JSONObject(); - - if (0 == (options & NOTICMODE)) - { - dictionary.addKey(KEY_TICMODE); - dictionary.set(KEY_TICMODE, this.getMode().name()); - } - - for (int i = 0; i < this.DataSetList.size(); i++) - { - TICFrameDataSet dataSet = this.DataSetList.get(i); - - // Si le DataSet n'est pas valid et que l'option "filtrer les - // groupes invalides" est activée, - if ((false == dataSet.isValid()) && ((options & NOINVALID) > 0)) - { - // Ignorer le DataSet - } - - // Sinon, - else - { - jsonObject.append(KEY_TICFRAME, dataSet.toJSON(options)); - } - - } - - dictionary.addKey(KEY_TICFRAME); - dictionary.set(KEY_TICFRAME, jsonObject.get(KEY_TICFRAME)); - - return dictionary; - } - - /** - * Get mode - * - * @return mode - */ - public abstract TICMode getMode(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java deleted file mode 100644 index ccea017..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/TICFrameDataSet.java +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame; - -import java.util.Objects; - -import org.json.JSONObject; - -import enedis.lab.types.BytesArray; - -/** - * TIC frame data set - */ -public abstract class TICFrameDataSet -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Beginning pattern */ - public static final byte BEGINNING_PATTERN = 0x0A; // LF - /** End pattern */ - public static final byte END_PATTERN = 0x0D; // CR - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected BytesArray label = null; - protected BytesArray data = null; - protected byte checksum; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TICFrameDataSet() - { - this.init(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Object - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public int hashCode() - { - return Objects.hash(this.checksum, this.data, this.label); - } - - @Override - public boolean equals(Object obj) - { - if (this == obj) - return true; - if (obj == null) - return false; - if (this.getClass() != obj.getClass()) - return false; - TICFrameDataSet other = (TICFrameDataSet) obj; - return this.checksum == other.checksum && Objects.equals(this.data, other.data) && Objects.equals(this.label, other.label); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Setup from string label and data - * - * @param label - * @param data - */ - public void setup(String label, String data) - { - this.setup(label.getBytes(), data.getBytes()); - } - - /** - * Setup from byte[] label and data - * - * @param label - * @param data - */ - public void setup(byte[] label, byte[] data) - { - this.label = new BytesArray(label); - this.data = new BytesArray(data); - } - - /** - * Get label - * - * @return label - */ - public String getLabel() - { - return new String(this.label.getBytes()); - } - - /** - * Set the Label - * - * @param label - */ - public void setLabel(String label) - { - this.setLabel(label.getBytes()); - } - - /** - * Set the Label - * - * @param label - */ - public void setLabel(byte[] label) - { - this.setLabel(new BytesArray(label)); - } - - /** - * Set the Label - * - * @param label - */ - public void setLabel(BytesArray label) - { - this.label = label; - this.setChecksum(); - } - - /** - * Get data - * - * @return data - */ - public String getData() - { - return new String(this.data.getBytes()); - } - - /** - * Set data - * - * @param data - */ - public void setData(String data) - { - this.setData(data.getBytes()); - } - - /** - * Set data - * - * @param data - */ - public void setData(byte[] data) - { - this.setData(new BytesArray(data)); - } - - /** - * Set data - * - * @param data - */ - public void setData(BytesArray data) - { - this.data = data; - this.setChecksum(); - } - - /** - * Get checksum - * - * @return checksum - */ - public byte getChecksum() - { - - return this.checksum; - } - - /** - * Compute and set the checksum (NB: Label and Data shall have been set) - * - * @return true if the operation succeeds, else, return false - */ - public boolean setChecksum() - { - Byte checksum = this.getConsistentChecksum(); - - if (checksum != null) - { - this.checksum = checksum; - return true; - } - - else - { - return false; - } - } - - /** - * Set the checksum at a given value - * - * @param checksum - */ - public void setChecksum(byte checksum) - { - this.checksum = checksum; - - } - - /** - * - * @return true if fields are consistent with checksum - */ - public boolean isValid() - { - Byte consistentChecksum = this.getConsistentChecksum(); - - if ((consistentChecksum != null) && (consistentChecksum.byteValue() == (this.checksum & (byte) 0xFF))) - { - return true; - } - - else - { - return false; - } - } - - /** - * Get bytes - * - * @return bytes - */ - public abstract byte[] getBytes(); - - /** - * Conver to JSON - * - * @return json object - */ - public abstract JSONObject toJSON(); - - /** - * Conver to JSON - * - * @param option - * @return json object - */ - public abstract JSONObject toJSON(int option); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Return the consistent checksum of the DataSet - * - * @return the consistent checksum of the DataSet - */ - protected abstract Byte getConsistentChecksum(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void init() - { - this.label = null; - this.data = null; - this.checksum = -1; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java deleted file mode 100644 index 417b23b..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoric.java +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.historic; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.frame.TICFrame; - -/** - * TIC frame historic - */ -public class TICFrameHistoric extends TICFrame -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Separator */ - public static final byte SEPARATOR = 0x20; // SP - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TICFrameHistoric() - { - super(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICFrame - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICFrameHistoricDataSet addDataSet(String label, String data) - { - return this.addDataSet(this.DataSetList.size(), label, data); - } - - @Override - public TICFrameHistoricDataSet addDataSet(int index, String label, String data) - { - TICFrameHistoricDataSet dataSet = (TICFrameHistoricDataSet) this.getDataSet(label); - - if (dataSet == null) - { - dataSet = new TICFrameHistoricDataSet(); - dataSet.setLabel(label); - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - if ((index >= 0) && (index < this.DataSetList.size())) - { - this.DataSetList.add(index, dataSet); - } - - else - { - this.DataSetList.add(dataSet); - } - } - - else - { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - this.DataSetList.remove(dataSet); - - if (index >= this.DataSetList.size()) - { - this.DataSetList.add(dataSet); - } - - else - { - this.DataSetList.add(index, dataSet); - } - } - - return dataSet; - } - - @Override - public TICMode getMode() - { - return TICMode.HISTORIC; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java deleted file mode 100644 index 8c4ef34..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/historic/TICFrameHistoricDataSet.java +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.historic; - -import org.json.JSONObject; - -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.types.BytesArray; - -/** - * TIC frame historic data set - */ -public class TICFrameHistoricDataSet extends TICFrameDataSet -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Separator */ - public static final byte SEPARATOR = 0x20; // SP - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - */ - public TICFrameHistoricDataSet() - { - super(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICFrameDataSet - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Byte getConsistentChecksum() - { - Byte crc = 0; - byte[] labelByte; - byte[] dataByte; - if ((this.label != null) && (this.data != null)) - { - labelByte = this.label.getBytes(); - for (int i = 0; i < labelByte.length; i++) - { - crc = this.computeUpdate(crc, labelByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - dataByte = this.data.getBytes(); - - for (int i = 0; i < dataByte.length; i++) - { - crc = this.computeUpdate(crc, dataByte[i]); - } - - return this.computeEnd(crc); - } - else - { - return null; - } - - } - - /** - * compute Update - * - * @param crc - * @param octet - * @return Byte crc after compute update - */ - public Byte computeUpdate(Byte crc, byte octet) - { - return (byte) (crc + (octet & 0xff)); - } - - /** - * compute End - * - * @param crc - * @return Byte crc after compute end - */ - public Byte computeEnd(Byte crc) - { - return (byte) ((crc & 0x3F) + 0x20); - } - - @Override - public byte[] getBytes() - { - BytesArray dataSet = new BytesArray(); - - if ((this.label != null) && (this.data != null)) - { - dataSet.add(BEGINNING_PATTERN); - dataSet.addAll(this.label.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.addAll(this.data.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.add(this.checksum); - - dataSet.add(END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public JSONObject toJSON() - { - return this.toJSON(TICFrame.TRIMMED); - } - - @Override - public JSONObject toJSON(int option) - { - JSONObject jsonObject = new JSONObject(); - - if ((option & TICFrame.NOCHECKSUM) > 0) - { - jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); - } - - else - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - - return jsonObject; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java deleted file mode 100644 index d50abe1..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICException.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.standard; - -import enedis.lab.protocol.tic.frame.TICError; - -/** - * TICException - * - */ -public class TICException extends Exception -{ - /** - * Unique identifier used for serialization - */ - private static final long serialVersionUID = -2780151361870269473L; - private TICError error; - - /** - * Constructor TICException - */ - public TICException() - { - super(); - this.reset(); - } - - /** - * Constructor TICException - * - * @param message - */ - public TICException(String message) - { - super(message); - this.error = TICError.TIC_READER_DEFAULT_ERROR; - } - - /** - * Constructor TICException - * - * @param message - * @param readerError - */ - public TICException(String message, TICError readerError) - { - super(message); - this.error = readerError; - } - - /** - * Reset TICError - */ - public void reset() - { - this.error = TICError.TIC_READER_DEFAULT_ERROR; - } - - /** - * Get error - * - * @return the readerError - */ - public TICError getError() - { - return this.error; - } -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java deleted file mode 100644 index d23c26a..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandard.java +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.standard; - -import java.time.LocalDateTime; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.frame.TICFrame; - -/** - * TIC frame standard - */ -public class TICFrameStandard extends TICFrame -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Begin info group */ - public static final byte INFOGROUP_BEGIN = 0x03; // LF - /** End info group */ - public static final byte INFOGROUP_END = 0x03; // CR - /** Sep info group */ - public static final byte INFOGROUP_SEP = 0x03; // HT - - /** Min size info group */ - public static final int INFOGROUP_MIN_SIZE = 7; // LF + Label + HT + Data + HT + - /** MIN_SIZE */ - public static final int MIN_SIZE = INFOGROUP_MIN_SIZE + 2; // ETX + INFGROUP_MIN_SIZE + STX - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TICFrameStandard() - { - super(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICFrame - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICFrameStandardDataSet addDataSet(String label, String data) - { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - - if (dataSet == null) - { - dataSet = new TICFrameStandardDataSet(); - dataSet.setLabel(label); - dataSet.setData(data); - dataSet.getConsistentChecksum(); - } - - else - { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - } - - this.DataSetList.add(dataSet); - - return dataSet; - } - - @Override - public TICFrameStandardDataSet addDataSet(int index, String label, String data) - { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - - if (dataSet == null) - { - dataSet = new TICFrameStandardDataSet(); - dataSet.setLabel(label); - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - if ((index >= 0) && (index < this.DataSetList.size())) - { - this.DataSetList.add(index, dataSet); - } - - else - { - this.DataSetList.add(dataSet); - } - } - - else - { - dataSet.setData(data); - dataSet.getConsistentChecksum(); - - this.DataSetList.remove(dataSet); - - if (index >= this.DataSetList.size()) - { - this.DataSetList.add(dataSet); - } - - else - { - this.DataSetList.add(index, dataSet); - } - } - - return dataSet; - } - - @Override - public TICMode getMode() - { - return TICMode.STANDARD; - } - - @Override - public String getData(String label) - { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - - if (dataSet != null) - { - if (dataSet.getLabel().equals("DATE")) - { - return dataSet.getDateTime(); - } - else - { - return dataSet.getData(); - } - } - - else - { - return null; - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Add data set - * - * @param label - * @param data - * @param dateTime - * @return the added data set - */ - public TICFrameStandardDataSet addDataSet(String label, String data, String dateTime) - { - TICFrameStandardDataSet dataSet = this.addDataSet(label, data); - - dataSet.setDateTime(dateTime); - - return dataSet; - } - - /** - * Get string datetime - * - * @param label - * @return string datetime - */ - public String getDateTime(String label) - { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - if (dataSet != null) - { - return dataSet.getDateTime(); - } - else - { - return null; - } - } - - /** - * Get date time as localDateTime - * - * @param label - * @return LocalDateTime - */ - public LocalDateTime getDateTimeAsLocalDateTime(String label) - { - TICFrameStandardDataSet dataSet = (TICFrameStandardDataSet) this.getDataSet(label); - if (dataSet != null) - { - return dataSet.getDateTimeAsLocalDateTime(); - } - else - { - return null; - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java b/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java deleted file mode 100644 index 8ee0198..0000000 --- a/src/main/java/enedis/lab/protocol/tic/frame/standard/TICFrameStandardDataSet.java +++ /dev/null @@ -1,352 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.protocol.tic.frame.standard; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; - -import org.json.JSONObject; - -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.types.BytesArray; - -/** - * TIC frame standard data set - */ -public class TICFrameStandardDataSet extends TICFrameDataSet -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Separator */ - public static final byte SEPARATOR = 0x09; // HT - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected BytesArray dateTime = null; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - */ - public TICFrameStandardDataSet() - { - super(); - this.init(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICFrameDataSet - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Byte getConsistentChecksum() - { - Byte crc = 0; - byte[] labelByte; - byte[] dataByte; - byte[] dateByte; - - if ((this.label != null) && (this.data != null)) - { - labelByte = this.label.getBytes(); - for (int i = 0; i < labelByte.length; i++) - { - crc = this.computeUpdate(crc, labelByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - - if (this.dateTime != null) - { - dateByte = this.dateTime.getBytes(); - for (int i = 0; i < dateByte.length; i++) - { - crc = this.computeUpdate(crc, dateByte[i]); - } - - crc = this.computeUpdate(crc, SEPARATOR); - } - - dataByte = this.data.getBytes(); - - for (int i = 0; i < dataByte.length; i++) - { - crc = this.computeUpdate(crc, dataByte[i]); - } - crc = this.computeUpdate(crc, SEPARATOR); - - return this.computeEnd(crc); - } - else - { - return null; - } - - } - - /** - * Compute update - * - * @param crc - * @param octet - * @return Byte crc compute after Update - */ - public Byte computeUpdate(Byte crc, byte octet) - { - return (byte) (crc + (octet & 0xff)); - } - - /** - * Compute end - * - * @param crc - * @return Byte crc compute after End - */ - public Byte computeEnd(Byte crc) - { - return (byte) ((crc & 0x3F) + 0x20); - } - - @Override - public byte[] getBytes() - { - BytesArray dataSet = new BytesArray(); - - if ((this.label != null) && (this.data != null)) - { - dataSet.add(BEGINNING_PATTERN); - dataSet.addAll(this.label.getBytes()); - - if (null != this.dateTime) - { - dataSet.add(SEPARATOR); - dataSet.addAll(this.dateTime.getBytes()); - } - - dataSet.add(SEPARATOR); - dataSet.addAll(this.data.getBytes()); - - dataSet.add(SEPARATOR); - dataSet.add(this.checksum); - - dataSet.add(END_PATTERN); - } - - return dataSet.getBytes(); - } - - @Override - public JSONObject toJSON() - { - return this.toJSON(TICFrame.TRIMMED); - } - - @Override - public JSONObject toJSON(int option) - { - JSONObject jsonObject = new JSONObject(); - - if ((option & TICFrame.NOCHECKSUM) > 0) - { - if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) - { - jsonObject.put(new String(this.label.getBytes()), new String(this.data.getBytes())); - } - - else - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); - jsonObject.put(new String(this.label.getBytes()), content); - } - } - - else - { - if ((this.dateTime == null) || ((option & TICFrame.NODATETIME) > 0)) - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - - else - { - JSONObject content = new JSONObject(); - content.put(TICFrame.KEY_DATA, new String(this.data.getBytes())); - content.put(TICFrame.KEY_DATETIME, new String(this.dateTime.getBytes())); - content.put(TICFrame.KEY_CHECKSUM, this.checksum); - jsonObject.put(new String(this.label.getBytes()), content); - } - } - - return jsonObject; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Setup - * - * @param label - * @param data - * @param dateTime - */ - public void setup(String label, String data, String dateTime) - { - this.setup(label.getBytes(), data.getBytes(), dateTime.getBytes()); - } - - /** - * Setup - * - * @param label - * @param data - * @param dateTime - */ - public void setup(byte[] label, byte[] data, byte[] dateTime) - { - super.setup(label, data); - this.dateTime = new BytesArray(dateTime); - } - - /** - * Get datetime - * - * @return datetime - */ - public String getDateTime() - { - return new String(this.dateTime.getBytes()); - } - - /** - * Check datetime - * - * @return result - */ - public Boolean checkDateTime() - { - if (this.dateTime == null) - { - return false; - } - return true; - } - - /** - * Get datetime as local datetime - * - * @return datetime as local datetime - */ - // DATE H190730100158 yyMMddHHmmss - public LocalDateTime getDateTimeAsLocalDateTime() - { - String strDateTime = this.getDateTime(); - LocalDateTime localDateTime = null; - - if (strDateTime != null && strDateTime.length() == 13) - { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMddHHmmss"); - try - { - localDateTime = LocalDateTime.parse(strDateTime.substring(1), formatter); - } - catch (DateTimeParseException e) - { - localDateTime = null; - } - } - - return localDateTime; - } - - /** - * Set date time - * - * @param dateTime - */ - public void setDateTime(String dateTime) - { - this.setDateTime(dateTime.getBytes()); - } - - /** - * Set datetime - * - * @param dateTime - */ - public void setDateTime(byte[] dateTime) - { - this.setDateTime(new BytesArray(dateTime)); - } - - /** - * Set date time - * - * @param dateTime - */ - public void setDateTime(BytesArray dateTime) - { - this.dateTime = dateTime; - this.setChecksum(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void init() - { - this.dateTime = null; - } -} diff --git a/src/main/java/enedis/lab/types/BytesArray.java b/src/main/java/enedis/lab/types/BytesArray.java deleted file mode 100644 index fb1cd50..0000000 --- a/src/main/java/enedis/lab/types/BytesArray.java +++ /dev/null @@ -1,1280 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; - -/** - * Bytes array - */ -public class BytesArray implements List -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Default option */ - public static final int DEFAULT_OPTIONS = 0x0000; - /** Greedy option */ - public static final int GREEDY = 0x0001; - /** Contiguous option */ - public static final int CONTIGUOUS = 0x0002; - /** Remove patterns option */ - public static final int REMOVE_PATTERNS = 0x0004; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Convert int to byte[] - * - * @param value - * @param numberOfByte - * @return byte[] - * @throws NumberFormatException - */ - public static byte[] intToArrayOfByte(int value, int numberOfByte) throws NumberFormatException - { - if (countBytes(value) > numberOfByte) - { - throw new NumberFormatException("Value " + value + " can't be converted in a byte[" + numberOfByte + "]"); - } - - byte[] byteArray = new byte[numberOfByte]; - - for (int i = 0; i < byteArray.length; i++) - { - if (i < Integer.BYTES) - { - byteArray[byteArray.length - i - 1] = (byte) (value >> (i * 8)); - } - } - - return byteArray; - } - - /** - * Conver int to BytesArray - * - * @param value - * @param numberOfByte - * @return BytesArray - * @throws Exception - */ - public static BytesArray valueOf(int value, int numberOfByte) throws Exception - { - return new BytesArray(intToArrayOfByte(value, numberOfByte)); - } - - private static int countBytes(int value) - { - int tmp = Math.abs(value); - int count = 0; - - for (int i = 0; i < Integer.BYTES; i++) - { - tmp = tmp >> 8; - count++; - if (tmp == 0) - { - break; - } - } - return count; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected byte[] buffer; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public BytesArray() - { - this.buffer = new byte[0]; - } - - /** - * Constructor from array of byte - * - * @param bytesArray - */ - public BytesArray(byte[] bytesArray) - { - this.buffer = bytesArray.clone(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// List - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public int size() - { - return this.buffer.length; - } - - @Override - public boolean isEmpty() - { - return (0 == this.buffer.length) ? true : false; - } - - @Override - public boolean contains(Object element) - { - int index = this.indexOf(element); - return (index >= 0) ? true : false; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public Iterator iterator() - { - throw new UnsupportedOperationException(); - } - - @Override - public Byte[] toArray() - { - Byte[] bytesArray = new Byte[this.buffer.length]; - - for (int i = 0; i < this.buffer.length; i++) - { - bytesArray[i] = this.buffer[i]; - } - - return bytesArray; - } - - @SuppressWarnings("unchecked") - @Override - public Byte[] toArray(Object[] a) - { - throw new UnsupportedOperationException(); - } - - @Override - public boolean add(Byte element) - { - byte byte_element = element; - byte[] bytesArray = new byte[this.buffer.length + 1]; - - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); - bytesArray[bytesArray.length - 1] = byte_element; - this.buffer = bytesArray; - - return true; - } - - @Override - public void add(int index, Byte element) - { - if ((index < 0) || (index >= this.buffer.length)) - { - this.add(element); - } - - else - { - byte[] bytesArray = new byte[this.buffer.length + 1]; - - // Si l'index est le premier - if (0 == index) - { - System.arraycopy(this.buffer, 0, bytesArray, 1, this.buffer.length); - } - - // Sinon, si l'index est le dernier - else if ((this.buffer.length - 1) == index) - { - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length - 1); - - bytesArray[bytesArray.length - 1] = this.buffer[this.buffer.length - 1]; - } - - // Sinon, si l'index est au milieu du tableau - else - { - System.arraycopy(this.buffer, 0, bytesArray, 0, index - 1); - - System.arraycopy(this.buffer, index, // src , src_pos - bytesArray, index + 1, // dest, dest_pos - this.buffer.length - index - 1); // size - } - - bytesArray[index] = element; - this.buffer = bytesArray; - } - } - - @Override - public boolean remove(Object element) - { - boolean success = true; - - if ((null != element) && (element instanceof Number)) - { - // Obtenir l'index de la première occurence de l'élément - int index = this.indexOf(element); - - // Si un tel élément a été trouvé, - if (index >= 0) - { - this.removeIndex(index); - } - - // Sinon, - else - { - success = false; - } - } - - else - { - success = false; - } - - return success; - } - - @Override - public Byte remove(int index) - { - Byte element = null; - - if ((index >= 0) && (index < this.buffer.length)) - { - element = new Byte(this.buffer[index]); - this.removeIndex(index); - } - - else - { - // Aucune action - } - - return element; - } - - /** - * Remove the buffer from an index to other - * - * @param fromIndex - * @param toIndex - * @return new length of buffer - */ - public int remove(int fromIndex, int toIndex) - { - int length = 0; - - if ((fromIndex >= 0) && (toIndex < this.buffer.length) && (toIndex > fromIndex)) - { - this.removeSubList(fromIndex, toIndex); - length = toIndex - fromIndex + 1; - } - - return length; - } - - @Override - public boolean containsAll(Collection c) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean addAll(Collection collection) - { - boolean hasChanged; - - if ((null != collection) && (collection.size() > 0)) - { - Object[] collectionArray = collection.toArray(); - byte[] bytesArray = new byte[this.buffer.length + collection.size()]; - - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); - - int j = this.buffer.length; - for (int i = 0; i < collectionArray.length; i++) - { - bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); - j++; - } - - this.buffer = bytesArray; - - hasChanged = true; - } - - else - { - hasChanged = false; - } - - return hasChanged; - } - - @Override - public boolean addAll(int index, Collection collection) - { - boolean hasChanged; - - if ((null != collection) && (collection.size() > 0) && (index >= 0) && (index <= this.buffer.length)) - { - Object[] collectionArray = collection.toArray(); - byte[] bytesArray = new byte[this.buffer.length + collection.size()]; - - // Cas ou on ajoute en début: - if (0 == index) - { - for (int i = 0; i < collectionArray.length; i++) - { - bytesArray[i] = ((Byte) collectionArray[i]).byteValue(); - } - - System.arraycopy(this.buffer, 0, bytesArray, collectionArray.length, this.buffer.length); - } - - // Cas où on ajoute à la fin: - else if (this.buffer.length == index) - { - System.arraycopy(this.buffer, 0, bytesArray, 0, this.buffer.length); - - int j = index; - for (int i = 0; i < collectionArray.length; i++) - { - bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); - j++; - } - } - - // Cas où on ajoute au milieu: - else - { - System.arraycopy(this.buffer, 0, bytesArray, 0, index); - - int j = index; - for (int i = 0; i < collectionArray.length; i++) - { - bytesArray[j] = ((Byte) collectionArray[i]).byteValue(); - j++; - } - - System.arraycopy(this.buffer, index, bytesArray, index + collectionArray.length, this.buffer.length - index); - } - - this.buffer = bytesArray; - - hasChanged = true; - } - - else - { - hasChanged = false; - } - - return hasChanged; - } - - @Override - public boolean removeAll(Collection c) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean retainAll(Collection c) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public void clear() - { - this.buffer = new byte[0]; - } - - @Override - public Byte get(int index) - { - Byte element = null; - - if ((index >= 0) && (index < this.buffer.length)) - { - element = new Byte(this.buffer[index]); - } - - else - { - - } - - return element; - } - - @Override - public Byte set(int index, Byte element) - { - Byte previous_element = null; - - if ((index >= 0) && (index < this.buffer.length)) - { - previous_element = new Byte(this.buffer[index]); - this.buffer[index] = element; - } - - else - { - previous_element = new Byte(this.buffer[this.buffer.length - 1]); - this.buffer[this.buffer.length - 1] = element; - } - - return new Byte(previous_element); - } - - @Override - public int indexOf(Object element) - { - return this.indexOf(element, 0); - } - - /** - * Get the buffer element of the given index - * - * @param element - * @param offset - * @return the element - */ - public int indexOf(Object element, int offset) - { - int index = -1; - - if ((null != element) && (element instanceof Number) && offset >= 0 && offset < this.buffer.length) - { - Byte byte_element = ((Number) element).byteValue(); - - for (int i = offset; i < this.buffer.length; i++) - { - if (this.buffer[i] == byte_element) - { - index = i; - break; - } - } - } - - else - { - // Aucune action - } - - return index; - } - - @Override - public int lastIndexOf(Object element) - { - int index = -1; - - if ((null != element) && (element instanceof Number)) - { - Byte byte_element = ((Number) element).byteValue(); - - for (int i = this.buffer.length - 1; i >= 0; i--) - { - if (this.buffer[i] == byte_element) - { - index = i; - break; - } - } - } - - else - { - // Aucune action - } - - return index; - } - - @Override - public ListIterator listIterator() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public ListIterator listIterator(int index) - { - // TODO Auto-generated method stub - return null; - } - - @Override - public BytesArray subList(int fromIndex, int toIndex) - { - BytesArray subBytesArray = new BytesArray(new byte[0]); - - if (fromIndex >= 0) - { - if ((toIndex > fromIndex) && (toIndex < this.buffer.length)) - { - int size = (toIndex - fromIndex) + 1; - byte[] buffer = new byte[size]; - - System.arraycopy(this.buffer, fromIndex, buffer, 0, size); - - subBytesArray = new BytesArray(buffer); - } - - else if ((toIndex < 0) || (toIndex >= this.buffer.length)) - { - int size = this.buffer.length - fromIndex; - byte[] buffer = new byte[size]; - - System.arraycopy(this.buffer, fromIndex, buffer, 0, size); - - subBytesArray = new BytesArray(buffer); - } - - else - { - - } - } - - return subBytesArray; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get bytes - * - * @return bytes - */ - public byte[] getBytes() - { - return this.buffer; - } - - /** - * Copy - * - * @return copied BytesArray - */ - public BytesArray copy() - { - return new BytesArray(this.buffer); - } - - /** - * Get index of element - * - * @param element - * @return index of element - */ - public List indexesOf(Object element) - { - List indexesList = new ArrayList(); - - if ((null != element) && (element instanceof Number)) - { - Byte byte_element = ((Number) element).byteValue(); - - for (int i = 0; i < this.buffer.length; i++) - { - if (this.buffer[i] == byte_element) - { - indexesList.add(i); - } - } - } - - else - { - // Aucune action - } - - return indexesList; - } - - /** - * - * @param value - * @return true if the BytesArray starts with the given value, else return false - */ - public boolean startsWith(byte value) - { - boolean retVal; - - if ((this.buffer.length > 0) && (this.buffer[0] == value)) - { - retVal = true; - } - - else - { - retVal = false; - } - - return retVal; - } - - /** - * - * @param value - * @return true if the BytesArray ends with the given value, else return false - */ - public boolean endsWith(byte value) - { - boolean retVal; - - if ((this.buffer.length > 0) && (this.buffer[this.buffer.length - 1] == value)) - { - retVal = true; - } - - else - { - retVal = false; - } - - return retVal; - } - - /** - * Get element count - * - * @param element - * @return element count - */ - public int count(Object element) - { - return this.indexesOf(element).size(); - } - - /** - * Split - * - * @param pattern - * @return split BytesArray - */ - public List split(Number pattern) - { - List patternsIndexesList = this.indexesOf(pattern.byteValue()); - List bytesArraysList = new ArrayList(); - - // Si le motif de séparation existe, copier chaque segment dans l'élément associé du tableau - if (false == patternsIndexesList.isEmpty()) - { - int begin_index = 0; - int end_index; - int segment_length; - int nb_segments = patternsIndexesList.size() + 1; - - for (int i = 0; i < nb_segments; i++) - { - end_index = (i < patternsIndexesList.size()) ? patternsIndexesList.get(i) : this.buffer.length; - - segment_length = end_index - begin_index; - - // Si la taille du segment est non nulle, - if (segment_length > 0) - { - byte[] segment = new byte[segment_length]; - - System.arraycopy(this.buffer, begin_index, segment, 0, segment_length); - - bytesArraysList.add(new BytesArray(segment)); - } - - // Sinon, - else - { - bytesArraysList.add(new BytesArray()); - } - - begin_index = end_index + 1; - } - } - - // Sinon, copier dans l'unique élément du tableau l'intégralité des données - else - { - bytesArraysList.add(new BytesArray(this.buffer)); - } - - return bytesArraysList; - } - - /** - * Extract parts in the ByteArray according specific 'begin' and 'end' patterns.
    - * NB : 'begin' and 'end' patterns shall be different. Use split() method if they don't.
    - *
    - * Example:
    - *
    - * BytesArray myArray(new byte[12] {42,14,20,98,34,47,14,61,17,98,110,25});
    - * List mySlicedArray = myArray.slice(14,98);
    - * mySlicedArray.get(0) contains {14,20,98}
    - * mySlicedArray.get(1) contains {14,61,17,98}
    - * - * @param beginPattern - * @param endPattern - * @return slices list - */ - public List slice(Number beginPattern, Number endPattern) - { - return this.slice(beginPattern, endPattern, -1, 0); - } - - /** - * - * @param beginPattern - * @param endPattern - * @param options - * @return slices list - */ - public List slice(Number beginPattern, Number endPattern, int options) - { - return this.slice(beginPattern, endPattern, -1, options); - } - - /** - * - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return slices list - */ - public List slice(Number beginPattern, Number endPattern, int occurences, int options) - { - List bytesArraysList; - - // ... Si l'appel de la fonction est "gourmand" (greedy), il n'y aura au mieux qu'un élément à retourner qui - // correspondra au plus grand segment [begin_pattern, end_pattern] du tableau - // NB le paramètre 'occurences' est alors non signifiant - if ((options & GREEDY) != 0) - { - bytesArraysList = this.sliceGreedy(beginPattern, endPattern, options); - } - - // ... Si l'appel de la fonction est "non gourmand" (not greedy), les éléments du tableau à retourner - // correspondront aux segments [begin_pattern, end_pattern] - // les plus rationnels, dans la limite des n premières occurences demandées - else - { - bytesArraysList = this.sliceNotGreedy(beginPattern, endPattern, occurences, options); - } - - return bytesArraysList; - } - - /** - * Add all byte - * - * @param bytesArray - * @return true if changed has been applied - */ - public boolean addAll(byte[] bytesArray) - { - boolean hasChanged = false; - - if (bytesArray != null && bytesArray.length > 0) - { - byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; - - // copie des données existantes - System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); - - // copie des nouvelles données - System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); - - this.buffer = newBytesArray; - hasChanged = true; - } - - else - { - hasChanged = false; - } - - return hasChanged; - } - - /** - * Add all byte at index - * - * @param index - * @param bytesArray - * @return true if changed has been applied - */ - public boolean addAll(int index, byte[] bytesArray) - { - boolean hasChanged = false; - - if ((index >= 0) && (index <= this.buffer.length)) - { - byte[] newBytesArray = new byte[this.buffer.length + bytesArray.length]; - - // Cas ou on ajoute au début: - if (0 == index) - { - System.arraycopy(bytesArray, 0, newBytesArray, 0, bytesArray.length); - - // copie des données existantes - System.arraycopy(this.buffer, 0, newBytesArray, bytesArray.length, this.buffer.length); - } - - // Cas où on ajoute à la fin: - else if (this.buffer.length == index) - { - // copie des données existantes - System.arraycopy(this.buffer, 0, newBytesArray, 0, this.buffer.length); - - // copie des nouvelles données - System.arraycopy(bytesArray, 0, newBytesArray, this.buffer.length, bytesArray.length); - } - - // Cas où on ajoute au milieu: - else - { - // copie des données existantes (premier segment) - System.arraycopy(this.buffer, 0, newBytesArray, 0, index); - - // copie des nouvelles données - System.arraycopy(bytesArray, 0, newBytesArray, index, bytesArray.length); - - // copie des données existantes (deuxième segment) - System.arraycopy(this.buffer, index, newBytesArray, index + bytesArray.length, this.buffer.length - index); - } - - this.buffer = newBytesArray; - hasChanged = true; - } - - else - { - hasChanged = false; - } - - return hasChanged; - } - - /** - * Compute the BytesArray checksum on 8bits (Byte) - * - * @return the computed checksum or 'null' if the BytesArray is empty - * - */ - public Byte checksum8() - { - Byte checksum = null; - - if (this.buffer.length > 0) - { - byte checksum_8 = 0; - - for (int i = 0; i < this.buffer.length; i++) - { - checksum_8 += this.buffer[i]; - } - - checksum = new Byte(checksum_8); - } - - else - { - - } - - return checksum; - } - - /** - * Compute the BytesArray checksum on 16 bits (Short) - * - * @return the computed checksum or 'null' if the BytesArray is empty - * - */ - public Short checksum16() - { - Short checksum = null; - - if (this.buffer.length > 0) - { - short checksum_16 = 0; - - for (int i = 0; i < this.buffer.length; i++) - { - checksum_16 += this.buffer[i]; - } - - checksum = new Short(checksum_16); - } - - else - { - - } - - return checksum; - } - - /** - * Compute the BytesArray checksum on 32 bits (Integer) - * - * @return the computed checksum or 'null' if the BytesArray is empty - * - */ - public Integer checksum32() - { - Integer checksum = null; - - if (this.buffer.length > 0) - { - int checksum_32 = 0; - - for (int i = 0; i < this.buffer.length; i++) - { - checksum_32 += this.buffer[i]; - } - - checksum = new Integer(checksum_32); - } - - else - { - - } - - return checksum; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * - * @param beginPattern - * @param endPattern - * @param options - * @return - */ - private List sliceGreedy(Number beginPattern, Number endPattern, int options) - { - List bytesArraysList = new ArrayList(); - - int beginIndex = this.indexOf(beginPattern.byteValue()); - - if (beginIndex >= 0) - { - int endIndex = this.lastIndexOf(endPattern.byteValue()); - - if (endIndex > 0) - { - if ((options & REMOVE_PATTERNS) != 0) - { - beginIndex++; - endIndex--; - } - - int size = endIndex - beginIndex + 1; - - if (size > 0) - { - byte[] bytesBuffer = new byte[size]; - - System.arraycopy(this.buffer, beginIndex, bytesBuffer, 0, size); - - bytesArraysList.add(new BytesArray(bytesBuffer)); - } - - else - { - bytesArraysList.add(new BytesArray(new byte[0])); - } - } - } - - else - { - - } - - return bytesArraysList; - } - - /** - * - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return - */ - private List sliceNotGreedy(Number beginPattern, Number endPattern, int occurences, int options) - { - List bytesArraysList = new ArrayList(); - - // Si la contiguité des segments est requise, - if ((options & CONTIGUOUS) != 0) - { - bytesArraysList = this.sliceContiguous(beginPattern, endPattern, occurences, options); - } - - else - { - bytesArraysList = this.sliceNotContiguous(beginPattern, endPattern, occurences, options); - } - - return bytesArraysList; - } - - /** - * - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return - */ - private List sliceContiguous(Number beginPattern, Number endPattern, int occurences, int options) - { - List bytesArraysList = new ArrayList(); - - int beginIndex = -1; - int endIndex = -1; - - for (int i = 0; i < this.buffer.length; i++) - { - // Traiter un motif DEBUT - if (this.buffer[i] == beginPattern.byteValue()) - { - if ((-1 == endIndex) || (this.buffer[i - 1] == endPattern.byteValue())) - { - beginIndex = i; - } - } - - // Traiter un motif FIN - else if (this.buffer[i] == endPattern.byteValue()) - { - // Si nous ne sommes pas au dernier élément du tableau - if (i < (this.buffer.length - 1)) - { - // Si le prochain caractère est le motif de début de trame - if (this.buffer[i + 1] == beginPattern.byteValue()) - { - endIndex = i; - - this.slicePart(bytesArraysList, beginIndex, endIndex, options); - - // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, - // interrompre le traitement - if ((occurences > 0) && (bytesArraysList.size() >= occurences)) - { - break; - } - } - } - - // Sinon, si nous sommes au dernier élément du tableau - else - { - endIndex = i; - - this.slicePart(bytesArraysList, beginIndex, endIndex, options); - - // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, - // interrompre le traitement - if ((occurences > 0) && (bytesArraysList.size() >= occurences)) - { - break; - } - } - } - - else - { - - } - } - - return bytesArraysList; - } - - /** - * - * @param beginPattern - * @param endPattern - * @param occurences - * @param options - * @return - */ - private List sliceNotContiguous(Number beginPattern, Number endPattern, int occurences, int options) - { - List bytesArraysList = new ArrayList(); - - int beginIndex = -1; - int endIndex = -1; - - for (int i = 0; i < this.buffer.length; i++) - { - if (this.buffer[i] == beginPattern.byteValue()) - { - beginIndex = i; - } - - else if (this.buffer[i] == endPattern.byteValue()) - { - endIndex = i; - - this.slicePart(bytesArraysList, beginIndex, endIndex, options); - - // Si un nombre d'occurences est spécifié et si le nombre de segments l'a déjà atteint, - // interrompre le traitement - if ((occurences > 0) && (bytesArraysList.size() >= occurences)) - { - break; - } - } - - else - { - - } - } - - return bytesArraysList; - } - - /** - * - * @param bytesArraysList - * @param beginIndex - * @param endIndex - * @param options - */ - private void slicePart(List bytesArraysList, int beginIndex, int endIndex, int options) - { - if (beginIndex >= 0) - { - if ((options & REMOVE_PATTERNS) != 0) - { - beginIndex++; - endIndex--; - } - - BytesArray part = this.subList(beginIndex, endIndex); - - if (null != part) - { - bytesArraysList.add(part); - } - } - } - - /** - * @param index - */ - private void removeIndex(int index) - { - byte[] bytesArray = new byte[this.buffer.length - 1]; - - // Si l'index est le premier du tableau - if (0 == index) - { - System.arraycopy(this.buffer, 1, bytesArray, 0, bytesArray.length); - } - - // Sinon, si l'index est le dernier - else if ((this.buffer.length - 1) == index) - { - System.arraycopy(this.buffer, 0, bytesArray, 0, bytesArray.length); - } - - // Sinon, si l'index est au milieu du tableau - else - { - System.arraycopy(this.buffer, 0, bytesArray, 0, index); - - System.arraycopy(this.buffer, index + 1, // src , src_pos - bytesArray, index, // dest, dest_pos - bytesArray.length - index); // size - } - - this.buffer = bytesArray; - } - - private void removeSubList(int fromIndex, int toIndex) - { - byte[] bytesArray = new byte[this.buffer.length - (toIndex - fromIndex + 1)]; - - if (fromIndex > 0) - { - System.arraycopy(this.buffer, 0, bytesArray, 0, fromIndex); - } - if (toIndex + 1 < this.buffer.length) - { - System.arraycopy(this.buffer, toIndex + 1, bytesArray, fromIndex, bytesArray.length - fromIndex); - } - - this.buffer = bytesArray; - } -} diff --git a/src/main/java/enedis/lab/types/DataArrayList.java b/src/main/java/enedis/lab/types/DataArrayList.java deleted file mode 100644 index a6b267c..0000000 --- a/src/main/java/enedis/lab/types/DataArrayList.java +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.RandomAccess; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Resizable-array implementation of the {@code DataList} interface. - * - * @param the type of elements in this data list - */ -public class DataArrayList extends ArrayList implements DataList -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = 3116080161929203526L; - - private static final int JSON_INDENT_FACTOR = 0; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Returns a fixed-size data list backed by the specified array. (Changes to - * the returned data list "write through" to the array.) This method acts - * as bridge between array-based and collection-based APIs, in - * combination with {@link Collection#toArray}. The returned list is - * serializable and implements {@link RandomAccess}. - * - *

    This method also provides a convenient way to create a fixed-size - * data list initialized to contain several elements: - *

    -     *     DataList<String> stooges = DataArrayList.asList("Larry", "Moe", "Curly");
    -     * 
    - * - * @param the class of the objects in the array - * @param items the array by which the data list will be backed - * @return a data list view of the specified array - */ - @SafeVarargs - public static DataList asList(E... items) - { - DataList dataList = new DataArrayList(); - - for(E item : items) - { - dataList.add(item); - } - - return dataList; - } - - /** - * Instantiate a data list from a File - * - * @param the class of the objects in the list - * @param file - * @param clazz - * - * @return a data list - * @throws JSONException - * @throws IOException - */ - public static DataList fromFile(File file,Class clazz) throws JSONException, IOException - { - InputStream stream = new FileInputStream(file); - - return fromStream(stream,clazz); - } - - /** - * Instantiate a data list from a Stream - * - * @param the class of the objects in the list - * @param stream - * @param clazz - * - * @return a data list - * @throws JSONException - * @throws IOException - */ - public static DataList fromStream(InputStream stream,Class clazz) throws JSONException, IOException - { - byte[] buffer = new byte[stream.available()]; - stream.read(buffer); - stream.close(); - String text = new String(buffer); - - return fromString(text,clazz); - } - - /** - * Instantiate a data list from a String - * - * @param the class of the objects in the list - * @param text - * @param clazz - * - * @return a data list - * @throws JSONException - */ - public static DataList fromString(String text,Class clazz) throws JSONException - { - if (text == null) - { - return null; - } - - JSONArray jsonArray = new JSONArray(text); - - return fromJSON(jsonArray,clazz); - } - - /** - * Instantiate a data list from a JSONObject - * - * @param the class of the objects in the list - * @param jsonArray - * @param clazz - * - * @return a data list - * @throws JSONException - */ - public static DataList fromJSON(JSONArray jsonArray, Class clazz) throws JSONException - { - if(jsonArray == null) - { - return null; - } - DataList dataList = new DataArrayList(); - for(int i = 0; i < jsonArray.length(); i++) - { - Object jsonItem = jsonArray.get(i); - E dataListItem; - try - { - if(JSONObject.NULL.equals(jsonItem)) - { - dataListItem = null; - } - else if(clazz.isAssignableFrom(jsonItem.getClass())) - { - dataListItem = clazz.cast(jsonItem); - } - else if(Enum.class.isAssignableFrom(clazz)) - { - dataListItem = convertToEnum(jsonItem,clazz); - } - else if(LocalDateTime.class.isAssignableFrom(clazz)) - { - dataListItem = convertToLocalDateTime(jsonItem,clazz); - } - else if(List.class.isAssignableFrom(clazz)) - { - dataListItem = convertToDataList(jsonItem,clazz); - } - else if(DataDictionary.class.isAssignableFrom(clazz)) - { - dataListItem = convertToDataDictionary(jsonItem,clazz); - } - else - { - throw new IllegalArgumentException("Cannot convert " + jsonItem.getClass().getName() + " to " + clazz.getName()); - } - } - catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) - { - throw new IllegalArgumentException("Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getMessage() + ")",e); - } - catch (InvocationTargetException e) - { - throw new IllegalArgumentException("Cannot convert " + jsonItem + " to " + clazz.getName() + " (" + e.getTargetException().getMessage() + ")",e); - } - dataList.add(dataListItem); - } - - return dataList; - } - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructs an empty data list with an initial capacity of ten. - */ - public DataArrayList() - { - super(); - } - - /** - * Constructs a data list containing the elements of the specified - * collection, in the order they are returned by the collection's - * iterator. - * - * @param collection the collection whose elements are to be placed into this data list - * @throws NullPointerException if the specified collection is null - */ - public DataArrayList(Collection collection) - { - super(collection); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public JSONArray toJSON() - { - JSONArray jsonArray = new JSONArray(); - - for(E item : this) - { - Object objectItem; - if(item instanceof DataDictionary) - { - DataDictionary dictionaryItem = (DataDictionary)item; - objectItem = dictionaryItem.toJSON(); - } - else - { - objectItem = item; - } - jsonArray.put(objectItem); - } - - return jsonArray; - } - - @Override - public void toFile(File file, int indentFactor) throws IOException - { - OutputStream stream = new FileOutputStream(file); - - this.toStream(stream, indentFactor); - } - - @Override - public void toStream(OutputStream stream, int indentFactor) throws IOException - { - String text = this.toString(indentFactor); - - stream.write(text.getBytes()); - } - - @Override - public String toString() - { - return this.toString(JSON_INDENT_FACTOR); - } - - @Override - public String toString(int identFactor) - { - return this.toJSON().toString(identFactor); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @SuppressWarnings("unchecked") - private static E convertToDataDictionary(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException - { - E result; - - if(value instanceof JSONObject) - { - Method method = clazz.getMethod("fromJSON",JSONObject.class,Class.class); - result = (E) method.invoke(null,value,clazz); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToDataList(Object value,Class clazz) - { - E result; - - if(value instanceof JSONArray) - { - result = (E) DataArrayList.fromJSON((JSONArray)value, Object.class); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToEnum(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException - { - E result; - - if(value instanceof String) - { - Method method = clazz.getDeclaredMethod("valueOf", String.class); - result = (E) method.invoke(null, (String)value); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } - - @SuppressWarnings("unchecked") - private static E convertToLocalDateTime(Object value,Class clazz) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException - { - E result; - - if(value instanceof String) - { - Method method = clazz.getDeclaredMethod("parse", CharSequence.class); - result = (E) method.invoke(null, (String)value); - } - else - { - throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + clazz.getName()); - } - - return result; - } -} diff --git a/src/main/java/enedis/lab/types/DataDictionary.java b/src/main/java/enedis/lab/types/DataDictionary.java deleted file mode 100644 index 56f3bfe..0000000 --- a/src/main/java/enedis/lab/types/DataDictionary.java +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Map; - -import org.json.JSONObject; - -/** - * Interface used to handle data dictionaries - */ -public interface DataDictionary extends Cloneable -{ - /** - * Check key exists - * - * @param key - * Key to check - * - * @return true if key exists, false otherwise - */ - public boolean exists(String key); - - /** - * Get the datadictionary current map keys list - * - * @return the datadictionary current map keys list - */ - public String[] keys(); - - /** - * Check if the datadictionary map contains the given key - * - * @param key - * @return true if the datadictionary map contains the given key - */ - public boolean existsInKeys(String key); - - /** - * Add the given key in the datadictionary map, if the given key already exists, do nothing - * - * @param key - */ - public void addKey(String key); - - /** - * Remove the given key in the datadictionary map - * - * @param key - */ - public void removeKey(String key); - - /** - * Clear the datadictionary map - */ - public void clear(); - - /** - * Get value of the given key - * - * @param key - * @return value of the given key - */ - public Object get(String key); - - /** - * Set value of the given key - * - * @param key - * @param value - * @throws DataDictionaryException - */ - public void set(String key, Object value) throws DataDictionaryException; - - /** - * Copy an other datadictionary into this one - * - * @param other - * @throws DataDictionaryException - */ - public void copy(DataDictionary other) throws DataDictionaryException; - - /** - * Convert this dataditionary to JSON - * - * @return a JSONObject - */ - public JSONObject toJSON(); - - /** - * Convert this dataditionary to Map - * - * @return a JSONObject - */ - public Map toMap(); - - /** - * Convert the given datadictionary to a File - * - * @param file - * @param indentFactor - * @throws IOException - */ - public void toFile(File file, int indentFactor) throws IOException; - - /** - * Convert the given datadictionary to a Stream - * - * @param stream - * @param indentFactor - * @throws IOException - */ - public void toStream(OutputStream stream, int indentFactor) throws IOException; - - @Override - public String toString(); - - /** - * Convert this dataditionary to a String - * - * @param indentFactor - * @return a JSONObject - */ - public String toString(int indentFactor); - - /** - * Convert this dataditionary to Map - * - * @return a JSONObject - * @throws CloneNotSupportedException - */ - public Object clone() throws CloneNotSupportedException; - - /** - * Print this datactionary in the console - */ - public void print(); -} diff --git a/src/main/java/enedis/lab/types/DataDictionaryException.java b/src/main/java/enedis/lab/types/DataDictionaryException.java deleted file mode 100644 index 0d2f088..0000000 --- a/src/main/java/enedis/lab/types/DataDictionaryException.java +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -/** - * Data dictionary exception - */ -public class DataDictionaryException extends Exception -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -7967428428453584771L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public DataDictionaryException() - { - super(); - } - - /** - * Constructor using message, cause, enable suppression flag and writable stack trace flag - * - * @param message - * @param cause - * @param enableSuppression - * @param writableStackTrace - */ - public DataDictionaryException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) - { - super(message, cause, enableSuppression, writableStackTrace); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public DataDictionaryException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public DataDictionaryException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public DataDictionaryException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/types/DataList.java b/src/main/java/enedis/lab/types/DataList.java deleted file mode 100644 index 12eed5f..0000000 --- a/src/main/java/enedis/lab/types/DataList.java +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.List; - -import org.json.JSONArray; -/** - * Interface extending Java standard List to provide JSON serialization mechanism - * - * @param the type of elements in this list - */ -public interface DataList extends List -{ - /** - * Write this data list to a File - * - * @param file the output file - * @param indentFactor JSON indentation factor - * @throws IOException if writing file fails - */ - public void toFile(File file, int indentFactor) throws IOException; - - /** - * Write this data list to a Stream - * - * @param stream the output stream - * @param indentFactor JSON indentation factor - * @throws IOException if writing stream fails - */ - public void toStream(OutputStream stream, int indentFactor) throws IOException; - - @Override - public String toString(); - /** - * Convert this data list to a String - * - * @param indentFactor JSON indentation factor - * @return JSON string representation - */ - public String toString(int indentFactor); - - /** - * Convert this data list to JSON array - * - * @return JSON array - */ - public JSONArray toJSON(); -} diff --git a/src/main/java/enedis/lab/types/ExceptionBase.java b/src/main/java/enedis/lab/types/ExceptionBase.java deleted file mode 100644 index 99e3e7f..0000000 --- a/src/main/java/enedis/lab/types/ExceptionBase.java +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types; - -/** - * Exception base - */ -@SuppressWarnings("serial") -public class ExceptionBase extends Exception -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected int code; - protected String info; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param code - * @param info - */ - public ExceptionBase(int code, String info) - { - this.setErrorCode(code); - this.setErrorInfo(info); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Exception - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public String getMessage() - { - return this.info + " (" + this.code + ")"; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get error code - * - * @return error code - */ - public int getErrorCode() - { - return this.code; - } - - /** - * Set error code - * - * @param errorCode - */ - public void setErrorCode(int errorCode) - { - this.code = errorCode; - } - - /** - * Get error info - * - * @return error info - */ - public String getErrorInfo() - { - return this.info; - } - - /** - * Set error info - * - * @param errorInfo - */ - public void setErrorInfo(String errorInfo) - { - this.info = errorInfo; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/types/configuration/Configuration.java b/src/main/java/enedis/lab/types/configuration/Configuration.java deleted file mode 100644 index 46939e9..0000000 --- a/src/main/java/enedis/lab/types/configuration/Configuration.java +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.configuration; - -import java.io.File; - -import enedis.lab.types.DataDictionary; - -/** - * Interface used to handle any configuration - */ -public interface Configuration extends DataDictionary -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Load configuration from file - * - * @throws ConfigurationException - * load file failed - */ - public void load() throws ConfigurationException; - - /** - * Save configuration to file - * - * @throws ConfigurationException - * save file failed - */ - public void save() throws ConfigurationException; - - /** - * Get config name - * - * @return configuration name - */ - public String getConfigName(); - - /** - * Get config file - * - * @return configuration file reference - */ - public File getConfigFile(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java b/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java deleted file mode 100644 index be58331..0000000 --- a/src/main/java/enedis/lab/types/configuration/ConfigurationBase.java +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.configuration; - -import java.io.File; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; - -/** - * Basic implementation of Configuration - */ -public class ConfigurationBase extends DataDictionaryBase implements Configuration -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private String name = null; - private File file = null; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ConfigurationBase() - { - super(); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ConfigurationBase(DataDictionary other) throws DataDictionaryException - { - super(other); - } - - /** - * Constructor using Map - * - * @param map - * @throws DataDictionaryException - */ - public ConfigurationBase(Map map) throws DataDictionaryException - { - super(map); - } - - /** - * Constructor using name and file - * - * @param name - * @param file - */ - public ConfigurationBase(String name, File file) - { - super(); - this.init(name, file); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Configuration - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void load() throws ConfigurationException - { - DataDictionary configuration; - - try - { - configuration = DataDictionaryBase.fromFile(this.file, this.getClass()); - } - catch (Exception exception) - { - throw new ConfigurationException("Cannot load configuration '" + ((this.name == null) ? "" : this.name) + "' from file '" + ((this.file == null) ? "" : this.file) - + "' (" + exception.getMessage() + ") " + exception.getClass().getSimpleName()); - } - try - { - this.copy(configuration); - } - catch (Exception exception) - { - throw new ConfigurationException("Cannot copy configuration '" + ((this.name == null) ? "" : this.name) + "' from file '" + ((this.file == null) ? "" : this.file) - + "' (" + exception.getMessage() + ")"); - } - } - - @Override - public void save() throws ConfigurationException - { - try - { - this.toFile(this.file, 2); - } - catch (Exception exception) - { - throw new ConfigurationException("Cannot save configuration '" + ((this.name == null) ? "" : this.name) + "' to file '" + ((this.file == null) ? "" : this.file) + "' (" - + exception.getMessage() + ")"); - } - } - - @Override - public String getConfigName() - { - return this.name; - } - - @Override - public File getConfigFile() - { - return this.file; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Set config name - * - * @param configName - */ - public void setConfigName(String configName) - { - this.name = configName; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void init(String name, File file) - { - this.name = name; - this.file = file; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java b/src/main/java/enedis/lab/types/configuration/ConfigurationException.java deleted file mode 100644 index 3e435e2..0000000 --- a/src/main/java/enedis/lab/types/configuration/ConfigurationException.java +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.configuration; - -import enedis.lab.types.DataDictionaryException; - -/** - * Configuration exception - */ -public class ConfigurationException extends DataDictionaryException -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = 8090072693974075297L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Create missing parameter exception - * - * @param parameter - * @return configurationException - */ - public static ConfigurationException createMissingParameterException(String parameter) - { - return new ConfigurationException("Parameter \'" + parameter + "\' is missing"); - } - - /** - * Create unknown parameter exception - * - * @param parameter - * @return configurationException - */ - public static ConfigurationException createUnknownParameterException(String parameter) - { - return new ConfigurationException("Parameter \'" + parameter + "\' is unknown"); - } - - /** - * Create invalid parameter value exception - * - * @param parameter - * @param info - * @return configurationException - */ - public static ConfigurationException createInvalidParameterValueException(String parameter, String info) - { - return new ConfigurationException("Parameter \'" + parameter + "\' has an invalid value : " + info); - } - - /** - * Create invalid paramter type exception - * - * @param parameter - * @param type - * @return configurationException - */ - public static ConfigurationException createInvalidParameterTypeException(String parameter, Class type) - { - return new ConfigurationException("Parameter \'" + parameter + "\' has an invalid type : " + type.getName()); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public ConfigurationException() - { - super(); - } - - /** - * Constructor using message, cause, enable suppression flag and writable stack trace flag - * - * @param message - * @param cause - * @param enableSuppression - * @param writableStackTrace - */ - public ConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) - { - super(message, cause, enableSuppression, writableStackTrace); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public ConfigurationException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public ConfigurationException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public ConfigurationException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java b/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java deleted file mode 100644 index 02ca337..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/DataDictionaryBase.java +++ /dev/null @@ -1,723 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.DataList; - -/** - * DataDictionary basic implementation - */ -public class DataDictionaryBase implements DataDictionary -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final int JSON_INDENT_FACTOR = 0; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Instantiate a datadictionary from a File - * - * @param file - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - * @throws IOException - */ - public static DataDictionary fromFile(File file, Class clazz) throws JSONException, DataDictionaryException, IOException - { - if (file == null) - { - throw new DataDictionaryException("Can't load file from null"); - } - - InputStream stream = new FileInputStream(file); - - return DataDictionaryBase.fromStream(stream, clazz); - } - - /** - * Instantiate a datadictionary from a Stream - * - * @param stream - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - * @throws IOException - */ - public static DataDictionary fromStream(InputStream stream, Class clazz) throws JSONException, DataDictionaryException, IOException - { - if (stream == null) - { - return null; - } - - byte[] buffer = new byte[stream.available()]; - stream.read(buffer); - stream.close(); - String text = new String(buffer); - - return DataDictionaryBase.fromString(text, clazz); - } - - /** - * Instantiate a datadictionary from a String - * - * @param text - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromString(String text, Class clazz) throws JSONException, DataDictionaryException - { - if (text == null) - { - return null; - } - - JSONObject jsonObject = new JSONObject(text); - - return DataDictionaryBase.fromJSON(jsonObject, clazz); - } - - /** - * Instantiate a datadictionary from a JSONObject - * - * @param jsonObject - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromJSON(JSONObject jsonObject, Class clazz) throws JSONException, DataDictionaryException - { - if (jsonObject == null) - { - return null; - } - - return fromMap(jsonObject.toMap(), clazz); - } - - /** - * Instantiate a datadictionary from a Map - * - * @param map - * @param clazz - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromMap(Map map, Class clazz) throws DataDictionaryException - { - if (map == null) - { - return null; - } - if (clazz == null) - { - return fromMap(map); - } - DataDictionary dataDictionnary; - try - { - Constructor constructor = clazz.getConstructor(Map.class); - dataDictionnary = constructor.newInstance(map); - } - catch (InvocationTargetException exception) - { - throw new DataDictionaryException("Cannot create instance of " + clazz.getName() + " : " + exception.getTargetException().getMessage()); - } - catch (Exception exception) - { - throw new DataDictionaryException("Cannot create instance of " + clazz.getName() + " : " + exception.getMessage()); - } - - return dataDictionnary; - } - - /** - * Instantiate a datadictionary from a Map - * - * @param map - * - * @return a datadictionary - * @throws JSONException - * @throws DataDictionaryException - */ - public static DataDictionary fromMap(Map map) throws DataDictionaryException - { - if (map == null) - { - return null; - } - DataDictionaryBase dataDictionnary = new DataDictionaryBase(); - for (String key : map.keySet()) - { - Object value = map.get(key); - if (value instanceof JSONObject) - { - JSONObject jsonObjectValue = (JSONObject) value; - DataDictionary dataDictionaryValue = fromMap(jsonObjectValue.toMap()); - dataDictionnary.set(key, dataDictionaryValue); - } - else if (value instanceof JSONArray) - { - JSONArray jsonArrayValue = (JSONArray) value; - DataList listValue = new DataArrayList(jsonArrayValue.toList()); - dataDictionnary.set(key, listValue); - } - else - { - dataDictionnary.set(key, value); - } - } - - return dataDictionnary; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keyDescriptors; - protected HashMap data; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public DataDictionaryBase() - { - this.init(); - } - - /** - * Constructor using map and setting key not defined allowed flag - * - * @param map - * @throws DataDictionaryException - */ - public DataDictionaryBase(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using dataDictionary and setting key not defined allowed flag - * - * @param other - * @throws DataDictionaryException - */ - public DataDictionaryBase(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionnary - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public final boolean exists(String key) - { - return this.data.containsKey(key); - } - - @Override - public final boolean existsInKeys(String key) - { - // @formatter:off - return this.keyDescriptors.stream() - .filter(k -> k.getName().equals(key)) - .findAny() - .isPresent(); - // @formatter:on - } - - @Override - public final String[] keys() - { - Set setKeys = this.data.keySet(); - String[] keys = new String[setKeys.size()]; - - setKeys.toArray(keys); - - return keys; - } - - @Override - public final void addKey(String key) - { - if (key != null) - { - this.data.putIfAbsent(key, null); - } - } - - @Override - public final void removeKey(String key) - { - if (key != null) - { - this.data.remove(key); - } - } - - @Override - public final Object get(String key) - { - return this.data.get(key); - } - - @Override - public final void set(String key, Object value) throws DataDictionaryException - { - if (key == null) - { - throw new DataDictionaryException("Key null not allowed"); - } - - if (this.keyDescriptors.isEmpty()) - { - this.data.put(key, value); - } - else - { - Optional> keyDescriptor = this.getKeyDescriptor(key); - if (keyDescriptor.isPresent()) - { - try - { - String setterName = this.getSetterName(key); - // @formatter:off - List methodNameList = Arrays.asList(this.getClass().getDeclaredMethods()) - .stream() - .map(m -> m.getName()) - .collect(Collectors.toList()); - // @formatter:on - if (methodNameList.contains(setterName)) - { - Method method = this.getClass().getDeclaredMethod(setterName, Object.class); - method.setAccessible(true); - method.invoke(this, value); - } - else - { - this.data.put(key, keyDescriptor.get().convert(value)); - } - } - catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) - { - throw new DataDictionaryException("Set key " + key + " failed : " + e.getClass().getSimpleName() + " -> " + e.getMessage()); - } - catch (InvocationTargetException e) - { - throw new DataDictionaryException( - "Set key " + key + " failed : " + e.getTargetException().getClass().getSimpleName() + " -> " + e.getTargetException().getMessage()); - } - - } - else - { - throw new DataDictionaryException("Key " + key + " not allowed"); - } - } - } - - @Override - public void copy(DataDictionary other) throws DataDictionaryException - { - String[] otherKeys = other.keys(); - - this.clear(); - for (int i = 0; i < otherKeys.length; i++) - { - this.set(otherKeys[i], other.get(otherKeys[i])); - } - - this.checkAndUpdate(); - } - - @Override - public final void clear() - { - this.data.clear(); - } - - @Override - public final int hashCode() - { - final int prime = 31; - int result = 1; - result = prime * result + ((this.data == null) ? 0 : this.data.hashCode()); - return result; - } - - @Override - public final boolean equals(Object object) - { - if (object == null) - { - return false; - } - if (this == object) - { - return true; - } - if (!(object instanceof DataDictionary)) - { - return false; - } - DataDictionaryBase other = (DataDictionaryBase) object; - - if (this.data == null) - { - if (other.data != null) - { - return false; - } - } - else - { - if (!this.data.keySet().equals(other.data.keySet())) - { - return false; - } - for (String key : this.data.keySet()) - { - Object thisValue = this.data.get(key); - Object otherValue = other.data.get(key); - - if (thisValue == null) - { - if (otherValue != null) - { - return false; - } - else - { - continue; - } - } - else if (otherValue == null) - { - return false; - } - if (!thisValue.getClass().equals(otherValue.getClass())) - { - if ((thisValue instanceof Number) && (otherValue instanceof Number)) - { - double thisDoubleValue = ((Number) thisValue).doubleValue(); - double otherDoubleValue = ((Number) otherValue).doubleValue(); - return thisDoubleValue == otherDoubleValue; - } - else - { - return false; - } - } - if (thisValue.getClass().isArray()) - { - if (thisValue instanceof boolean[]) - { - if (!Arrays.equals((boolean[]) thisValue, (boolean[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof byte[]) - { - if (!Arrays.equals((byte[]) thisValue, (byte[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof char[]) - { - if (!Arrays.equals((char[]) thisValue, (char[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof short[]) - { - if (!Arrays.equals((short[]) thisValue, (short[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof int[]) - { - if (!Arrays.equals((int[]) thisValue, (int[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof long[]) - { - if (!Arrays.equals((long[]) thisValue, (long[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof float[]) - { - if (!Arrays.equals((float[]) thisValue, (float[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof double[]) - { - if (!Arrays.equals((double[]) thisValue, (double[]) otherValue)) - { - return false; - } - } - else if (thisValue instanceof Object[]) - { - if (!Arrays.equals((Object[]) thisValue, (Object[]) otherValue)) - { - return false; - } - } - } - else - { - if (!thisValue.equals(otherValue)) - { - return false; - } - } - } - } - - return true; - } - - @Override - public final void toFile(File file, int indentFactor) throws IOException - { - OutputStream stream = new FileOutputStream(file); - - this.toStream(stream, indentFactor); - } - - @Override - public final void toStream(OutputStream stream, int indentFactor) throws IOException - { - String text = this.toString(indentFactor); - - stream.write(text.getBytes()); - } - - @Override - public JSONObject toJSON() - { - JSONObject jsonObject = new JSONObject(); - String[] keys = this.keys(); - - for (int i = 0; i < keys.length; i++) - { - Object value = this.get(keys[i]); - if (value instanceof DataDictionaryBase) - { - DataDictionaryBase dataDictionary = (DataDictionaryBase) value; - jsonObject.put(keys[i], dataDictionary.toJSON()); - } - else if (value instanceof LocalDateTime) - { - String textValue = ((LocalDateTime) value).format(KeyDescriptorLocalDateTime.DEFAULT_FORMATTER); - jsonObject.put(keys[i], textValue); - } - else - { - jsonObject.put(keys[i], value); - } - } - - return jsonObject; - } - - @SuppressWarnings("unchecked") - @Override - public final Map toMap() - { - return (Map) this.data.clone(); - } - - @Override - public String toString() - { - return this.toString(DataDictionaryBase.JSON_INDENT_FACTOR); - } - - @Override - public String toString(int identFactor) - { - return this.toJSON().toString(identFactor); - } - - @Override - public DataDictionaryBase clone() - { - try - { - Constructor constructor = this.getClass().getConstructor(DataDictionary.class); - return this.getClass().cast(constructor.newInstance(this)); - } - catch (Exception e) - { - throw new RuntimeException(e.getMessage(), e); - } - } - - @Override - public void print() - { - System.out.println(this.toString(2)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected final void checkAndUpdate() throws DataDictionaryException - { - this.updateOptionalParameters(); - this.checkMandatoryParameters(); - } - - protected void checkMandatoryParameters() throws DataDictionaryException - { - for (KeyDescriptor key : this.keyDescriptors) - { - if (key.isMandatory() && !this.exists(key.getName())) - { - throw new DataDictionaryException("Mandatory key " + key.getName() + " not defined"); - } - } - } - - protected void updateOptionalParameters() throws DataDictionaryException - { - } - - protected void addKeyDescriptor(KeyDescriptor keyDescriptor) throws DataDictionaryException - { - if (this.existsInKeys(keyDescriptor.getName())) - { - throw new DataDictionaryException("Key " + keyDescriptor.getName() + "already exists"); - } - this.keyDescriptors.add(keyDescriptor); - } - - protected void addAllKeyDescriptor(List> keyDescriptor) throws DataDictionaryException - { - for (KeyDescriptor key : keyDescriptor) - { - this.addKeyDescriptor(key); - } - } - - protected Optional> getKeyDescriptor(String name) - { - // @formatter:off - return this.keyDescriptors.stream() - .filter(k -> k.getName().equals(name)) - .findFirst(); - // @formatter:on - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private String getSetterName(String key) - { - return "set" + key.substring(0, 1).toUpperCase() + key.substring(1); - } - - private void init() - { - this.keyDescriptors = new ArrayList>(); - this.data = new HashMap(); - } - -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java deleted file mode 100644 index 1596b47..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptor.java +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor interface - * - * @param - */ -public interface KeyDescriptor -{ - /** - * Get key name - * - * @return key name - */ - public String getName(); - - /** - * Get mandatory flag - * - * @return mandatory flag - */ - public boolean isMandatory(); - - /** - * Set mandatory flag - * - * @param mandatory - */ - public void setMandatory(boolean mandatory); - - /** - * Set a list of accepted values - * - * @param acceptedValues - */ - @SuppressWarnings("unchecked") - public void setAcceptedValues(T... acceptedValues); - - /** - * Convert a Object value to a T value - * - * @param value - * object to convert - * @return value converted to T type - * @throws DataDictionaryException - */ - public T convert(Object value) throws DataDictionaryException; - - /** - * Convert a T value to String - * - * @param value - * value to convert to String - * @return String representation of the value - */ - public String toString(T value); -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java deleted file mode 100644 index cac21c7..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorBase.java +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.util.Arrays; -import java.util.List; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor base - * - * @param - */ -public abstract class KeyDescriptorBase implements KeyDescriptor -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private String name; - private boolean mandatory; - protected List acceptedValues; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor setting all attributes - * - * @param name - * @param mandatory - */ - public KeyDescriptorBase(String name, boolean mandatory) - { - super(); - this.name = name; - this.mandatory = mandatory; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// KeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public final T convert(Object value) throws DataDictionaryException - { - T convertedValue = null; - - if (value == null) - { - this.handleNullValue(); - return null; - } - - convertedValue = this.convertValue(value); - - this.checkAcceptedValues(convertedValue); - - return convertedValue; - } - - @Override - public String toString(T value) - { - if (value == null) - { - return null; - } - - return value.toString(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get key name - * - * @return key name - */ - @Override - public String getName() - { - return this.name; - } - - /** - * Set key name - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Get mandatory flag - * - * @return mandatory flag - */ - @Override - public boolean isMandatory() - { - return this.mandatory; - } - - /** - * Set mandatory flag - * - * @param mandatory - */ - @Override - public void setMandatory(boolean mandatory) - { - this.mandatory = mandatory; - } - - /** - * Set mandatory value - * - * @param acceptedValues - */ - @SuppressWarnings("unchecked") - @Override - public void setAcceptedValues(T... acceptedValues) - { - this.acceptedValues = Arrays.asList(acceptedValues); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected abstract T convertValue(Object value) throws DataDictionaryException; - - protected final void handleNullValue() throws DataDictionaryException - { - if (this.isMandatory()) - { - throw new DataDictionaryException("Cannot set null " + this.getName()); - } - } - - protected void checkAcceptedValues(T value) throws DataDictionaryException - { - if (this.acceptedValues != null) - { - boolean accepted = false; - - for (T v : this.acceptedValues) - { - if (v.equals(value)) - { - accepted = true; - break; - } - } - - if (!accepted) - { - throw new DataDictionaryException("Key " + this.getName() + " doesn't respect mandatory value"); - } - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java deleted file mode 100644 index 397464d..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorDataDictionary.java +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor DataDictionary - * - * @param - */ -public class KeyDescriptorDataDictionary extends KeyDescriptorBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Class dataDictionaryClass; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param dataDictionaryClass - */ - public KeyDescriptorDataDictionary(String name, boolean mandatory, Class dataDictionaryClass) - { - super(name, mandatory); - this.dataDictionaryClass = dataDictionaryClass; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @SuppressWarnings("unchecked") - @Override - public T convertValue(Object value) throws DataDictionaryException - { - T convertedValue = null; - - if (this.dataDictionaryClass.isAssignableFrom(value.getClass())) - { - convertedValue = this.dataDictionaryClass.cast(value); - } - else if (value instanceof String) - { - convertedValue = (T) DataDictionaryBase.fromString((String) value, this.dataDictionaryClass); - } - else if (value instanceof DataDictionary) - { - try - { - Constructor constructor = this.dataDictionaryClass.getConstructor(DataDictionary.class); - convertedValue = constructor.newInstance((DataDictionary) value); - } - catch (InvocationTargetException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getTargetException().getMessage()); - } - catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getMessage()); - } - } - else if (value instanceof Map) - { - try - { - Constructor constructor = this.dataDictionaryClass.getConstructor(Map.class); - convertedValue = constructor.newInstance((Map) value); - } - catch (InvocationTargetException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getTargetException().getMessage()); - } - catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " - + this.dataDictionaryClass.getSimpleName() + " : " + e.getMessage()); - } - } - else - { - throw new DataDictionaryException( - "Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + this.dataDictionaryClass.getSimpleName()); - } - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java deleted file mode 100644 index aeffc8f..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorEnum.java +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor Enum - * - * @param - */ -@SuppressWarnings("rawtypes") -public class KeyDescriptorEnum extends KeyDescriptorBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Class enumClass; - private String prefix; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param enumClass - */ - public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass) - { - this(name, mandatory, enumClass, ""); - } - - /** - * Constructor setting all parameters - * - * @param name - * @param mandatory - * @param enumClass - * @param prefix - */ - public KeyDescriptorEnum(String name, boolean mandatory, Class enumClass, String prefix) - { - super(name, mandatory); - this.enumClass = enumClass; - this.prefix = prefix; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public T convertValue(Object value) throws DataDictionaryException - { - T convertedValue = null; - - if (this.enumClass.isAssignableFrom(value.getClass())) - { - convertedValue = this.enumClass.cast(value); - } - else if (value instanceof String) - { - convertedValue = this.toEnum((String) value); - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to " + this.enumClass.getSimpleName()); - } - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @SuppressWarnings("unchecked") - protected T toEnum(String value) throws DataDictionaryException - { - T convertedValue = null; - try - { - String preparedValue = this.prefix + value.toUpperCase().replace(".", "_").replace("-", "_"); - convertedValue = (T) Enum.valueOf(this.enumClass, preparedValue); - } - catch (IllegalArgumentException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": " + this.enumClass.getSimpleName() + " doesn't contain " + value); - } - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java deleted file mode 100644 index 464997d..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorList.java +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.json.JSONObject; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor List - * - * @param - */ -public class KeyDescriptorList extends KeyDescriptorBase> -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Class itemClass; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param itemClass - */ - public KeyDescriptorList(String name, boolean mandatory, Class itemClass) - { - super(name, mandatory); - this.itemClass = itemClass; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// KeyDescriptor> - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public List convertValue(Object value) throws DataDictionaryException - { - List convertedValue = new ArrayList(); - - if (this.itemClass.isAssignableFrom(value.getClass())) - { - T convertedItem = this.convertItem(value); - convertedValue.add(convertedItem); - } - else if (value instanceof HashMap) - { - T convertedItem = this.convertItem(value); - convertedValue.add(convertedItem); - } - else if (value instanceof List) - { - for (Object item : (List) value) - { - T convertedItem = this.convertItem(item); - convertedValue.add(convertedItem); - } - } - else if (value instanceof Object[]) - { - for (Object item : (Object[]) value) - { - T convertedItem = this.convertItem(item); - convertedValue.add(convertedItem); - } - } - else - { - throw new DataDictionaryException( - "Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to List<" + this.itemClass.getSimpleName() + ">"); - } - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private T convertItem(Object item) throws DataDictionaryException - { - T convertedItem = null; - if (this.itemClass.isAssignableFrom(item.getClass())) - { - convertedItem = this.itemClass.cast(item); - } - else if (DataDictionary.class.isAssignableFrom(this.itemClass)) - { - convertedItem = this.convertDataDictionaryItem(item); - } - else if (Enum.class.isAssignableFrom(this.itemClass)) - { - convertedItem = this.convertEnumItem(item); - } - else - { - throw new DataDictionaryException( - "Key " + this.getName() + ": at least on item isn't a " + this.itemClass.getSimpleName() + ", type received :" + item.getClass().getSimpleName()); - } - return convertedItem; - } - - private T convertDataDictionaryItem(Object item) throws DataDictionaryException - { - T convertedItem = null; - if (item instanceof JSONObject) - { - JSONObject itemJsonObject = (JSONObject) item; - try - { - Constructor constructor = this.itemClass.getConstructor(Map.class); - convertedItem = (T) constructor.newInstance(itemJsonObject.toMap()); - } - catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) - { - e.printStackTrace(); - } - catch (InvocationTargetException e) - { - e.printStackTrace(); - } - } - else if (item instanceof Map) - { - try - { - Constructor constructor = this.itemClass.getConstructor(Map.class); - convertedItem = (T) constructor.newInstance((Map) item); - } - catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) - { - e.printStackTrace(); - } - catch (InvocationTargetException e) - { - e.printStackTrace(); - } - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": at least on item isn't a JSONObject, type received :" + item.getClass().getSimpleName()); - } - return convertedItem; - } - - @SuppressWarnings("unchecked") - private T convertEnumItem(Object item) throws DataDictionaryException - { - T convertedItem = null; - if (item instanceof String) - { - String itemString = (String) item; - try - { - Method valueOf = this.itemClass.getMethod("valueOf", String.class); - convertedItem = (T) valueOf.invoke(null, itemString.toUpperCase()); - } - catch (SecurityException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) - { - e.printStackTrace(); - } - catch (InvocationTargetException e) - { - e.printStackTrace(); - } - - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": at least on item isn't a JSONObject, type received :" + item.getClass().getSimpleName()); - } - return convertedItem; - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java deleted file mode 100644 index 92b47e7..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorListMinMaxSize.java +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.util.List; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.MinMaxChecker; - -/** - * DataDictionary key descriptor Number min max - * - * @param - */ -public class KeyDescriptorListMinMaxSize extends KeyDescriptorList -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private MinMaxChecker minMaxChecker; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param itemClass - */ - public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass) - { - this(name, mandatory, itemClass, null, null); - } - - /** - * Constructor setting all attributes - * - * @param name - * @param mandatory - * @param itemClass - * @param min - * @param max - */ - public KeyDescriptorListMinMaxSize(String name, boolean mandatory, Class itemClass, Integer min, Integer max) - { - super(name, mandatory, itemClass); - this.minMaxChecker = new MinMaxChecker(min, max); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public List convertValue(Object value) throws DataDictionaryException - { - List convertedValue = super.convertValue(value); - - this.check(convertedValue); - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get min - * - * @return min - */ - public Integer getMin() - { - return this.minMaxChecker.getMin().intValue(); - } - - /** - * Set min - * - * @param min - * @throws IllegalArgumentException - * if min is greater than max - */ - public void setMin(Integer min) - { - this.minMaxChecker.setMin(min); - } - - /** - * Get max - * - * @return max - */ - public Integer getMax() - { - return this.minMaxChecker.getMax().intValue(); - } - - /** - * Set max - * - * @param max - * @throws IllegalArgumentException - * if max is smaller than min - */ - public void setMax(Integer max) - { - this.minMaxChecker.setMax(max); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void check(List list) throws DataDictionaryException - { - if (list != null) - { - if (!this.minMaxChecker.check(list.size())) - { - throw new DataDictionaryException("Key " + this.getName() + ": value size (" + list.size() + ") out of bound"); - } - } - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java deleted file mode 100644 index 7f868dd..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorLocalDateTime.java +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor LocalDateTime - */ -public class KeyDescriptorLocalDateTime extends KeyDescriptorBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Default pattern used */ - public static final String DEFAULT_PATTERN = "dd/MM/yyyy HH:mm:ss"; - /** Default formatter used */ - public static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_PATTERN); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private DateTimeFormatter formatter; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorLocalDateTime(String name, boolean mandatory) - { - this(name, mandatory, DEFAULT_PATTERN); - } - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param formatterPattern - */ - public KeyDescriptorLocalDateTime(String name, boolean mandatory, String formatterPattern) - { - super(name, mandatory); - this.formatter = DateTimeFormatter.ofPattern(formatterPattern); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// KeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public String toString(LocalDateTime value) - { - if (value == null) - { - return null; - } - - return this.formatter.format(value); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// KeyDescriptorBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public LocalDateTime convertValue(Object value) throws DataDictionaryException - { - LocalDateTime convertedValue = null; - - if (value instanceof LocalDateTime) - { - convertedValue = (LocalDateTime) value; - } - else if (value instanceof String) - { - convertedValue = this.toLocalDateTime((String) value); - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to LocalDateTime"); - } - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private LocalDateTime toLocalDateTime(String value) throws DataDictionaryException - { - LocalDateTime convertedValue = null; - try - { - convertedValue = LocalDateTime.parse(value, this.formatter); - } - catch (DateTimeParseException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": string " + value + " cannot be converted to LocalDateTime"); - } - return convertedValue; - } - -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java deleted file mode 100644 index c455a86..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumber.java +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor Number - */ -public class KeyDescriptorNumber extends KeyDescriptorBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorNumber(String name, boolean mandatory) - { - super(name, mandatory); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Number convertValue(Object value) throws DataDictionaryException - { - Number convertedValue = null; - - if (value instanceof Number) - { - convertedValue = (Number) value; - } - else if (value instanceof String) - { - convertedValue = this.toNumber((String) value); - } - else - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to Number"); - } - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void checkAcceptedValues(Number value) throws DataDictionaryException - { - if (this.acceptedValues != null) - { - boolean accepted = false; - - for(Number v : this.acceptedValues) - { - if(v.doubleValue() == value.doubleValue()) - { - accepted = true; - break; - } - } - - if(!accepted) - { - throw new DataDictionaryException("Key " + this.getName() + " doesn't respect mandatory value"); - } - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Number toNumber(String value) throws DataDictionaryException - { - Number out = null; - try - { - out = Double.valueOf((String) value); - } - catch (NumberFormatException e) - { - throw new DataDictionaryException("Key " + this.getName() + ": Cannot convert type " + value.getClass().getSimpleName() + " to Number"); - } - return out; - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java deleted file mode 100644 index ce295f3..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorNumberMinMax.java +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.MinMaxChecker; - -/** - * DataDictionary key descriptor Number min max - */ -public class KeyDescriptorNumberMinMax extends KeyDescriptorNumber -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private MinMaxChecker minMaxChecker; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorNumberMinMax(String name, boolean mandatory) - { - this(name, mandatory, null, null); - } - - /** - * Constructor setting all attributes - * - * @param name - * @param mandatory - * @param min - * @param max - */ - public KeyDescriptorNumberMinMax(String name, boolean mandatory, Number min, Number max) - { - super(name, mandatory); - this.minMaxChecker = new MinMaxChecker(min, max); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Number convertValue(Object value) throws DataDictionaryException - { - Number convertedValue = super.convertValue(value); - - this.check(convertedValue); - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get min - * - * @return min - */ - public Number getMin() - { - return this.minMaxChecker.getMin(); - } - - /** - * Set min - * - * @param min - * @throws IllegalArgumentException - * if min is greater than max - */ - public void setMin(Number min) - { - this.minMaxChecker.setMin(min); - } - - /** - * Get max - * - * @return max - */ - public Number getMax() - { - return this.minMaxChecker.getMax(); - } - - /** - * Set max - * - * @param max - * @throws IllegalArgumentException - * if max is smaller than min - */ - public void setMax(Number max) - { - this.minMaxChecker.setMax(max); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void check(Number convertedValue) throws DataDictionaryException - { - if (convertedValue != null) - { - if (!this.minMaxChecker.check(convertedValue)) - { - throw new DataDictionaryException("Key " + this.getName() + ": value (" + convertedValue + ") out of bound"); - } - } - } -} diff --git a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java b/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java deleted file mode 100644 index a7ca434..0000000 --- a/src/main/java/enedis/lab/types/datadictionary/KeyDescriptorString.java +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.types.datadictionary; - -import enedis.lab.types.DataDictionaryException; - -/** - * DataDictionary key descriptor String - */ -public class KeyDescriptorString extends KeyDescriptorBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final boolean DEFAULT_EMPTY_ALLOW_FLAG = true; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private boolean emptyAllow; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param name - * @param mandatory - */ - public KeyDescriptorString(String name, boolean mandatory) - { - this(name, mandatory, DEFAULT_EMPTY_ALLOW_FLAG); - } - - /** - * Default constructor - * - * @param name - * @param mandatory - * @param emptyAllow - */ - public KeyDescriptorString(String name, boolean mandatory, boolean emptyAllow) - { - super(name, mandatory); - this.emptyAllow = emptyAllow; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryKeyDescriptor - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public String convertValue(Object value) throws DataDictionaryException - { - String convertedValue = null; - - convertedValue = value.toString(); - - this.check(convertedValue); - - return convertedValue; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get empty allow flag - * - * @return empty allow flag - */ - public boolean isEmptyAllow() - { - return this.emptyAllow; - } - - /** - * Set empty allow flag - * - * @param emptyAllow - */ - public void setEmptyAllow(boolean emptyAllow) - { - this.emptyAllow = emptyAllow; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void check(String value) throws DataDictionaryException - { - if (!this.emptyAllow && value.trim().isEmpty()) - { - throw new DataDictionaryException("Key " + this.getName() + ": value can't be empty"); - } - } - -} diff --git a/src/main/java/enedis/lab/util/MinMaxChecker.java b/src/main/java/enedis/lab/util/MinMaxChecker.java deleted file mode 100644 index 12ad83c..0000000 --- a/src/main/java/enedis/lab/util/MinMaxChecker.java +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util; - -/** - * Min max class - */ -public class MinMaxChecker -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Number min; - private Number max; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MinMaxChecker() - { - super(); - } - - /** - * Constructor setting parameters - * - * @param min - * @param max - */ - public MinMaxChecker(Number min, Number max) - { - this(); - this.setMin(min); - this.setMax(max); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get min - * - * @return min - */ - public Number getMin() - { - return this.min; - } - - /** - * Set min - * - * @param min - * @throws IllegalArgumentException - * if min is greater than max - */ - public void setMin(Number min) - { - if (min != null) - { - if (this.max != null && min.doubleValue() > this.max.doubleValue()) - { - throw new IllegalArgumentException("min can't be greater than max"); - } - } - this.min = min; - } - - /** - * Get max - * - * @return max - */ - public Number getMax() - { - return this.max; - } - - /** - * Set max - * - * @param max - * @throws IllegalArgumentException - * if max is smaller than min - */ - public void setMax(Number max) - { - if (max != null) - { - if (this.min != null && max.doubleValue() < this.min.doubleValue()) - { - throw new IllegalArgumentException("max can't be smaller than min"); - } - } - this.max = max; - } - - /** - * Check the given value - * - * @param value - * @return true if the value is in [min, max] - */ - public boolean check(Number value) - { - if (value != null) - { - if (this.min != null) - { - if (value.doubleValue() < this.min.doubleValue()) - { - return false; - } - } - if (this.max != null) - { - if (value.doubleValue() > this.max.doubleValue()) - { - return false; - } - } - return true; - } - else - { - return false; - } - - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/util/SystemError.java b/src/main/java/enedis/lab/util/SystemError.java deleted file mode 100644 index 90b48c1..0000000 --- a/src/main/java/enedis/lab/util/SystemError.java +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util; - -import org.apache.commons.lang3.SystemUtils; - -import com.sun.jna.Native; -import com.sun.jna.platform.win32.Kernel32Util; - -/** - * Class for system error - * - */ -public class SystemError -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get system last error code - * - * @return System last error code - */ - static public int getCode() - { - return Native.getLastError(); - } - - /** - * Get system last error message - * - * @return System last error message - */ - static public String getMessage() - { - return getMessage(getCode()); - } - - /** - * Get system error message associated with code - * - * @param code - * the system error code - * - * @return System error message - */ - static public String getMessage(int code) - { - String message = null; - - if (SystemUtils.IS_OS_WINDOWS) - { - message = Kernel32Util.formatMessage(code); - } - else if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_UNIX || SystemUtils.IS_OS_MAC_OSX) - { - message = SystemLibC.INSTANCE.strerror(code); - } - - return message; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/util/SystemLibC.java b/src/main/java/enedis/lab/util/SystemLibC.java deleted file mode 100644 index 49592d5..0000000 --- a/src/main/java/enedis/lab/util/SystemLibC.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util; - -import com.sun.jna.Library; -import com.sun.jna.Native; - -/** - * Interface for system of C library - * - */ -public interface SystemLibC extends Library -{ - /** - * Instance - */ - SystemLibC INSTANCE = Native.load("c", SystemLibC.class); - - /** - * Get string error from code - * - * @param code - * @return string error - */ - public String strerror(int code); -} diff --git a/src/main/java/enedis/lab/util/message/Event.java b/src/main/java/enedis/lab/util/message/Event.java deleted file mode 100644 index 1b23b0a..0000000 --- a/src/main/java/enedis/lab/util/message/Event.java +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; - -/** - * Event class - * - * Generated - */ -public abstract class Event extends Message -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATE_TIME = "dateTime"; - - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.EVENT; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorLocalDateTime kDateTime; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Event() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Event(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Event(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param dateTime - * @throws DataDictionaryException - */ - public Event(String name, LocalDateTime dateTime) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setDateTime(dateTime); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Message - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get date time - * - * @return the date time - */ - public LocalDateTime getDateTime() - { - return (LocalDateTime) this.data.get(KEY_DATE_TIME); - } - - /** - * Set date time - * - * @param dateTime - * @throws DataDictionaryException - */ - public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException - { - this.setDateTime((Object) dateTime); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setDateTime(Object dateTime) throws DataDictionaryException - { - this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); - this.keys.add(this.kDateTime); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/Message.java b/src/main/java/enedis/lab/util/message/Message.java deleted file mode 100644 index efec77a..0000000 --- a/src/main/java/enedis/lab/util/message/Message.java +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * Message class - * - * Generated - */ -public class Message extends DataDictionaryBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Key type */ - public static final String KEY_TYPE = "type"; - /** Key name */ - public static final String KEY_NAME = "name"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorEnum kType; - protected KeyDescriptorString kName; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Message() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Message(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Message(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @throws DataDictionaryException - */ - public Message(MessageType type, String name) throws DataDictionaryException - { - this(); - - this.setType(type); - this.setName(name); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get type - * - * @return the type - */ - public MessageType getType() - { - return (MessageType) this.data.get(KEY_TYPE); - } - - /** - * Get name - * - * @return the name - */ - public String getName() - { - return (String) this.data.get(KEY_NAME); - } - - /** - * Set type - * - * @param type - * @throws DataDictionaryException - */ - public void setType(MessageType type) throws DataDictionaryException - { - this.setType((Object) type); - } - - /** - * Set name - * - * @param name - * @throws DataDictionaryException - */ - public void setName(String name) throws DataDictionaryException - { - this.setName((Object) name); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setType(Object type) throws DataDictionaryException - { - this.data.put(KEY_TYPE, this.kType.convert(type)); - } - - protected void setName(Object name) throws DataDictionaryException - { - this.data.put(KEY_NAME, this.kName.convert(name)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kType = new KeyDescriptorEnum(KEY_TYPE, true, MessageType.class); - this.keys.add(this.kType); - - this.kName = new KeyDescriptorString(KEY_NAME, true, false); - this.keys.add(this.kName); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/MessageType.java b/src/main/java/enedis/lab/util/message/MessageType.java deleted file mode 100644 index 6441f34..0000000 --- a/src/main/java/enedis/lab/util/message/MessageType.java +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -/** - * Message type - */ -public enum MessageType -{ - /** Event */ - EVENT, - /** Request */ - REQUEST, - /** Response */ - RESPONSE; -} diff --git a/src/main/java/enedis/lab/util/message/Request.java b/src/main/java/enedis/lab/util/message/Request.java deleted file mode 100644 index 7ad542b..0000000 --- a/src/main/java/enedis/lab/util/message/Request.java +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; - -/** - * Request class - * - * Generated - */ -public abstract class Request extends Message -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.REQUEST; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Request() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Request(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Request(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @throws DataDictionaryException - */ - public Request(MessageType type, String name) throws DataDictionaryException - { - this(); - - this.setType(type); - this.setName(name); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Message - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/Response.java b/src/main/java/enedis/lab/util/message/Response.java deleted file mode 100644 index 772c4b0..0000000 --- a/src/main/java/enedis/lab/util/message/Response.java +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * Response class - * - * Generated - */ -public abstract class Response extends Message -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATE_TIME = "dateTime"; - protected static final String KEY_ERROR_CODE = "errorCode"; - protected static final String KEY_ERROR_MESSAGE = "errorMessage"; - - private static final MessageType TYPE_ACCEPTED_VALUE = MessageType.RESPONSE; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorLocalDateTime kDateTime; - protected KeyDescriptorNumber kErrorCode; - protected KeyDescriptorString kErrorMessage; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Response() - { - super(); - this.loadKeyDescriptors(); - - this.kType.setAcceptedValues(TYPE_ACCEPTED_VALUE); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public Response(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public Response(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param type - * @param name - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public Response(MessageType type, String name, LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(); - - this.setType(type); - this.setName(name); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Message - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_TYPE)) - { - this.setType(TYPE_ACCEPTED_VALUE); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get date time - * - * @return the date time - */ - public LocalDateTime getDateTime() - { - return (LocalDateTime) this.data.get(KEY_DATE_TIME); - } - - /** - * Get error code - * - * @return the error code - */ - public Number getErrorCode() - { - return (Number) this.data.get(KEY_ERROR_CODE); - } - - /** - * Get error message - * - * @return the error message - */ - public String getErrorMessage() - { - return (String) this.data.get(KEY_ERROR_MESSAGE); - } - - /** - * Set date time - * - * @param dateTime - * @throws DataDictionaryException - */ - public void setDateTime(LocalDateTime dateTime) throws DataDictionaryException - { - this.setDateTime((Object) dateTime); - } - - /** - * Set error code - * - * @param errorCode - * @throws DataDictionaryException - */ - public void setErrorCode(Number errorCode) throws DataDictionaryException - { - this.setErrorCode((Object) errorCode); - } - - /** - * Set error message - * - * @param errorMessage - * @throws DataDictionaryException - */ - public void setErrorMessage(String errorMessage) throws DataDictionaryException - { - this.setErrorMessage((Object) errorMessage); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setDateTime(Object dateTime) throws DataDictionaryException - { - this.data.put(KEY_DATE_TIME, this.kDateTime.convert(dateTime)); - } - - protected void setErrorCode(Object errorCode) throws DataDictionaryException - { - this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); - } - - protected void setErrorMessage(Object errorMessage) throws DataDictionaryException - { - this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kDateTime = new KeyDescriptorLocalDateTime(KEY_DATE_TIME, true); - this.keys.add(this.kDateTime); - - this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); - this.keys.add(this.kErrorCode); - - this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, false, true); - this.keys.add(this.kErrorMessage); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/ResponseBase.java b/src/main/java/enedis/lab/util/message/ResponseBase.java deleted file mode 100644 index bd44949..0000000 --- a/src/main/java/enedis/lab/util/message/ResponseBase.java +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; - -/** - * ResponseBase class - * - * Generated - */ -public class ResponseBase extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseBase() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseBase(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseBase(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param name - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseBase(String name, LocalDateTime dateTime, Number errorCode, String errorMessage, DataDictionaryBase data) throws DataDictionaryException - { - this(); - - this.setName(name); - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - public DataDictionaryBase getData() - { - return (DataDictionaryBase) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(DataDictionaryBase data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, DataDictionaryBase.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageException.java b/src/main/java/enedis/lab/util/message/exception/MessageException.java deleted file mode 100644 index 217fe8e..0000000 --- a/src/main/java/enedis/lab/util/message/exception/MessageException.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Message exception - */ -public class MessageException extends Exception -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java deleted file mode 100644 index 27847b0..0000000 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidContentException.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class MessageInvalidContentException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageInvalidContentException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageInvalidContentException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageInvalidContentException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageInvalidContentException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java deleted file mode 100644 index c1e04fb..0000000 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidFormatException.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class MessageInvalidFormatException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageInvalidFormatException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageInvalidFormatException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageInvalidFormatException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageInvalidFormatException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java b/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java deleted file mode 100644 index 0eee2df..0000000 --- a/src/main/java/enedis/lab/util/message/exception/MessageInvalidTypeException.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class MessageInvalidTypeException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageInvalidTypeException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageInvalidTypeException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageInvalidTypeException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageInvalidTypeException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java deleted file mode 100644 index 326eb13..0000000 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyNameDoesntExistException.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class MessageKeyNameDoesntExistException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageKeyNameDoesntExistException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageKeyNameDoesntExistException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageKeyNameDoesntExistException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageKeyNameDoesntExistException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java b/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java deleted file mode 100644 index 747843b..0000000 --- a/src/main/java/enedis/lab/util/message/exception/MessageKeyTypeDoesntExistException.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class MessageKeyTypeDoesntExistException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageKeyTypeDoesntExistException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public MessageKeyTypeDoesntExistException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public MessageKeyTypeDoesntExistException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public MessageKeyTypeDoesntExistException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java b/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java deleted file mode 100644 index e1f0277..0000000 --- a/src/main/java/enedis/lab/util/message/exception/UnsupportedMessageException.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.exception; - -/** - * Unvalid message format exception - */ -public class UnsupportedMessageException extends MessageException -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public UnsupportedMessageException() - { - super(); - } - - /** - * Constructor using message and cause - * - * @param message - * @param cause - */ - public UnsupportedMessageException(String message, Throwable cause) - { - super(message, cause); - } - - /** - * Constructor using message - * - * @param message - */ - public UnsupportedMessageException(String message) - { - super(message); - } - - /** - * Constructor using cause - * - * @param cause - */ - public UnsupportedMessageException(Throwable cause) - { - super(cause); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java deleted file mode 100644 index 9ecbf5b..0000000 --- a/src/main/java/enedis/lab/util/message/factory/AbstractMessageFactory.java +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import java.util.HashMap; -import java.util.Map; - -import org.json.JSONException; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.util.message.Message; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.UnsupportedMessageException; - -/** - * Message factory - * - * @param - */ -public class AbstractMessageFactory -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Class clazz; - private Map> messageClasses; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param clazz - */ - public AbstractMessageFactory(Class clazz) - { - this.clazz = clazz; - this.messageClasses = new HashMap>(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get message from text - * - * @param text - * @param name - * @return message - * @throws UnsupportedMessageException - * @throws MessageInvalidFormatException - * @throws MessageInvalidContentException - */ - public final T getMessage(String text, String name) throws UnsupportedMessageException, MessageInvalidFormatException, MessageInvalidContentException - { - T message = null; - - try - { - Class messageClazz = this.messageClasses.get(name); - - if (messageClazz != null) - { - message = messageClazz.cast(DataDictionaryBase.fromString(text, messageClazz)); - } - else - { - throw new UnsupportedMessageException("Unsupported " + this.clazz.getSimpleName() + " : " + name); - } - } - catch (JSONException e) - { - throw new MessageInvalidFormatException("Invalid " + this.clazz.getSimpleName() + " " + name + " format, it should be JSON : " + e.getMessage(), e); - } - catch (DataDictionaryException e) - { - throw new MessageInvalidContentException("Invalid " + this.clazz.getSimpleName() + " " + name + " content : " + e.getMessage(), e); - } - - return message; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Add a Message class to decode message with the given name - * - * @param name - * @param messageClazz - */ - public final void addMessageClass(String name, Class messageClazz) - { - if (name == null || messageClazz == null) - { - throw new IllegalArgumentException("Name and " + this.clazz.getSimpleName() + " class can't be null"); - } - - this.messageClasses.put(name, messageClazz); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} \ No newline at end of file diff --git a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java b/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java deleted file mode 100644 index 84a8e75..0000000 --- a/src/main/java/enedis/lab/util/message/factory/BasicMessageFactory.java +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import org.json.JSONException; -import org.json.JSONObject; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Message; -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; - -/** - * Message factory - */ -public abstract class BasicMessageFactory -{ - /** - * Get message from text - * - * @param text - * @return message - * @throws MessageInvalidFormatException - * @throws MessageKeyTypeDoesntExistException - * @throws MessageKeyNameDoesntExistException - * @throws MessageInvalidTypeException - */ - public static Message getMessage(String text) - throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, MessageInvalidTypeException - { - JSONObject messageJson = convertTextToJson(text); - - MessageType type = extractType(messageJson); - String name = extractName(messageJson); - - return createMessage(type, name); - } - - private static JSONObject convertTextToJson(String text) throws MessageInvalidFormatException - { - try - { - return new JSONObject(text); - } - catch (JSONException e) - { - throw new MessageInvalidFormatException("Invalid format, it should be JSON : " + e.getMessage()); - } - } - - private static MessageType extractType(JSONObject jsonObj) throws MessageKeyTypeDoesntExistException, MessageInvalidFormatException, MessageInvalidTypeException - { - try - { - checkKeyTypeExists(jsonObj); - String typeStr = jsonObj.getString(Message.KEY_TYPE); - MessageType type = MessageType.valueOf(typeStr); - return type; - } - catch (JSONException e) - { - throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); - } - catch (IllegalArgumentException e) - { - throw new MessageInvalidTypeException("Invalid format : " + e.getMessage()); - } - } - - private static void checkKeyTypeExists(JSONObject jsonObj) throws MessageKeyTypeDoesntExistException - { - if (!jsonObj.has(Message.KEY_TYPE)) - { - throw new MessageKeyTypeDoesntExistException("Key type missing"); - } - } - - private static String extractName(JSONObject jsonObj) throws MessageInvalidFormatException, MessageKeyNameDoesntExistException - { - try - { - checkKeyNameExists(jsonObj); - return jsonObj.getString(Message.KEY_NAME); - } - catch (JSONException e) - { - throw new MessageInvalidFormatException("Invalid format : " + e.getMessage()); - } - } - - private static void checkKeyNameExists(JSONObject jsonObj) throws MessageKeyNameDoesntExistException - { - if (!jsonObj.has(Message.KEY_NAME)) - { - throw new MessageKeyNameDoesntExistException("Key name missing"); - } - } - - private static Message createMessage(MessageType type, String name) - { - try - { - return new Message(type, name); - } - catch (DataDictionaryException e) - { - return null; - } - } -} diff --git a/src/main/java/enedis/lab/util/message/factory/EventFactory.java b/src/main/java/enedis/lab/util/message/factory/EventFactory.java deleted file mode 100644 index f349021..0000000 --- a/src/main/java/enedis/lab/util/message/factory/EventFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Event; - -/** - * Request factory - */ -public class EventFactory extends AbstractMessageFactory -{ - /** - * Default constructor - */ - public EventFactory() - { - super(Event.class); - } -} diff --git a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java b/src/main/java/enedis/lab/util/message/factory/MessageFactory.java deleted file mode 100644 index daa4df0..0000000 --- a/src/main/java/enedis/lab/util/message/factory/MessageFactory.java +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Message; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; - -/** - * Message factory - */ -public class MessageFactory -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private RequestFactory requestFactory; - private ResponseFactory responseFactory; - private EventFactory eventFactory; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public MessageFactory() - { - super(); - } - - /** - * Constructor using field - * - * @param requestFactory - * @param responseFactory - * @param eventFactory - */ - public MessageFactory(RequestFactory requestFactory, ResponseFactory responseFactory, EventFactory eventFactory) - { - super(); - this.requestFactory = requestFactory; - this.responseFactory = responseFactory; - this.eventFactory = eventFactory; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get message from String - * - * @param text - * @return message - * @throws MessageInvalidTypeException - * @throws MessageKeyNameDoesntExistException - * @throws MessageKeyTypeDoesntExistException - * @throws MessageInvalidFormatException - * @throws MessageInvalidContentException - * @throws UnsupportedMessageException - */ - public Message getMessage(String text) throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - this.checkSubFactoryReferences(); - - Message genericMessage = BasicMessageFactory.getMessage(text); - Message message = null; - - // @formatter:off - switch (genericMessage.getType()) - { - case REQUEST: message = this.requestFactory.getMessage(text, genericMessage.getName()); break; - case RESPONSE: message = this.responseFactory.getMessage(text, genericMessage.getName()); break; - case EVENT: message = this.eventFactory.getMessage(text, genericMessage.getName()); break; - default: - break; - } - // @formatter:on - - return message; - } - - /** - * Set request factory - * - * @param requestFactory - */ - public void setRequestFactory(RequestFactory requestFactory) - { - this.requestFactory = requestFactory; - } - - /** - * Set response factory - * - * @param responseFactory - */ - public void setResponseFactory(ResponseFactory responseFactory) - { - this.responseFactory = responseFactory; - } - - /** - * Set event factory - * - * @param eventFactory - */ - public void setEventFactory(EventFactory eventFactory) - { - this.eventFactory = eventFactory; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void checkSubFactoryReferences() - { - if (this.requestFactory == null || this.responseFactory == null || this.eventFactory == null) - { - throw new IllegalStateException("requestFactory, responseFactory and eventFactory have to be set"); - } - } - -} diff --git a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java b/src/main/java/enedis/lab/util/message/factory/RequestFactory.java deleted file mode 100644 index 4df5690..0000000 --- a/src/main/java/enedis/lab/util/message/factory/RequestFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Request; - -/** - * Request factory - */ -public class RequestFactory extends AbstractMessageFactory -{ - /** - * Default constructor - */ - public RequestFactory() - { - super(Request.class); - } -} diff --git a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java b/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java deleted file mode 100644 index eda280e..0000000 --- a/src/main/java/enedis/lab/util/message/factory/ResponseFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.message.factory; - -import enedis.lab.util.message.Response; - -/** - * Request factory - */ -public class ResponseFactory extends AbstractMessageFactory -{ - /** - * Default constructor - */ - public ResponseFactory() - { - super(Response.class); - } -} diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifier.java b/src/main/java/enedis/lab/util/task/FilteredNotifier.java deleted file mode 100644 index bf86a63..0000000 --- a/src/main/java/enedis/lab/util/task/FilteredNotifier.java +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.Collection; -import java.util.function.Predicate; - -/** - * Notifier interface with filter - * - * @param - * the filter - * @param - * the subscriber - */ -public interface FilteredNotifier extends Notifier -{ - /** - * Add a subscriber with filter - * - * @param filter - * the filter used for subscription - * @param listener - * the subscriber reference - * @throws Exception - * on subscription failure - */ - public void subscribe(F filter, T listener) throws Exception; - - /** - * Remove a subscriber with filter - * - * @param filter - * the filter used for subscription - * @param listener - * the subscriber reference - * @throws Exception - * if filter or listener not found - */ - public void unsubscribe(F filter, T listener) throws Exception; - - /** - * Check if the given filter has a given subscriber - * - * @param filter - * the filter used for subscription - * @param listener - * the subscriber reference - * @return true if the given subscriber exists - */ - public boolean hasSubscriber(F filter, T listener); - - /** - * Get subscribers associated with filter - * - * @param filter - * the filter used for subscription - * @return The subscribers collection - */ - public Collection getSubscribers(F filter); - - /** - * Get subscribers including global and/or filters - * - * @param includeGlobal - * indicates if includes global subscribers - * @param includeFilter - * indicates if includes filters subscribers - * @return The subscribers collection - */ - public Collection getSubscribers(boolean includeGlobal, boolean includeFilter); - - /** - * Get subscribers associated with predicate filter - * - * @param predicate - * the predicate used to test with filters used for subscription - * @param includeGlobal - * indicates if includes global subscribers - * @return The subscribers collection - */ - public Collection getSubscribers(Predicate predicate, boolean includeGlobal); - - /** - * Check if the given filter has been set - * - * @param filter - * the filter used for subscription - * @return true if the given filter exists - */ - public boolean hasFilter(F filter); - - /** - * Get filters - * - * @return The filters collection - */ - public Collection getFilters(); - - /** - * Get filters associated with listener - * - * @param listener - * the subscriber reference - * @return The filters collection - */ - public Collection getFilters(T listener); -} diff --git a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java b/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java deleted file mode 100644 index 5b772d7..0000000 --- a/src/main/java/enedis/lab/util/task/FilteredNotifierBase.java +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Predicate; - -/** - * Filtered notifier with subscribers basic implementation - * - * @param - * the filter type - * @param - * the subscriber type - */ -public class FilteredNotifierBase implements FilteredNotifier -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Map> subscribersFiltered; - private Lock subscribersFilteredLock = new ReentrantLock(); - protected Collection subscribersUnfiltered; - private Lock subscribersUnfilteredLock = new ReentrantLock(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public FilteredNotifierBase() - { - super(); - this.subscribersFiltered = new ConcurrentHashMap>(); - this.subscribersUnfiltered = new CopyOnWriteArraySet(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Listenable - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void subscribe(T listener) - { - if (listener == null) - { - return; - } - this.subscribersUnfilteredLock.lock(); - this.subscribersUnfiltered.add(listener); - this.subscribersUnfilteredLock.unlock(); - } - - @Override - public void unsubscribe(T listener) - { - if (listener == null) - { - return; - } - for (F filter : this.getFilters()) - { - this.unsubscribe(filter, listener); - } - this.subscribersUnfilteredLock.lock(); - this.subscribersUnfiltered.remove(listener); - this.subscribersUnfilteredLock.unlock(); - } - - @Override - public boolean hasSubscriber(T listener) - { - for (F filter : this.getFilters()) - { - if (this.hasSubscriber(filter, listener)) - { - return true; - } - } - - this.subscribersUnfilteredLock.lock(); - boolean hasSubscriber = this.subscribersUnfiltered.contains(listener); - this.subscribersUnfilteredLock.unlock(); - - return hasSubscriber; - } - - @Override - public Collection getSubscribers() - { - return this.getSubscribers(true, true); - } - - @Override - public void subscribe(F filter, T listener) - { - if (filter == null || listener == null) - { - return; - } - Collection subscribers; - if (this.hasFilter(filter)) - { - subscribers = this.getSubscribers(filter); - if (!subscribers.contains(listener)) - { - subscribers.add(listener); - } - } - else - { - subscribers = new HashSet(); - subscribers.add(listener); - } - this.subscribersFilteredLock.lock(); - this.subscribersFiltered.put(filter, subscribers); - this.subscribersFilteredLock.unlock(); - } - - @Override - public void unsubscribe(F filter, T listener) - { - if (filter == null || listener == null) - { - return; - } - Collection subscribers = this.getSubscribers(filter); - if (subscribers.contains(listener)) - { - subscribers.remove(listener); - if (subscribers.size() == 0) - { - this.subscribersFilteredLock.lock(); - this.subscribersFiltered.remove(filter); - this.subscribersFilteredLock.unlock(); - } - } - } - - @Override - public boolean hasSubscriber(F filter, T listener) - { - Collection subscribers = this.getSubscribers(filter); - - return subscribers.contains(listener); - } - - @Override - public Collection getSubscribers(F filter) - { - this.subscribersFilteredLock.lock(); - Collection subscribersFiltered = (this.hasFilter(filter)) ? this.subscribersFiltered.get(filter) : new HashSet(); - this.subscribersFilteredLock.unlock(); - - return subscribersFiltered; - } - - @Override - public Collection getSubscribers(boolean includeGlobal, boolean includeFilter) - { - Collection subscribers = new HashSet(); - - if (includeFilter) - { - for (F filter : this.getFilters()) - { - Collection filterSubscribers = this.getSubscribers(filter); - subscribers.addAll(filterSubscribers); - } - } - if (includeGlobal) - { - this.subscribersUnfilteredLock.lock(); - subscribers.addAll(this.subscribersUnfiltered); - this.subscribersUnfilteredLock.unlock(); - } - - return subscribers; - } - - @Override - public Collection getSubscribers(Predicate predicate, boolean includeGlobal) - { - Collection subscribers = new HashSet(); - - if (predicate != null) - { - this.subscribersFilteredLock.lock(); - for (F filter : this.subscribersFiltered.keySet()) - { - if (predicate.test(filter)) - { - subscribers.addAll(this.subscribersFiltered.get(filter)); - } - } - this.subscribersFilteredLock.unlock(); - } - if (includeGlobal) - { - this.subscribersUnfilteredLock.lock(); - subscribers.addAll(this.subscribersUnfiltered); - this.subscribersUnfilteredLock.unlock(); - } - - return subscribers; - } - - @Override - public boolean hasFilter(F filter) - { - this.subscribersFilteredLock.lock(); - boolean hasFilter = (filter != null) ? this.subscribersFiltered.containsKey(filter) : false; - this.subscribersFilteredLock.unlock(); - - return hasFilter; - } - - @Override - public Collection getFilters() - { - this.subscribersFilteredLock.lock(); - Collection subscribersFiltered = this.subscribersFiltered.keySet(); - this.subscribersFilteredLock.unlock(); - - return subscribersFiltered; - } - - @Override - public Collection getFilters(T listener) - { - Collection filters = new HashSet(); - - for (F filter : this.getFilters()) - { - Collection subscribers = this.getSubscribers(filter); - if (subscribers.contains(listener)) - { - filters.add(filter); - } - } - - return filters; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} \ No newline at end of file diff --git a/src/main/java/enedis/lab/util/task/Notifier.java b/src/main/java/enedis/lab/util/task/Notifier.java deleted file mode 100644 index e8ec73e..0000000 --- a/src/main/java/enedis/lab/util/task/Notifier.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.Collection; - -/** - * Notifier interface - * - * @param - */ -public interface Notifier -{ - /** - * Add a subscriber - * - * @param subscriber - */ - public void subscribe(T subscriber); - - /** - * Remove a subscriber - * - * @param subscriber - */ - public void unsubscribe(T subscriber); - - /** - * Check if the given subscriber has been added - * - * @param subscriber - * @return true if the given subscriber has been added - */ - public boolean hasSubscriber(T subscriber); - - /** - * Get subscribers - * - * @return The subscribers collection - */ - public Collection getSubscribers(); -} diff --git a/src/main/java/enedis/lab/util/task/NotifierBase.java b/src/main/java/enedis/lab/util/task/NotifierBase.java deleted file mode 100644 index d0a9979..0000000 --- a/src/main/java/enedis/lab/util/task/NotifierBase.java +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.Collection; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; - -/** - * Notifier with subscribers basic implementation - * - * @param - * the subscriber type - */ -public class NotifierBase implements Notifier -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Set subscribers; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public NotifierBase() - { - super(); - this.subscribers = new CopyOnWriteArraySet(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Listenable - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void subscribe(T listener) - { - if (listener != null && !this.subscribers.contains(listener)) - { - this.subscribers.add(listener); - } - } - - @Override - public void unsubscribe(T listener) - { - if (listener != null && this.subscribers.contains(listener)) - { - this.subscribers.remove(listener); - } - } - - @Override - public boolean hasSubscriber(T listener) - { - return this.subscribers.contains(listener); - } - - @Override - public Collection getSubscribers() - { - return this.subscribers; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} \ No newline at end of file diff --git a/src/main/java/enedis/lab/util/task/Subscriber.java b/src/main/java/enedis/lab/util/task/Subscriber.java deleted file mode 100644 index 253daec..0000000 --- a/src/main/java/enedis/lab/util/task/Subscriber.java +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -/** - * Subscriber interface - */ -public interface Subscriber -{ - -} diff --git a/src/main/java/enedis/lab/util/task/Task.java b/src/main/java/enedis/lab/util/task/Task.java deleted file mode 100644 index d3ead6f..0000000 --- a/src/main/java/enedis/lab/util/task/Task.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -/** - * Task interface - */ -public interface Task -{ - /** - * Start task - */ - public void start(); - - /** - * Stop task - */ - public void stop(); - - /** - * Get the task running state - * - * @return true if the task running - */ - public boolean isRunning(); -} diff --git a/src/main/java/enedis/lab/util/task/TaskBase.java b/src/main/java/enedis/lab/util/task/TaskBase.java deleted file mode 100644 index 15b94d8..0000000 --- a/src/main/java/enedis/lab/util/task/TaskBase.java +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.concurrent.atomic.AtomicBoolean; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Task basic implementation - */ -public abstract class TaskBase implements Task, Runnable -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private AtomicBoolean stopRequired; - private Thread task; - protected static Logger logger = LogManager.getLogger(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TaskBase() - { - super(); - this.stopRequired = new AtomicBoolean(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Task - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void start() - { - if (this.task == null || !this.task.isAlive()) - { - this.stopRequired.set(false); - this.task = new Thread(this); - this.task.start(); - } - } - - @Override - public void stop() - { - try - { - if (this.task != null && this.task.isAlive()) - { - this.stopRequired.set(true); - if (this.task != Thread.currentThread()) - { - this.task.join(); - } - } - } - catch (InterruptedException exception) - { - logger.error("Stop task interrupted", exception); - } - } - - @Override - public final boolean isRunning() - { - return this.task == null ? false : this.task.isAlive(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void run() - { - this.runOnStart(); - this.runProcess(); - this.runOnTerminate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected final boolean isStopRequired() - { - return this.stopRequired.get(); - } - - protected final void runOnStart() - { - try - { - this.onStart(); - } - catch (Exception exception) - { - logger.error("Task on start aborted", exception); - this.runOnError(exception); - } - } - - protected final void runProcess() - { - try - { - this.process(); - } - catch (Exception exception) - { - logger.error("Task process aborted", exception); - this.runOnError(exception); - } - } - - protected final void runOnTerminate() - { - try - { - this.onTerminate(); - } - catch (Exception exception) - { - logger.error("Task on terminate aborted", exception); - this.runOnError(exception); - } - } - - protected final void runOnError(Exception exception) - { - try - { - this.onError(exception); - } - catch (Exception onErrorException) - { - logger.error("Task on error aborted", exception); - } - } - - /** - * Task core process method - */ - protected abstract void process(); - - protected void onStart() - { - } - - protected void onTerminate() - { - } - - protected void onError(Exception exception) - { - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodic.java b/src/main/java/enedis/lab/util/task/TaskPeriodic.java deleted file mode 100644 index 84f279d..0000000 --- a/src/main/java/enedis/lab/util/task/TaskPeriodic.java +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.concurrent.atomic.AtomicLong; - -import enedis.lab.util.time.Time; - -/** - * Periodic task with configurable period - */ -public abstract class TaskPeriodic extends TaskBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default period (in milliseconds) used to execute process - */ - public static final long DEFAULT_PERIOD = 1000; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private AtomicLong period = new AtomicLong(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor with default parameter - * - * @see #DEFAULT_PERIOD - */ - public TaskPeriodic() - { - super(); - this.setPeriod(DEFAULT_PERIOD); - } - - /** - * Constructor with polling period - * - * @param period - * the period (in milliseconds) used to execute process - */ - public TaskPeriodic(long period) - { - super(); - this.setPeriod(period); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Runnable - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void run() - { - this.runOnStart(); - while (!this.isStopRequired()) - { - this.runProcess(); - if (this.isStopRequired()) - { - break; - } - this.waitPeriod(); - } - this.runOnTerminate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get polling period - * - * @return The period (in milliseconds) used to execute process - */ - public long getPeriod() - { - return this.period.get(); - } - - /** - * Set the polling period - * - * @param period - * the period (in milliseconds) used to execute process - * @throws IllegalArgumentException - * if period <= 0 - */ - public void setPeriod(long period) - { - if (period <= 0) - { - throw new IllegalArgumentException("Cannot set period " + period + " : must be > 0"); - } - this.period.set(period); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void waitPeriod() - { - Time.sleep(this.getPeriod()); - } -} diff --git a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java b/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java deleted file mode 100644 index bf34fb8..0000000 --- a/src/main/java/enedis/lab/util/task/TaskPeriodicWithSubscribers.java +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.task; - -import java.util.Collection; - -/** - * Periodic task with subscribers basic implementation - * - * @param - * the subscriber type - */ -public abstract class TaskPeriodicWithSubscribers extends TaskPeriodic implements Notifier -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected Notifier notifier; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TaskPeriodicWithSubscribers() - { - super(); - this.notifier = new NotifierBase(); - } - - /** - * Constructor setting period - * - * @param period - * the period in milliseconds - */ - public TaskPeriodicWithSubscribers(long period) - { - super(period); - this.notifier = new NotifierBase(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void subscribe(T listener) - { - this.notifier.subscribe(listener); - } - - @Override - public void unsubscribe(T listener) - { - this.notifier.unsubscribe(listener); - } - - @Override - public boolean hasSubscriber(T listener) - { - return this.notifier.hasSubscriber(listener); - } - - @Override - public Collection getSubscribers() - { - return this.notifier.getSubscribers(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/lab/util/time/Time.java b/src/main/java/enedis/lab/util/time/Time.java deleted file mode 100644 index 25999f0..0000000 --- a/src/main/java/enedis/lab/util/time/Time.java +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.util.time; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; - -/** - * Time implementation - */ -public abstract class Time -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Millisecond */ - public final static int MILLISECOND = 1; - /** Second */ - public final static int SECOND = 1000 * MILLISECOND; - /** Minute */ - public final static int MINUTE = 60 * SECOND; - /** Hour */ - public final static int HOUR = 60 * MINUTE; - /** Day */ - public final static int DAY = 24 * HOUR; - - /* Default format for displaying the date/time */ - private static final String DFLT_DATETIME_FORMAT = "yyyy/MM/dd HH:mm:ss.SSS"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Wait the given duration in millisecond - * - * @param duration - */ - public static void sleep(long duration) - { - if (duration <= 0L) - { - return; - } - try - { - Thread.sleep(duration); - } - catch (InterruptedException e) - { - e.printStackTrace(); - } - } - - /** - * Convert date/time from long number (since an origin) to a string in the default format - * - * @param timestamp - * a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) - * @return the time/date string in the default format - */ - public static String timestampToDateTimeStr(long timestamp) - { - return timestampToDateTimeStr(timestamp, DFLT_DATETIME_FORMAT); - } - - /** - * Convert date/time from long number to a string in the specified format - * - * @param timestamp - * a long number (of milliseconds since January 1, 1970, 00:00:00 GMT) - * @param dateTimeFormatStr - * the format specified - * @return the time/date string in the specified format - */ - public static String timestampToDateTimeStr(long timestamp, String dateTimeFormatStr) - { - DateFormat dateFormat = new SimpleDateFormat(dateTimeFormatStr); - Date date = new Date(timestamp); - return dateFormat.format(date); - } - - /** - * Timestamp a message with the current DateTime - * - * @param msg - * @return the message decorated with current DateTime - */ - public static String timestamp(String msg) - { - return timestamp(msg, DFLT_DATETIME_FORMAT); - } - - /** - * Timestamp a message with the current DateTime - * - * @param msg - * @param dateTimeFormat - * @return the message decorated with current DateTime - */ - public static String timestamp(String msg, String dateTimeFormat) - { - DateFormat dateFormat = new SimpleDateFormat(dateTimeFormat); - Date date = new Date(); - String text = dateFormat.format(date) + " : " + msg; - return text; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java b/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java deleted file mode 100644 index 1cbce3d..0000000 --- a/src/main/java/enedis/tic/core/ReadNextFrameSubscriber.java +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -public class ReadNextFrameSubscriber implements TICCoreSubscriber -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICCoreFrame frame; - private TICCoreError error; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public ReadNextFrameSubscriber() - { - super(); - this.clear(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onData(TICCoreFrame frame) - { - this.frame = frame; - } - - @Override - public void onError(TICCoreError error) - { - this.error = error; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreFrame getData() - { - return this.frame; - } - - public TICCoreError getError() - { - return this.error; - } - - public void clear() - { - this.frame = null; - this.error = null; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/tic/core/TICCore.java b/src/main/java/enedis/tic/core/TICCore.java deleted file mode 100644 index b04d740..0000000 --- a/src/main/java/enedis/tic/core/TICCore.java +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.util.List; - -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.util.task.Task; - -/** - * TICCore interface - */ -public interface TICCore extends Task -{ - /** - * Get available TICs - * - * @return The collection of TIC identifiers - */ - public List getAvailableTICs(); - - /** - * Get modems informations - * - * @return The collection of TIC port descriptors - */ - public List getModemsInfo(); - - /** - * Read next frame - * - * @param identifier - * the TIC identifier - * - * @return Frame read or null if timed out - * @throws TICCoreException - * if identifier not found or if any error occurs - */ - public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException; - - /** - * Read next frame - * - * @param identifier - * the TIC identifier - * @param timeout - * the read timeout in milliseconds - * - * @return Frame read or null if timed out - * @throws TICCoreException - * if identifier not found or if any error occurs - */ - public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException; - - /** - * Add a subscriber - * - * @param subscriber - */ - public void subscribe(TICCoreSubscriber subscriber); - - /** - * Remove a subscriber - * - * @param subscriber - */ - public void unsubscribe(TICCoreSubscriber subscriber); - - /** - * Add a subscriber with identifier - * - * @param identifier - * the identifier used for subscription - * @param listener - * the subscriber reference - * @throws Exception - * on subscription failure - */ - public void subscribe(TICIdentifier identifier, TICCoreSubscriber listener) throws TICCoreException; - - /** - * Remove a subscriber with identifier - * - * @param identifier - * the identifier used for subscription - * @param listener - * the subscriber reference - * @throws Exception - * if filter or listener not found - */ - public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber listener) throws TICCoreException; - - /** - * Get TICs identifier associated with a subscriber - * - * @param listener - * the subscriber reference - * - * @return The collection of TIC identifiers for the subscriber - */ - public List getIndentifiers(TICCoreSubscriber listener); -} diff --git a/src/main/java/enedis/tic/core/TICCoreBase.java b/src/main/java/enedis/tic/core/TICCoreBase.java deleted file mode 100644 index 206c705..0000000 --- a/src/main/java/enedis/tic/core/TICCoreBase.java +++ /dev/null @@ -1,581 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.function.Predicate; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.io.PlugSubscriber; -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.io.tic.TICPortFinder; -import enedis.lab.io.tic.TICPortFinderBase; -import enedis.lab.io.tic.TICPortPlugNotifier; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.time.Time; -import enedis.lab.util.task.FilteredNotifier; -import enedis.lab.util.task.FilteredNotifierBase; -import enedis.lab.util.task.Task; -import enedis.lab.util.task.TaskBase; - -/** - * TICCore implementation - */ -public class TICCoreBase implements TICCore, Task, TICCoreSubscriber, PlugSubscriber -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final int PLUG_NOTIFIER_POLLING_PERIOD = 100; - private static final int READ_NEXT_FRAME_TIMEOUT = 30000; - private static final int READ_NEXT_FRAME_POLLING_PERIOD = 100; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICPortFinder portFinder; - private TICPortPlugNotifier plugNotifier; - private long plugNotifierPeriod; - private Constructor streamConstructor; - private TICMode streamMode; - private List nativePortNamesOnStart; - private Collection streamList; - private FilteredNotifier eventNotifier; - private static Logger logger = LogManager.getLogger(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreBase() - { - this(TICMode.AUTO, null); - } - - public TICCoreBase(TICMode streamMode, List nativePortNamesStart) - { - this(TICPortFinderBase.getInstance(), PLUG_NOTIFIER_POLLING_PERIOD, TICCoreStreamBase.class, streamMode, nativePortNamesStart); - } - - public TICCoreBase(TICPortFinder ticPortFinder, long plugNotifierPeriod, Class streamClass, TICMode streamMode, List nativePortNamesOnStart) - { - super(); - this.portFinder = ticPortFinder; - this.plugNotifierPeriod = plugNotifierPeriod; - try - { - this.streamConstructor = streamClass.getConstructor(String.class, String.class, TICMode.class); - } - catch (NoSuchMethodException e) - { - throw new IllegalArgumentException("Class " + streamClass.getName() + " has no valid constructor : " + e.getMessage()); - } - if (streamMode == null) - { - throw new IllegalArgumentException("TICMode should be defined"); - } - this.streamMode = streamMode; - this.nativePortNamesOnStart = nativePortNamesOnStart; - this.streamList = Collections.synchronizedSet(new HashSet()); - this.eventNotifier = new FilteredNotifierBase(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCore - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public List getAvailableTICs() - { - List identifiers = new DataArrayList(); - - for (TICCoreStream stream : this.streamList) - { - identifiers.add(stream.getIdentifier()); - } - - return identifiers; - } - - @Override - public List getModemsInfo() - { - List descriptors = new DataArrayList(); - - for (TICCoreStream stream : this.streamList) - { - TICPortDescriptor descriptor = this.portFinder.findByPortName(stream.getIdentifier().getPortName()); - descriptors.add(descriptor); - } - - return descriptors; - } - - @Override - public TICCoreFrame readNextFrame(TICIdentifier identifier) throws TICCoreException - { - return this.readNextFrame(identifier, READ_NEXT_FRAME_TIMEOUT); - } - - @Override - public TICCoreFrame readNextFrame(TICIdentifier identifier, int timeout) throws TICCoreException - { - TICCoreFrame frame = null; - TICPortDescriptor descriptor = null; - TICCoreStream stream = this.findStream(identifier); - - if (stream == null) - { - descriptor = this.portFinder.findNative(identifier.getPortName()); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - stream = this.startNewStream(descriptor); - } - ReadNextFrameSubscriber subscriber = new ReadNextFrameSubscriber(); - stream.subscribe(subscriber); - - long begin = System.nanoTime(); - long elapsed = 0; - while (elapsed < timeout) - { - if (subscriber.getError() != null) - { - TICCoreException exception = new TICCoreException(subscriber.getError().getErrorCode().intValue(), subscriber.getError().getErrorMessage()); - logger.error(exception.getMessage(), exception); - stream.unsubscribe(subscriber); - this.closeNativeStream(identifier, stream, subscriber); - throw exception; - } - if (subscriber.getData() != null) - { - frame = subscriber.getData(); - break; - } - Time.sleep(READ_NEXT_FRAME_POLLING_PERIOD); - elapsed = (System.nanoTime() - begin) / 1000000; - } - stream.unsubscribe(subscriber); - this.closeNativeStream(identifier, stream, subscriber); - if (elapsed >= timeout) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), "Stream " + identifier + " data read timeout !"); - logger.error(exception.getMessage(), exception); - throw exception; - } - return frame; - } - - @Override - public void subscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException - { - TICCoreStream stream = this.findStream(identifier); - - if (stream == null) - { - TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), "Stream " + identifier + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - this.startNewStream(descriptor); - } - try - { - this.eventNotifier.subscribe(identifier, subscriber); - } - catch (Exception e) - { - logger.error(e.getMessage(), e); - } - } - - @Override - public void unsubscribe(TICIdentifier identifier, TICCoreSubscriber subscriber) throws TICCoreException - { - TICPortDescriptor descriptor = this.portFinder.findNative(identifier.getPortName()); - if (descriptor != null) - { - if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) - { - Collection subscribers = this.findSubscribers(identifier, false); - if (subscribers.contains(subscriber) && subscribers.size() == 1) - { - this.stopStream(descriptor); - } - } - } - try - { - this.eventNotifier.unsubscribe(identifier, subscriber); - } - catch (Exception e) - { - logger.error(e.getMessage(), e); - } - } - - @Override - public void subscribe(TICCoreSubscriber subscriber) - { - this.eventNotifier.subscribe(subscriber); - } - - @Override - public void unsubscribe(TICCoreSubscriber subscriber) - { - this.eventNotifier.unsubscribe(subscriber); - } - - @Override - public List getIndentifiers(TICCoreSubscriber listener) - { - List identifiers = new ArrayList(); - - if (this.eventNotifier.getSubscribers(true, false).contains(listener)) - { - identifiers.addAll(this.getAvailableTICs()); - } - else if (this.eventNotifier.getSubscribers(false, true).contains(listener)) - { - identifiers.addAll(this.eventNotifier.getFilters(listener)); - } - - return identifiers; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCoreSubscriber - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onData(TICCoreFrame frame) - { - logger.trace("TICCore frame:\n" + frame.toString(2)); - Collection subscriberList = this.findSubscribers(frame.getIdentifier(), true); - Task task = new TaskBase() - { - @Override - public void process() - { - TICCoreBase.this.notifyOnData(frame, subscriberList); - } - }; - task.start(); - } - - @Override - public void onError(TICCoreError error) - { - logger.error("TICCore error:\n" + error.toString(2)); - Collection subscriberList = this.findSubscribers(error.getIdentifier(), true); - Task task = new TaskBase() - { - @Override - public void process() - { - TICCoreBase.this.notifyOnError(error, subscriberList); - } - }; - task.start(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// PortPlugSubscriber - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onPlugged(TICPortDescriptor descriptor) - { - logger.info("TICCore modem plugged:\n" + descriptor.toString(2)); - this.startNewStream(descriptor); - } - - @Override - public void onUnplugged(TICPortDescriptor descriptor) - { - logger.info("TICCore modem unplugged:\n" + descriptor.toString(2)); - TICIdentifier identifier = this.stopStream(descriptor); - Task task = new TaskBase() - { - @Override - public void process() - { - TICCoreBase.this.notifyOnUnpluggedAndUnsubscribe(identifier); - } - }; - task.start(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Task - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void start() - { - if (!this.isRunning()) - { - logger.info("Starting TICCore"); - logger.debug("Starting TIC port plug notifier"); - this.plugNotifier = new TICPortPlugNotifier(this.plugNotifierPeriod, this.portFinder); - this.plugNotifier.subscribe(this); - this.plugNotifier.start(); - logger.debug("TIC port plug notifier started"); - if (this.nativePortNamesOnStart != null && this.nativePortNamesOnStart.size() > 0) - { - logger.debug("Starting natives TIC port: " + Arrays.toString(this.nativePortNamesOnStart.toArray())); - for (String portName : this.nativePortNamesOnStart) - { - TICPortDescriptor descriptor = this.portFinder.findNative(portName); - logger.debug("Starting native TIC port " + portName + " : " + descriptor); - if (descriptor != null && this.findStream(descriptor) == null) - { - this.startNewStream(descriptor); - } - } - } - } - } - - @Override - public void stop() - { - logger.info("Stopping TICCore"); - logger.debug("Stopping TIC port plug notifier"); - this.plugNotifier.unsubscribe(this); - this.plugNotifier.stop(); - logger.debug("Stopping all streams"); - for (TICCoreStream stream : this.streamList) - { - stream.unsubscribe(this); - stream.stop(); - } - this.streamList.clear(); - logger.debug("Removing all subscribers"); - this.unsubscribe(this.eventNotifier.getSubscribers()); - } - - @Override - public boolean isRunning() - { - return (this.plugNotifier != null) ? this.plugNotifier.isRunning() : false; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICCoreStream findStream(TICIdentifier identifier) - { - for (TICCoreStream stream : this.streamList) - { - if (stream.getIdentifier().matches(identifier)) - { - return stream; - } - } - - return null; - } - - private TICCoreStream findStream(TICPortDescriptor descriptor) - { - for (TICCoreStream stream : this.streamList) - { - TICIdentifier identifier = stream.getIdentifier(); - if (descriptor.getPortId() != null && identifier.getPortId() != null) - { - if (descriptor.getPortId().equals(identifier.getPortId())) - { - return stream; - } - } - if (descriptor.getPortName() != null && identifier.getPortName() != null) - { - if (descriptor.getPortName().equals(identifier.getPortName())) - { - return stream; - } - } - } - - return null; - } - - private TICCoreStream startNewStream(TICPortDescriptor descriptor) - { - TICCoreStream stream = null; - logger.debug("TICCore starting new stream : " + descriptor.toString()); - try - { - stream = this.streamConstructor.newInstance(descriptor.getPortId(), descriptor.getPortName(), this.streamMode); - - stream.subscribe(this); - - stream.start(); - this.streamList.add(stream); - logger.debug("TICCore started new stream : " + descriptor.toString()); - } - catch (Exception e) - { - logger.error(e.getMessage(), e); - } - return stream; - } - - private TICIdentifier stopStream(TICPortDescriptor descriptor) - { - TICIdentifier identifier = null; - TICCoreStream stream = this.findStream(descriptor); - logger.debug("TICCore stopping new stream : " + descriptor.toString()); - if (stream != null) - { - identifier = stream.getIdentifier(); - stream.unsubscribe(this); - stream.stop(); - this.streamList.remove(stream); - logger.debug("TICCore stopped new stream : " + descriptor.toString()); - } - - return identifier; - } - - private void closeNativeStream(TICIdentifier identifier, TICCoreStream stream, ReadNextFrameSubscriber subscriber) - { - TICPortDescriptor nativeDescriptor = this.portFinder.findNative(identifier.getPortName()); - if (nativeDescriptor != null) - { - if (this.nativePortNamesOnStart == null || !this.nativePortNamesOnStart.contains(identifier.getPortName())) - { - Collection streamSubscribers = stream.getSubscribers(); - Collection ticCoreSubscribers = this.findSubscribers(identifier, false); - if (!streamSubscribers.contains(subscriber) && streamSubscribers.contains(this) && streamSubscribers.size() == 1 && ticCoreSubscribers.size() == 0) - { - this.stopStream(nativeDescriptor); - } - } - } - } - - private Collection findSubscribers(TICIdentifier sourceIdentifier, boolean globalSubscribers) - { - Predicate predicate = new Predicate() - { - @Override - public boolean test(TICIdentifier identifier) - { - return sourceIdentifier.matches(identifier); - } - }; - - return this.eventNotifier.getSubscribers(predicate, globalSubscribers); - } - - private void notifyOnUnpluggedAndUnsubscribe(TICIdentifier identifier) - { - try - { - TICCoreError error = new TICCoreError(identifier, TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), - "TICCore stream " + identifier + " has been unplugged (unsubscribe has been forced)"); - Collection subscriberList = this.findSubscribers(identifier, true); - this.notifyOnError(error, subscriberList); - subscriberList = this.findSubscribers(identifier, false); - this.unsubscribe(subscriberList); - } - catch (DataDictionaryException e) - { - logger.error(e.getMessage(), e); - } - } - - private void notifyOnData(TICCoreFrame frame, Collection subscriberList) - { - for (TICCoreSubscriber subscriber : subscriberList) - { - subscriber.onData(frame); - } - } - - private void notifyOnError(TICCoreError error, Collection subscriberList) - { - for (TICCoreSubscriber subscriber : subscriberList) - { - subscriber.onError(error); - } - } - - private void unsubscribe(Collection subscriberList) - { - for (TICCoreSubscriber subscriber : subscriberList) - { - this.unsubscribe(subscriber); - } - } -} diff --git a/src/main/java/enedis/tic/core/TICCoreError.java b/src/main/java/enedis/tic/core/TICCoreError.java deleted file mode 100644 index 2e9ad38..0000000 --- a/src/main/java/enedis/tic/core/TICCoreError.java +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.types.datadictionary.KeyDescriptorNumber; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * TICCoreError class - * - * Generated - */ -public class TICCoreError extends DataDictionaryBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_IDENTIFIER = "identifier"; - protected static final String KEY_ERROR_CODE = "errorCode"; - protected static final String KEY_ERROR_MESSAGE = "errorMessage"; - protected static final String KEY_DATA = "data"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kIdentifier; - protected KeyDescriptorNumber kErrorCode; - protected KeyDescriptorString kErrorMessage; - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected TICCoreError() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICCoreError(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICCoreError(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param identifier - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public TICCoreError(TICIdentifier identifier, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(identifier, errorCode, errorMessage, null); - } - - /** - * Constructor setting parameters to specific values - * - * @param identifier - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public TICCoreError(TICIdentifier identifier, Number errorCode, String errorMessage, DataDictionary data) throws DataDictionaryException - { - this(); - - this.setIdentifier(identifier); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get identifier - * - * @return the identifier - */ - public TICIdentifier getIdentifier() - { - return (TICIdentifier) this.data.get(KEY_IDENTIFIER); - } - - /** - * Get error code - * - * @return the error code - */ - public Number getErrorCode() - { - return (Number) this.data.get(KEY_ERROR_CODE); - } - - /** - * Get error message - * - * @return the error message - */ - public String getErrorMessage() - { - return (String) this.data.get(KEY_ERROR_MESSAGE); - } - - /** - * Get data - * - * @return the data - */ - public DataDictionaryBase getData() - { - return (DataDictionaryBase) this.data.get(KEY_DATA); - } - - /** - * Set identifier - * - * @param identifier - * @throws DataDictionaryException - */ - public void setIdentifier(TICIdentifier identifier) throws DataDictionaryException - { - this.setIdentifier((Object) identifier); - } - - /** - * Set error code - * - * @param errorCode - * @throws DataDictionaryException - */ - public void setErrorCode(Number errorCode) throws DataDictionaryException - { - this.setErrorCode((Object) errorCode); - } - - /** - * Set error message - * - * @param errorMessage - * @throws DataDictionaryException - */ - public void setErrorMessage(String errorMessage) throws DataDictionaryException - { - this.setErrorMessage((Object) errorMessage); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(DataDictionary data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setIdentifier(Object identifier) throws DataDictionaryException - { - this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); - } - - protected void setErrorCode(Object errorCode) throws DataDictionaryException - { - this.data.put(KEY_ERROR_CODE, this.kErrorCode.convert(errorCode)); - } - - protected void setErrorMessage(Object errorMessage) throws DataDictionaryException - { - this.data.put(KEY_ERROR_MESSAGE, this.kErrorMessage.convert(errorMessage)); - } - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kIdentifier = new KeyDescriptorDataDictionary(KEY_IDENTIFIER, true, TICIdentifier.class); - this.keys.add(this.kIdentifier); - - this.kErrorCode = new KeyDescriptorNumber(KEY_ERROR_CODE, true); - this.keys.add(this.kErrorCode); - - this.kErrorMessage = new KeyDescriptorString(KEY_ERROR_MESSAGE, true, false); - this.keys.add(this.kErrorMessage); - - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, DataDictionaryBase.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/core/TICCoreErrorCode.java b/src/main/java/enedis/tic/core/TICCoreErrorCode.java deleted file mode 100644 index 548a78f..0000000 --- a/src/main/java/enedis/tic/core/TICCoreErrorCode.java +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -public enum TICCoreErrorCode -{ - NO_ERROR(0), - STREAM_PORT_ID_NOT_FOUND(1), - STREAM_PORT_NAME_NOT_FOUND(2), - STREAM_PORT_DESCRIPTOR_EMPTY(3), - STREAM_MODE_NOT_DEFINED(4), - STREAM_IDENTIFIER_NOT_FOUND(5), - STREAM_UNPLUGGED(6), - DATA_READ_TIMEOUT(7), - OTHER_REASON(99); - - private int code; - - private TICCoreErrorCode(int code) - { - this.code = code; - } - - public int getCode() - { - return this.code; - } -} diff --git a/src/main/java/enedis/tic/core/TICCoreException.java b/src/main/java/enedis/tic/core/TICCoreException.java deleted file mode 100644 index aa0fa67..0000000 --- a/src/main/java/enedis/tic/core/TICCoreException.java +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.types.ExceptionBase; - -public class TICCoreException extends ExceptionBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -3285641164559292710L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreException(int code, String info) - { - super(code, info); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/tic/core/TICCoreFrame.java b/src/main/java/enedis/tic/core/TICCoreFrame.java deleted file mode 100644 index eb6c7f2..0000000 --- a/src/main/java/enedis/tic/core/TICCoreFrame.java +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorLocalDateTime; - -/** - * TICCoreFrame class - * - * Generated - */ -public class TICCoreFrame extends DataDictionaryBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_IDENTIFIER = "identifier"; - protected static final String KEY_MODE = "mode"; - protected static final String KEY_CAPTURE_DATE_TIME = "captureDateTime"; - protected static final String KEY_CONTENT = "content"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kIdentifier; - protected KeyDescriptorEnum kMode; - protected KeyDescriptorLocalDateTime kCaptureDateTime; - protected KeyDescriptorDataDictionary kContent; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected TICCoreFrame() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICCoreFrame(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICCoreFrame(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param identifier - * @param mode - * @param captureDateTime - * @param content - * @throws DataDictionaryException - */ - public TICCoreFrame(TICIdentifier identifier, TICMode mode, LocalDateTime captureDateTime, DataDictionaryBase content) throws DataDictionaryException - { - this(); - - this.setIdentifier(identifier); - this.setMode(mode); - this.setCaptureDateTime(captureDateTime); - this.setContent(content); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get identifier - * - * @return the identifier - */ - public TICIdentifier getIdentifier() - { - return (TICIdentifier) this.data.get(KEY_IDENTIFIER); - } - - /** - * Get mode - * - * @return the mode - */ - public TICMode getMode() - { - return (TICMode) this.data.get(KEY_MODE); - } - - /** - * Get capture date time - * - * @return the capture date time - */ - public LocalDateTime getCaptureDateTime() - { - return (LocalDateTime) this.data.get(KEY_CAPTURE_DATE_TIME); - } - - /** - * Get content - * - * @return the content - */ - public DataDictionaryBase getContent() - { - return (DataDictionaryBase) this.data.get(KEY_CONTENT); - } - - /** - * Set identifier - * - * @param identifier - * @throws DataDictionaryException - */ - public void setIdentifier(TICIdentifier identifier) throws DataDictionaryException - { - this.setIdentifier((Object) identifier); - } - - /** - * Set mode - * - * @param mode - * @throws DataDictionaryException - */ - public void setMode(TICMode mode) throws DataDictionaryException - { - this.setMode((Object) mode); - } - - /** - * Set capture date time - * - * @param captureDateTime - * @throws DataDictionaryException - */ - public void setCaptureDateTime(LocalDateTime captureDateTime) throws DataDictionaryException - { - this.setCaptureDateTime((Object) captureDateTime); - } - - /** - * Set content - * - * @param content - * @throws DataDictionaryException - */ - public void setContent(DataDictionaryBase content) throws DataDictionaryException - { - this.setContent((Object) content); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setIdentifier(Object identifier) throws DataDictionaryException - { - this.data.put(KEY_IDENTIFIER, this.kIdentifier.convert(identifier)); - } - - protected void setMode(Object mode) throws DataDictionaryException - { - this.data.put(KEY_MODE, this.kMode.convert(mode)); - } - - protected void setCaptureDateTime(Object captureDateTime) throws DataDictionaryException - { - this.data.put(KEY_CAPTURE_DATE_TIME, this.kCaptureDateTime.convert(captureDateTime)); - } - - protected void setContent(Object content) throws DataDictionaryException - { - this.data.put(KEY_CONTENT, this.kContent.convert(content)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kIdentifier = new KeyDescriptorDataDictionary(KEY_IDENTIFIER, true, TICIdentifier.class); - this.keys.add(this.kIdentifier); - - this.kMode = new KeyDescriptorEnum(KEY_MODE, true, TICMode.class); - this.keys.add(this.kMode); - - this.kCaptureDateTime = new KeyDescriptorLocalDateTime(KEY_CAPTURE_DATE_TIME, true); - this.keys.add(this.kCaptureDateTime); - - this.kContent = new KeyDescriptorDataDictionary(KEY_CONTENT, true, DataDictionaryBase.class); - this.keys.add(this.kContent); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/core/TICCoreStream.java b/src/main/java/enedis/tic/core/TICCoreStream.java deleted file mode 100644 index 8a72a48..0000000 --- a/src/main/java/enedis/tic/core/TICCoreStream.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.util.task.Notifier; -import enedis.lab.util.task.Task; - -/** - * TICCore stream interface - */ -public interface TICCoreStream extends Notifier, Task -{ - /** - * Get identifier - * - * @return The TICIdentifier associated with the stream - */ - public TICIdentifier getIdentifier(); -} diff --git a/src/main/java/enedis/tic/core/TICCoreStreamBase.java b/src/main/java/enedis/tic/core/TICCoreStreamBase.java deleted file mode 100644 index 21ff386..0000000 --- a/src/main/java/enedis/tic/core/TICCoreStreamBase.java +++ /dev/null @@ -1,378 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.time.LocalDateTime; -import java.util.Collection; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.io.channels.Channel; -import enedis.lab.io.channels.ChannelException; -import enedis.lab.io.datastreams.DataStreamBase; -import enedis.lab.io.datastreams.DataStreamDirection; -import enedis.lab.io.datastreams.DataStreamException; -import enedis.lab.io.datastreams.DataStreamListener; -import enedis.lab.io.datastreams.DataStreamStatus; -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.io.tic.TICPortFinderBase; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.protocol.tic.channels.ChannelTICSerialPort; -import enedis.lab.protocol.tic.channels.ChannelTICSerialPortConfiguration; -import enedis.lab.protocol.tic.datastreams.TICInputStream; -import enedis.lab.protocol.tic.datastreams.TICStreamConfiguration; -import enedis.lab.protocol.tic.frame.TICFrame; -import enedis.lab.protocol.tic.frame.TICFrameDataSet; -import enedis.lab.protocol.tic.frame.historic.TICFrameHistoric; -import enedis.lab.protocol.tic.frame.standard.TICFrameStandard; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.util.task.Notifier; -import enedis.lab.util.task.NotifierBase; -import enedis.lab.util.task.Task; - -/** - * TICCore stream implementation - */ -public class TICCoreStreamBase implements TICCoreStream, Task, DataStreamListener -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICIdentifier identifier; - private DataStreamBase stream; - private Channel channel; - private Notifier notifier; - static private Logger logger = LogManager.getLogger(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreStreamBase(String portId, String portName, TICMode ticMode) throws TICCoreException - { - TICPortDescriptor descriptor = null; - - if (portId != null) - { - descriptor = TICPortFinderBase.getInstance().findByPortId(portId); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_ID_NOT_FOUND.getCode(), "TICCore stream port id " + portId + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - } - else if (portName != null) - { - descriptor = TICPortFinderBase.getInstance().findByPortName(portName); - if (descriptor == null) - { - descriptor = TICPortFinderBase.getInstance().findNative(portName); - if (descriptor == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_NAME_NOT_FOUND.getCode(), - "TICCore stream port name " + portName + " not found!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - } - } - else - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_PORT_DESCRIPTOR_EMPTY.getCode(), "TICCore stream port descriptor empty!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - if (ticMode == null) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.STREAM_MODE_NOT_DEFINED.getCode(), "TICCore stream mode not defined!"); - logger.error(exception.getMessage(), exception); - throw exception; - } - try - { - this.identifier = new TICIdentifier(portId, portName, null); - this.channel = new ChannelTICSerialPort(new ChannelTICSerialPortConfiguration("Channel@" + descriptor.getPortName(), portName, ticMode)); - this.stream = new TICInputStream( - new TICStreamConfiguration("Stream@" + descriptor.getPortName(), DataStreamDirection.INPUT, "Channel@" + descriptor.getPortName(), ticMode)); - this.stream.setChannel(this.channel); - this.notifier = new NotifierBase(); - logger = LogManager.getLogger(); - } - catch (DataDictionaryException | ChannelException | DataStreamException e) - { - TICCoreException exception = new TICCoreException(TICCoreErrorCode.OTHER_REASON.getCode(), "TICCore stream instanciation failed : " + e.getMessage()); - logger.error(exception.getMessage(), exception); - throw exception; - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCoreStream - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICIdentifier getIdentifier() - { - TICIdentifier identifier; - - synchronized (this.identifier) - { - identifier = (TICIdentifier) this.identifier.clone(); - } - - return identifier; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Notifier - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Collection getSubscribers() - { - return this.notifier.getSubscribers(); - } - - @Override - public boolean hasSubscriber(TICCoreSubscriber subscriber) - { - return this.notifier.hasSubscriber(subscriber); - } - - @Override - public void subscribe(TICCoreSubscriber subscriber) - { - this.notifier.subscribe(subscriber); - } - - @Override - public void unsubscribe(TICCoreSubscriber subscriber) - { - this.notifier.unsubscribe(subscriber); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataStreamListener - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onDataReceived(String dataStreamName, DataDictionary data) - { - TICCoreFrame frame = this.createFrame(data); - - if (frame != null) - { - this.notifyOnData(frame); - } - else - { - logger.debug("Frame creation skipped due to null TICFrame"); - } - } - - @Override - public void onDataSent(String dataStreamName, DataDictionary data) - { - } - - @Override - public void onStatusChanged(String dataStreamName, DataStreamStatus newStatus) - { - } - - @Override - public void onErrorDetected(String dataStreamName, int errorCode, String errorMessage, DataDictionary data) - { - TICCoreError error = this.createError(errorCode, errorMessage, data); - this.notifyOnError(error); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Task - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void start() - { - this.channel.start(); - try - { - this.stream.open(); - this.stream.subscribe(this); - } - catch (DataStreamException e) - { - logger.error(e.getMessage()); - } - } - - @Override - public void stop() - { - this.stream.unsubscribe(this); - try - { - this.stream.close(); - } - catch (DataStreamException e) - { - logger.error(e.getMessage()); - } - this.channel.stop(); - } - - @Override - public boolean isRunning() - { - return this.channel.isRunning(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TICCoreFrame createFrame(DataDictionary data) - { - TICCoreFrame frame = new TICCoreFrame(); - try - { - frame.setCaptureDateTime(LocalDateTime.now()); - TICFrame ticFrame = (TICFrame) data.get(TICInputStream.KEY_DATA); - - if (ticFrame == null) - { - logger.warn("TICFrame is null. Skipping frame creation."); - return null; - } - - DataDictionaryBase content = new DataDictionaryBase(); - for (TICFrameDataSet frameDataSet : ticFrame.getDataSetList()) - { - String label = frameDataSet.getLabel(); - content.set(label, ticFrame.getData(label)); - } - frame.setContent(content); - String serialNumber = null; - if (ticFrame instanceof TICFrameStandard) - { - frame.setMode(TICMode.STANDARD); - serialNumber = (String) content.get("ADSC"); - } - else if (ticFrame instanceof TICFrameHistoric) - { - frame.setMode(TICMode.HISTORIC); - serialNumber = (String) content.get("ADCO"); - } - synchronized (this.identifier) - { - this.identifier.setSerialNumber(serialNumber); - frame.setIdentifier(this.identifier.clone()); - } - } - catch (DataDictionaryException e) - { - logger.error(e.getMessage()); - frame = null; - } - - return frame; - } - - private TICCoreError createError(int errorCode, String errorMessage, DataDictionary data) - { - TICCoreError error = null; - try - { - error = new TICCoreError(this.getIdentifier(), errorCode, errorMessage, data); - } - catch (DataDictionaryException e) - { - logger.error(e.getMessage()); - } - - return error; - } - - private void notifyOnData(TICCoreFrame frame) - { - if (frame == null) - { - return; - } - for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) - { - subscriber.onData(frame); - } - } - - private void notifyOnError(TICCoreError error) - { - if (error == null) - { - return; - } - for (TICCoreSubscriber subscriber : this.notifier.getSubscribers()) - { - subscriber.onError(error); - } - } -} diff --git a/src/main/java/enedis/tic/core/TICCoreSubscriber.java b/src/main/java/enedis/tic/core/TICCoreSubscriber.java deleted file mode 100644 index 4adc14e..0000000 --- a/src/main/java/enedis/tic/core/TICCoreSubscriber.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.util.task.Subscriber; - -/** - * TICCore subscriber interface - */ -public interface TICCoreSubscriber extends Subscriber -{ - /** - * Notify when data - * - * @param frame - * the frame received - */ - public void onData(TICCoreFrame frame); - - /** - * Notify when error - * - * @param error - * the error detected - */ - public void onError(TICCoreError error); -} diff --git a/src/main/java/enedis/tic/core/TICIdentifier.java b/src/main/java/enedis/tic/core/TICIdentifier.java deleted file mode 100644 index 7f83c81..0000000 --- a/src/main/java/enedis/tic/core/TICIdentifier.java +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorString; - -/** - * TICIdentifier class - * - * Generated - */ -public class TICIdentifier extends DataDictionaryBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_PORT_ID = "portId"; - protected static final String KEY_PORT_NAME = "portName"; - protected static final String KEY_SERIAL_NUMBER = "serialNumber"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorString kPortId; - protected KeyDescriptorString kPortName; - protected KeyDescriptorString kSerialNumber; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected TICIdentifier() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TICIdentifier(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TICIdentifier(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param portId - * @param portName - * @param serialNumber - * @throws DataDictionaryException - */ - public TICIdentifier(String portId, String portName, String serialNumber) throws DataDictionaryException - { - this(); - - this.setPortId((Object) portId); - this.setPortName((Object) portName); - this.setSerialNumber((Object) serialNumber); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// DataDictionaryBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (this.getPortId() == null && this.getPortName() == null && this.getSerialNumber() == null) - { - throw new DataDictionaryException("Empty TICIdentifier not allowed!"); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public boolean matches(TICIdentifier identifier) - { - if (identifier != null) - { - if (this.getSerialNumber() != null && identifier.getSerialNumber() != null) - { - return this.getSerialNumber().equals(identifier.getSerialNumber()); - } - if (this.getPortId() != null && identifier.getPortId() != null) - { - return this.getPortId().equals(identifier.getPortId()); - } - if (this.getPortName() != null && identifier.getPortName() != null) - { - return this.getPortName().equals(identifier.getPortName()); - } - } - - return false; - } - - /** - * Get port id - * - * @return the port id - */ - public String getPortId() - { - return (String) this.data.get(KEY_PORT_ID); - } - - /** - * Get port name - * - * @return the port name - */ - public String getPortName() - { - return (String) this.data.get(KEY_PORT_NAME); - } - - /** - * Get serial number - * - * @return the serial number - */ - public String getSerialNumber() - { - return (String) this.data.get(KEY_SERIAL_NUMBER); - } - - /** - * Set port id - * - * @param portId - * @throws DataDictionaryException - */ - public void setPortId(String portId) throws DataDictionaryException - { - if (portId == null && this.getPortName() == null && this.getSerialNumber() == null) - { - throw new DataDictionaryException("Cannot set null portId because empty TICIdentifier not allowed!"); - } - this.setPortId((Object) portId); - } - - /** - * Set port name - * - * @param portName - * @throws DataDictionaryException - */ - public void setPortName(String portName) throws DataDictionaryException - { - if (this.getPortId() == null && portName == null && this.getSerialNumber() == null) - { - throw new DataDictionaryException("Cannot set null portName because empty TICIdentifier not allowed!"); - } - this.setPortName((Object) portName); - } - - /** - * Set serial number - * - * @param serialNumber - * @throws DataDictionaryException - */ - public void setSerialNumber(String serialNumber) throws DataDictionaryException - { - if (this.getPortId() == null && this.getPortName() == null && serialNumber == null) - { - throw new DataDictionaryException("Cannot set null serialNumber because empty TICIdentifier not allowed!"); - } - this.setSerialNumber((Object) serialNumber); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setPortId(Object portId) throws DataDictionaryException - { - this.data.put(KEY_PORT_ID, this.kPortId.convert(portId)); - } - - protected void setPortName(Object portName) throws DataDictionaryException - { - this.data.put(KEY_PORT_NAME, this.kPortName.convert(portName)); - } - - protected void setSerialNumber(Object serialNumber) throws DataDictionaryException - { - this.data.put(KEY_SERIAL_NUMBER, this.kSerialNumber.convert(serialNumber)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kPortId = new KeyDescriptorString(KEY_PORT_ID, false, false); - this.keys.add(this.kPortId); - - this.kPortName = new KeyDescriptorString(KEY_PORT_NAME, false, false); - this.keys.add(this.kPortName); - - this.kSerialNumber = new KeyDescriptorString(KEY_SERIAL_NUMBER, false, false); - this.keys.add(this.kSerialNumber); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java b/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java deleted file mode 100644 index 82dcd19..0000000 --- a/src/main/java/enedis/tic/service/TIC2WebSocketApplication.java +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service; - -import java.io.File; -import java.io.InputStream; -import java.util.Properties; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.LoggerContext; - -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.tic.core.TICCore; -import enedis.tic.core.TICCoreBase; -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.client.TIC2WebSocketClientPoolBase; -import enedis.tic.service.config.TIC2WebSocketConfiguration; -import enedis.tic.service.netty.TIC2WebSocketServer; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandlerBase; -import picocli.CommandLine; -import picocli.CommandLine.IVersionProvider; -import picocli.CommandLine.ParseResult; - -/** - * Class used to handle the application - */ -public class TIC2WebSocketApplication -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Project properties (see file project.properties) - */ - public static final Properties PROJECT_PROPERTIES = new Properties(); - - static - { - try - { - InputStream stream = TIC2WebSocketApplication.class.getClassLoader().getResourceAsStream("TIC2WebSocket.properties"); - - if (stream == null) - { - System.err.println("Can't find projet property file!"); - } - else - { - TIC2WebSocketApplication.PROJECT_PROPERTIES.load(stream); - } - } - catch (Exception exception) - { - System.err.println("Can't load projet property file!"); - exception.printStackTrace(System.err); - } - } - - /** - * Project name ("project_name" from project.properties) - */ - public static final String NAME = PROJECT_PROPERTIES.getProperty("project_name", ""); - - /** - * Project version ("project_version" from project.properties) - */ - public static final String VERSION = PROJECT_PROPERTIES.getProperty("project_version", ""); - - /** - * Project description ("project_description" from project.properties) - */ - public static final String DESCRIPTION = PROJECT_PROPERTIES.getProperty("project_description", ""); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Program entry point - * - * @param args - * Command line arguments - */ - public static void main(String[] args) - { - try - { - TIC2WebSocketApplication application = new TIC2WebSocketApplication(args); - int result = application.run(); - System.exit(result); - } - catch (Exception exception) - { - exception.printStackTrace(System.err); - System.exit(-1); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private String[] commandLineArgs; - private CommandLine commandLineParser; - private TIC2WebSocketCommandLine commandLine; - private Logger logger; - private boolean started; - private TIC2WebSocketConfiguration configuration; - - private TIC2WebSocketClientPool clientPool; - private TIC2WebSocketServer server; - private TIC2WebSocketRequestHandler requestHandler; - private TICCore ticCore; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Application constructor - * - * @param args - * Command line arguments - */ - public TIC2WebSocketApplication(String[] args) - { - this.commandLineArgs = args; - this.commandLine = new TIC2WebSocketCommandLine(); - this.commandLineParser = new CommandLine(this.commandLine); - this.commandLineParser.setCommandName(TIC2WebSocketApplication.NAME); - this.commandLineParser.getCommandSpec().versionProvider(new IVersionProvider() - { - @Override - public String[] getVersion() throws Exception - { - return new String[] { TIC2WebSocketApplication.NAME + " v" + TIC2WebSocketApplication.VERSION }; - } - }); - this.commandLineParser.registerConverter(Level.class, new TIC2WebSocketCommandLine.VerboseLevelConverter()); - this.updateLoggerConfiguration(Level.ERROR); - this.started = false; - } - - /** - * Application execution - * - * @return 0 if success, else an error code - * @throws InterruptedException - * If the application thread gets interrupted - * @see TIC2WebSocketApplicationErrorCode - */ - public int run() throws InterruptedException - { - int result = this.init(); - - if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) - { - return result; - } - result = this.start(); - if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) - { - return result; - } - - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() - { - @Override - public void run() - { - TIC2WebSocketApplication.this.stop(); - } - })); - - while (this.isStarted()) - { - Thread.sleep(1000); - } - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - /** - * Application initialization - * - * @return 0 if success, else an error code - * @see TIC2WebSocketApplicationErrorCode - */ - public int init() - { - if (this.isStarted()) - { - this.logger.error("Application already started!"); - return TIC2WebSocketApplicationErrorCode.APPLICATION_ALREADY_STARTED.code(); - } - int result = this.parseCommandLine(); - if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) - { - return result; - } - result = this.updateLoggerConfiguration(this.commandLine.verboseLevel); - if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) - { - return result; - } - - result = this.loadConfiguration(); - if (result != TIC2WebSocketApplicationErrorCode.NO_ERROR.code()) - { - return result; - } - - this.logger.info(TIC2WebSocketApplication.NAME + " initialized"); - - this.ticCore = new TICCoreBase(this.configuration.getTicMode(), this.configuration.getTicPortNames()); - this.clientPool = new TIC2WebSocketClientPoolBase(); - this.requestHandler = new TIC2WebSocketRequestHandlerBase(this.ticCore); - - this.server = new TIC2WebSocketServer("localhost", this.configuration.getServerPort().intValue(), - this.clientPool, this.requestHandler); - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - /** - * Application start-up - * - * @return 0 if success, else an error code - */ - public int start() - { - if (this.commandLineParser.isUsageHelpRequested()) - { - this.commandLineParser.usage(System.out); - } - if (this.commandLineParser.isVersionHelpRequested()) - { - this.commandLineParser.printVersionHelp(System.out); - } - this.logger.info(TIC2WebSocketApplication.NAME + " starting"); - - try - { - this.ticCore.start(); - this.server.start(); - } - catch (Exception exception) - { - this.logger.error(exception.getMessage(), exception); - return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); - } - - this.logger.info(TIC2WebSocketApplication.NAME + " started"); - this.started = true; - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - /** - * Application start-up indicator - * - * @return true if the application is started, false otherwise - */ - public boolean isStarted() - { - return this.started; - } - - /** - * Application stop - * - * @return 0 if success, else an error code - * @see TIC2WebSocketApplicationErrorCode - */ - public int stop() - { - if (!this.isStarted()) - { - this.logger.error("Application not started!"); - return TIC2WebSocketApplicationErrorCode.APPLICATION_NOT_STARTED.code(); - } - - try - { - this.server.stop(); - this.ticCore.stop(); - } - catch (Exception exception) - { - this.logger.error(exception.getMessage(), exception); - return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); - } - - this.logger.info(TIC2WebSocketApplication.NAME + " stopped"); - this.started = false; - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private int parseCommandLine() - { - try - { - ParseResult parseResult = this.commandLineParser.parseArgs(this.commandLineArgs); - if (parseResult.errors().size() > 0) - { - this.logger.error("Invalid command line!"); - for (int i = 0; i < parseResult.errors().size(); i++) - { - this.logger.error(parseResult.errors().get(i).getMessage()); - } - return TIC2WebSocketApplicationErrorCode.COMMAND_LINE_INVALID.code(); - } - } - catch (Exception exception) - { - this.logger.error("Invalid command line!"); - this.logger.error(exception.getMessage()); - return TIC2WebSocketApplicationErrorCode.COMMAND_LINE_INVALID.code(); - } - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - private int updateLoggerConfiguration(Level level) - { - try - { - System.setProperty("log4j.configurationFile", "log4j2-" + level.name().toLowerCase() + ".xml"); - LoggerContext context = (LoggerContext) LogManager.getContext(false); - context.reconfigure(); - this.logger = LogManager.getLogger(); - } - catch (Exception exception) - { - System.err.println("Log configuration update failure!"); - System.err.println(exception.getMessage()); - return TIC2WebSocketApplicationErrorCode.UPDATE_LOGGER_CONFIGURATION_FAILURE.code(); - } - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - } - - private int loadConfiguration() - { - String configFile = null; - - if (this.commandLine.configFile != null) - { - configFile = this.commandLine.configFile; - } - else - { - configFile = System.getProperty("configFile", NAME + "Configuration.json"); - } - File configPath = new File(configFile); - if (!configPath.exists()) - { - this.logger.error("No configuration file path '" + configPath.getAbsolutePath() + "' not found"); - return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); - } - try - { - this.logger.info("Loading configuration file " + configPath); - this.configuration = (TIC2WebSocketConfiguration) DataDictionaryBase.fromFile(configPath, TIC2WebSocketConfiguration.class); - } - catch (Exception exception) - { - this.logger.error("Loading configuration file " + configPath + " failed", exception); - return TIC2WebSocketApplicationErrorCode.LOAD_CONFIGURATION_FAILURE.code(); - } - - return TIC2WebSocketApplicationErrorCode.NO_ERROR.code(); - - } -} \ No newline at end of file diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java b/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java deleted file mode 100644 index c2b970c..0000000 --- a/src/main/java/enedis/tic/service/TIC2WebSocketApplicationErrorCode.java +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service; - -/** - * Application error codes list - */ -public enum TIC2WebSocketApplicationErrorCode -{ - /** Success code */ - NO_ERROR(0), - /** Command line invalid code */ - COMMAND_LINE_INVALID(1), - /** Update logger configuration code */ - UPDATE_LOGGER_CONFIGURATION_FAILURE(2), - /** Application already started code */ - APPLICATION_ALREADY_STARTED(3), - /** Application not started code */ - APPLICATION_NOT_STARTED(4), - /** Load configuration failure code */ - LOAD_CONFIGURATION_FAILURE(5); - - private int code; - - private TIC2WebSocketApplicationErrorCode(int code) - { - this.code = code; - } - - /** - * Return error code - * - * @return code - */ - public int code() - { - return this.code; - } - -} diff --git a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java b/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java deleted file mode 100644 index ac5474b..0000000 --- a/src/main/java/enedis/tic/service/TIC2WebSocketCommandLine.java +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service; - -import org.apache.logging.log4j.Level; - -import picocli.CommandLine.Command; -import picocli.CommandLine.ITypeConverter; -import picocli.CommandLine.Option; -import picocli.CommandLine.TypeConversionException; - -/** - * Class used for command line - */ -@Command() -public class TIC2WebSocketCommandLine -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - static class VerboseLevelConverter implements ITypeConverter - { - @Override - public Level convert(String value) throws Exception - { - try - { - switch (Integer.parseInt(value)) - { - case 0: - return Level.OFF; - case 1: - return Level.FATAL; - case 2: - return Level.ERROR; - case 3: - return Level.WARN; - case 4: - return Level.INFO; - case 5: - return Level.DEBUG; - case 6: - return Level.TRACE; - default: - throw new TypeConversionException(value); - } - } - catch (NumberFormatException exception) - { - Level level = Level.getLevel(value); - if (level == null) - { - throw new TypeConversionException(value); - } - return level; - } - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Option(names = { "-h", "--help" }, usageHelp = true, description = "Display help") - boolean helpRequested = false; - - @Option(names = { "--version" }, versionHelp = true, description = "Display version") - boolean versionRequested = false; - - //@formatter:off - @Option(names = { "-v", - "--verbose" }, defaultValue = "2", paramLabel = "LEVEL", - description = "Define verbosity level:\n" + - "0 ou OFF = muted\n" + - "1 ou FATAL = critical errors\n" + - "2 ou ERROR = error (default)\n" + - "3 ou WARN = warnings\n" + - "4 ou INFO = informations\n" + - "5 ou DEBUG = debugging\n" + - "6 ou TRACE = traces\n") - Level verboseLevel; - //@formatter:on - - @Option(names = { "--configFile" }, paramLabel = "PATH", description = "Set configuration file") - String configFile = null; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -}; diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java deleted file mode 100644 index b45040a..0000000 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClient.java +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.client; - -import java.time.LocalDateTime; - -import io.netty.channel.Channel; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Event; -import enedis.tic.core.TICCoreError; -import enedis.tic.core.TICCoreFrame; -import enedis.tic.core.TICCoreSubscriber; -import enedis.tic.service.endpoint.EventSender; -import enedis.tic.service.message.EventOnError; -import enedis.tic.service.message.EventOnTICData; - -/** - * TIC2WebSocket client - */ -public class TIC2WebSocketClient implements TICCoreSubscriber -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Logger logger; - private Channel channel; - private EventSender eventSender; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param channel - * @param eventSender - */ - public TIC2WebSocketClient(Channel channel, EventSender eventSender) { - super(); - this.logger = LogManager.getLogger(this.getClass()); - this.channel = channel; - this.eventSender = eventSender; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCoreSubscriber - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onData(TICCoreFrame frame) - { - try - { - Event event = new EventOnTICData(LocalDateTime.now(), frame); - this.eventSender.sendEvent(this.channel, event); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } - - @Override - public void onError(TICCoreError error) - { - try - { - Event event = new EventOnError(LocalDateTime.now(), error); - this.eventSender.sendEvent(this.channel, event); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get client websocket channel - * - * @return websocket channel - */ - public Channel getChannel() - { - return this.channel; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java deleted file mode 100644 index 1518ae4..0000000 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPool.java +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.client; - -import java.util.Optional; - -import io.netty.channel.Channel; - -import enedis.tic.service.endpoint.EventSender; - -/** - * TIC2WebSocket client pool interface - */ -public interface TIC2WebSocketClientPool -{ - /** - * Get client from channel id - * - * @param channelId - * @return client - */ - public Optional getClient(String channelId); - - /** - * Check if a client with the given channel id exists - * - * @param channelId - * @return true if a client with the given channel id exists - */ - public boolean exists(String channelId); - - /** - * Create a new client - * - * @param channel - * @param sender - * @return new client - */ - public TIC2WebSocketClient createClient(Channel channel, EventSender sender); - - /** - * Remove client with the given channel id - * - * @param channelId - */ - public void remove(String channelId); -} diff --git a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java b/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java deleted file mode 100644 index 7cf68db..0000000 --- a/src/main/java/enedis/tic/service/client/TIC2WebSocketClientPoolBase.java +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.client; - -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; - -import io.netty.channel.Channel; - -import enedis.tic.service.endpoint.EventSender; - -/** - * Client pool - */ -public class TIC2WebSocketClientPoolBase implements TIC2WebSocketClientPool -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Set clients; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - */ - public TIC2WebSocketClientPoolBase() - { - super(); - this.clients = new CopyOnWriteArraySet(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Optional getClient(String channelId) - { - // @formatter:off - return this.clients.stream() - .filter(c -> c.getChannel().id().asLongText().equals(channelId)) - .findAny(); - // @formatter:on - } - - @Override - public boolean exists(String channelId) - { - return this.getClient(channelId).isPresent(); - } - - @Override - public TIC2WebSocketClient createClient(Channel channel, EventSender sender) - { - this.checkArguments(channel, sender); - - String channelId = channel.id().asLongText(); - Optional client = this.getClient(channelId); - if (!client.isPresent()) - { - TIC2WebSocketClient newClient = new TIC2WebSocketClient(channel, sender); - this.clients.add(newClient); - return newClient; - } - else - { - return client.get(); - } - } - - @Override - public void remove(String channelId) - { - Optional client = this.getClient(channelId); - if (client.isPresent()) - { - this.clients.remove(client.get()); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void checkArguments(Channel channel, EventSender sender) - { - if (channel == null || sender == null) { - throw new IllegalArgumentException("Cannot create client with a null Channel or null EventSender"); - } - } -} diff --git a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java b/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java deleted file mode 100644 index 6fa8e44..0000000 --- a/src/main/java/enedis/tic/service/config/TIC2WebSocketConfiguration.java +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.config; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.configuration.ConfigurationBase; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorEnum; -import enedis.lab.types.datadictionary.KeyDescriptorListMinMaxSize; -import enedis.lab.types.datadictionary.KeyDescriptorNumberMinMax; - -/** - * TIC2WebSocketConfiguration class - * - * Generated - */ -public class TIC2WebSocketConfiguration extends ConfigurationBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_SERVER_PORT = "serverPort"; - protected static final String KEY_TIC_MODE = "ticMode"; - protected static final String KEY_TIC_PORT_NAMES = "ticPortNames"; - - private static final Number SERVER_PORT_MIN = 1; - private static final Number SERVER_PORT_MAX = 65535; - private static final int TIC_PORT_NAMES_MIN_SIZE = 1; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorNumberMinMax kServerPort; - protected KeyDescriptorEnum kTicMode; - protected KeyDescriptorListMinMaxSize kTicPortNames; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected TIC2WebSocketConfiguration() - { - super(); - this.loadKeyDescriptors(); - - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public TIC2WebSocketConfiguration(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public TIC2WebSocketConfiguration(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * - * Constructor setting configuration name/file and parameters to default values - * - * @param name - * the configuration name - * @param file - * the configuration file - */ - public TIC2WebSocketConfiguration(String name, File file) - { - this(); - this.init(name, file); - } - - /** - * Constructor setting parameters to specific values - * - * @param serverPort - * @param ticMode - * @param ticPortNames - * @throws DataDictionaryException - */ - public TIC2WebSocketConfiguration(Number serverPort, TICMode ticMode, List ticPortNames) throws DataDictionaryException - { - this(); - - this.setServerPort(serverPort); - this.setTicMode(ticMode); - this.setTicPortNames(ticPortNames); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get server port - * - * @return the server port - */ - public Number getServerPort() - { - return (Number) this.data.get(KEY_SERVER_PORT); - } - - /** - * Get tic mode - * - * @return the tic mode - */ - public TICMode getTicMode() - { - return (TICMode) this.data.get(KEY_TIC_MODE); - } - - /** - * Get tic port names - * - * @return the tic port names - */ - @SuppressWarnings("unchecked") - public List getTicPortNames() - { - return (List) this.data.get(KEY_TIC_PORT_NAMES); - } - - /** - * Set server port - * - * @param serverPort - * @throws DataDictionaryException - */ - public void setServerPort(Number serverPort) throws DataDictionaryException - { - this.setServerPort((Object) serverPort); - } - - /** - * Set tic mode - * - * @param ticMode - * @throws DataDictionaryException - */ - public void setTicMode(TICMode ticMode) throws DataDictionaryException - { - this.setTicMode((Object) ticMode); - } - - /** - * Set tic port names - * - * @param ticPortNames - * @throws DataDictionaryException - */ - public void setTicPortNames(List ticPortNames) throws DataDictionaryException - { - this.setTicPortNames((Object) ticPortNames); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setServerPort(Object serverPort) throws DataDictionaryException - { - this.data.put(KEY_SERVER_PORT, this.kServerPort.convert(serverPort)); - } - - protected void setTicMode(Object ticMode) throws DataDictionaryException - { - this.data.put(KEY_TIC_MODE, this.kTicMode.convert(ticMode)); - } - - protected void setTicPortNames(Object ticPortNames) throws DataDictionaryException - { - List portNames = this.kTicPortNames.convert(ticPortNames); - for (int i = 0; i < portNames.size(); i++) - { - String portName = portNames.get(i); - if (portName == null) - { - throw new DataDictionaryException("Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + "(" + portName + ") cannot be null"); - } - else if (portName.isEmpty()) - { - throw new DataDictionaryException("Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + "(" + portName + ") cannot be empty"); - } - else if (i != portNames.lastIndexOf(portName)) - { - throw new DataDictionaryException( - "Key " + KEY_TIC_PORT_NAMES + ": value at index " + i + " and " + portNames.lastIndexOf(portName) + "(" + portName + ") are identical"); - } - } - this.data.put(KEY_TIC_PORT_NAMES, portNames); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kServerPort = new KeyDescriptorNumberMinMax(KEY_SERVER_PORT, true, SERVER_PORT_MIN, SERVER_PORT_MAX); - this.keys.add(this.kServerPort); - - this.kTicMode = new KeyDescriptorEnum(KEY_TIC_MODE, false, TICMode.class); - this.keys.add(this.kTicMode); - - this.kTicPortNames = new KeyDescriptorListMinMaxSize(KEY_TIC_PORT_NAMES, false, String.class, TIC_PORT_NAMES_MIN_SIZE, null); - this.keys.add(this.kTicPortNames); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/endpoint/EventSender.java b/src/main/java/enedis/tic/service/endpoint/EventSender.java deleted file mode 100644 index 2437707..0000000 --- a/src/main/java/enedis/tic/service/endpoint/EventSender.java +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.endpoint; - -import io.netty.channel.Channel; - -import enedis.lab.util.message.Event; - -/** - * Event sender interface - */ -public interface EventSender -{ - /** - * Send event - * - * @param channel - * @param event - */ - public void sendEvent(Channel channel, Event event); -} diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java deleted file mode 100644 index 5eca674..0000000 --- a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointErrorCode.java +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.endpoint; - -/** - * End point error codes list - */ -public enum TIC2WebSocketEndPointErrorCode -{ - /** Success code */ - NO_ERROR(0), - /** Unvalid message */ - INVALID_MESSAGE_FORMAT(-1), - /** Message type missing */ - MESSAGE_TYPE_MISSING(-2), - /** Message name missing */ - MESSAGE_NAME_MISSING(-3), - /** Message type invalid */ - MESSAGE_TYPE_INVALID(-4), - /** Unsupported message */ - UNSUPPORTED_MESSAGE(-5), - /** Invalid messge content */ - INVALID_MESSAGE_CONTENT(-6), - /** Internal error */ - INTERNAL_ERROR(-7), - /** Subscription fail */ - SUBSCRIPTION_FAIL(-10), - /** Unsubscription fail */ - UNSUBSCRIPTION_FAIL(-11), - /** TIC identifier not found */ - IDENTIFIER_NOT_FOUND(-12), - /** Read TIC timeout */ - READ_TIMEOUT(-13); - - private int value; - - private TIC2WebSocketEndPointErrorCode(int value) - { - this.value = value; - } - - /** - * Return error code - * - * @return code - */ - public int value() - { - return this.value; - } - -} diff --git a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java b/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java deleted file mode 100644 index 4e7b38c..0000000 --- a/src/main/java/enedis/tic/service/endpoint/TIC2WebSocketEndPointException.java +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.endpoint; - -/** - * @author Antoine - * - */ -/** - * TIC2WebSocket end point Exception - */ -public class TIC2WebSocketEndPointException extends Exception -{ - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final long serialVersionUID = -2263755971102386572L; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TIC2WebSocketEndPointErrorCode code; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param code - */ - public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code) - { - super(); - this.code = code; - } - - /** - * Constructor using message and cause - * - * @param code - * - * @param message - * @param cause - */ - public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, String message, Throwable cause) - { - super(message, cause); - this.code = code; - } - - /** - * Constructor using message - * - * @param code - * - * @param message - */ - public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, String message) - { - super(message); - this.code = code; - } - - /** - * Constructor using cause - * - * @param code - * - * @param cause - */ - public TIC2WebSocketEndPointException(TIC2WebSocketEndPointErrorCode code, Throwable cause) - { - super(cause); - this.code = code; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get error code - * - * @return error code - */ - public TIC2WebSocketEndPointErrorCode getCode() - { - return this.code; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/main/java/enedis/tic/service/message/EventOnError.java b/src/main/java/enedis/tic/service/message/EventOnError.java deleted file mode 100644 index da39693..0000000 --- a/src/main/java/enedis/tic/service/message/EventOnError.java +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Event; -import enedis.tic.core.TICCoreError; - -/** - * EventOnError class - * - * Generated - */ -public class EventOnError extends Event -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "OnError"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected EventOnError() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public EventOnError(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public EventOnError(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param data - * @throws DataDictionaryException - */ - public EventOnError(LocalDateTime dateTime, TICCoreError data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Event - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - public TICCoreError getData() - { - return (TICCoreError) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICCoreError data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreError.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/EventOnTICData.java b/src/main/java/enedis/tic/service/message/EventOnTICData.java deleted file mode 100644 index c47d701..0000000 --- a/src/main/java/enedis/tic/service/message/EventOnTICData.java +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Event; -import enedis.tic.core.TICCoreFrame; - -/** - * EventOnTICData class - * - * Generated - */ -public class EventOnTICData extends Event -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "OnTICData"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected EventOnTICData() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public EventOnTICData(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public EventOnTICData(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param data - * @throws DataDictionaryException - */ - public EventOnTICData(LocalDateTime dateTime, TICCoreFrame data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Event - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - public TICCoreFrame getData() - { - return (TICCoreFrame) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICCoreFrame data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, true, TICCoreFrame.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java deleted file mode 100644 index 0fdd485..0000000 --- a/src/main/java/enedis/tic/service/message/RequestGetAvailableTICs.java +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Request; - -/** - * RequestGetAvailableTICs class - * - * Generated - */ -public class RequestGetAvailableTICs extends Request -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Message name */ - public static final String NAME = "GetAvailableTICs"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @throws DataDictionaryException - */ - public RequestGetAvailableTICs() throws DataDictionaryException - { - super(); - - this.kName.setAcceptedValues(NAME); - - this.checkAndUpdate(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestGetAvailableTICs(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestGetAvailableTICs(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Request - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java b/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java deleted file mode 100644 index 250981a..0000000 --- a/src/main/java/enedis/tic/service/message/RequestGetModemsInfo.java +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Request; - -/** - * RequestGetModemsInfo class - * - * Generated - */ -public class RequestGetModemsInfo extends Request -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Message name */ - public static final String NAME = "GetModemsInfo"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public RequestGetModemsInfo() throws DataDictionaryException - { - super(); - - this.kName.setAcceptedValues(NAME); - - this.checkAndUpdate(); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestGetModemsInfo(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestGetModemsInfo(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Request - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/tic/service/message/RequestReadTIC.java b/src/main/java/enedis/tic/service/message/RequestReadTIC.java deleted file mode 100644 index 95fb548..0000000 --- a/src/main/java/enedis/tic/service/message/RequestReadTIC.java +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Request; -import enedis.tic.core.TICIdentifier; - -/** - * RequestReadTIC class - * - * Generated - */ -public class RequestReadTIC extends Request -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "ReadTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected RequestReadTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestReadTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestReadTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param data - * @throws DataDictionaryException - */ - public RequestReadTIC(TICIdentifier data) throws DataDictionaryException - { - this(); - - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Request - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - public TICIdentifier getData() - { - return (TICIdentifier) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICIdentifier data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, true, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java deleted file mode 100644 index 499917a..0000000 --- a/src/main/java/enedis/tic/service/message/RequestSubscribeTIC.java +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Request; -import enedis.tic.core.TICIdentifier; - -/** - * RequestSubscribeTIC class - * - * Generated - */ -public class RequestSubscribeTIC extends Request -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "SubscribeTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected RequestSubscribeTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestSubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestSubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param data - * @throws DataDictionaryException - */ - public RequestSubscribeTIC(List data) throws DataDictionaryException - { - this(); - - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Request - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java deleted file mode 100644 index 6556d82..0000000 --- a/src/main/java/enedis/tic/service/message/RequestUnsubscribeTIC.java +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Request; -import enedis.tic.core.TICIdentifier; - -/** - * RequestUnsubscribeTIC class - * - * Generated - */ -public class RequestUnsubscribeTIC extends Request -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "UnsubscribeTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected RequestUnsubscribeTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public RequestUnsubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public RequestUnsubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param data - * @throws DataDictionaryException - */ - public RequestUnsubscribeTIC(List data) throws DataDictionaryException - { - this(); - - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Request - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java b/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java deleted file mode 100644 index 8c92f4d..0000000 --- a/src/main/java/enedis/tic/service/message/ResponseGetAvailableTICs.java +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Response; -import enedis.tic.core.TICIdentifier; - -/** - * ResponseGetAvailableTICs class - * - * Generated - */ -public class ResponseGetAvailableTICs extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "GetAvailableTICs"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseGetAvailableTICs() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseGetAvailableTICs(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseGetAvailableTICs(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseGetAvailableTICs(LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICIdentifier.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java b/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java deleted file mode 100644 index 7a66ad1..0000000 --- a/src/main/java/enedis/tic/service/message/ResponseGetModemsInfo.java +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorList; -import enedis.lab.util.message.Response; - -/** - * ResponseGetModemsInfo class - * - * Generated - */ -public class ResponseGetModemsInfo extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "GetModemsInfo"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorList kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseGetModemsInfo() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseGetModemsInfo(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseGetModemsInfo(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseGetModemsInfo(LocalDateTime dateTime, Number errorCode, String errorMessage, List data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - @SuppressWarnings("unchecked") - public List getData() - { - return (List) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(List data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorList(KEY_DATA, false, TICPortDescriptor.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java b/src/main/java/enedis/tic/service/message/ResponseReadTIC.java deleted file mode 100644 index 0a864cb..0000000 --- a/src/main/java/enedis/tic/service/message/ResponseReadTIC.java +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.types.datadictionary.KeyDescriptorDataDictionary; -import enedis.lab.util.message.Response; -import enedis.tic.core.TICCoreFrame; - -/** - * ResponseReadTIC class - * - * Generated - */ -public class ResponseReadTIC extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected static final String KEY_DATA = "data"; - - /** Message name */ - public static final String NAME = "ReadTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - protected KeyDescriptorDataDictionary kData; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseReadTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseReadTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseReadTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @param data - * @throws DataDictionaryException - */ - public ResponseReadTIC(LocalDateTime dateTime, Number errorCode, String errorMessage, TICCoreFrame data) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - this.setData(data); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Get data - * - * @return the data - */ - public TICCoreFrame getData() - { - return (TICCoreFrame) this.data.get(KEY_DATA); - } - - /** - * Set data - * - * @param data - * @throws DataDictionaryException - */ - public void setData(TICCoreFrame data) throws DataDictionaryException - { - this.setData((Object) data); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected void setData(Object data) throws DataDictionaryException - { - this.data.put(KEY_DATA, this.kData.convert(data)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - this.kData = new KeyDescriptorDataDictionary(KEY_DATA, false, TICCoreFrame.class); - this.keys.add(this.kData); - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java deleted file mode 100644 index 9577924..0000000 --- a/src/main/java/enedis/tic/service/message/ResponseSubscribeTIC.java +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.util.message.Response; - -/** - * ResponseSubscribeTIC class - * - * Generated - */ -public class ResponseSubscribeTIC extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Message name */ - public static final String NAME = "SubscribeTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseSubscribeTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseSubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseSubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public ResponseSubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java b/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java deleted file mode 100644 index b182e62..0000000 --- a/src/main/java/enedis/tic/service/message/ResponseUnsubscribeTIC.java +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import enedis.lab.types.DataDictionary; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.KeyDescriptor; -import enedis.lab.util.message.Response; - -/** - * ResponseUnsubscribeTIC class - * - * Generated - */ -public class ResponseUnsubscribeTIC extends Response -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** Message name */ - public static final String NAME = "UnsubscribeTIC"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private List> keys = new ArrayList>(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - protected ResponseUnsubscribeTIC() - { - super(); - this.loadKeyDescriptors(); - - this.kName.setAcceptedValues(NAME); - } - - /** - * Constructor using map - * - * @param map - * @throws DataDictionaryException - */ - public ResponseUnsubscribeTIC(Map map) throws DataDictionaryException - { - this(); - this.copy(fromMap(map)); - } - - /** - * Constructor using datadictionary - * - * @param other - * @throws DataDictionaryException - */ - public ResponseUnsubscribeTIC(DataDictionary other) throws DataDictionaryException - { - this(); - this.copy(other); - } - - /** - * Constructor setting parameters to specific values - * - * @param dateTime - * @param errorCode - * @param errorMessage - * @throws DataDictionaryException - */ - public ResponseUnsubscribeTIC(LocalDateTime dateTime, Number errorCode, String errorMessage) throws DataDictionaryException - { - this(); - - this.setDateTime(dateTime); - this.setErrorCode(errorCode); - this.setErrorMessage(errorMessage); - - this.checkAndUpdate(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Response - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void updateOptionalParameters() throws DataDictionaryException - { - if (!this.exists(KEY_NAME)) - { - this.setName(NAME); - } - super.updateOptionalParameters(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void loadKeyDescriptors() - { - try - { - - this.addAllKeyDescriptor(this.keys); - } - catch (DataDictionaryException e) - { - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java deleted file mode 100644 index 834533b..0000000 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketEventFactory.java +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message.factory; - -import enedis.lab.util.message.factory.EventFactory; -import enedis.tic.service.message.EventOnError; -import enedis.tic.service.message.EventOnTICData; - -/** - * TIC2WebSocket request factory - */ -public class TIC2WebSocketEventFactory extends EventFactory -{ - /** - * Default constructor - */ - public TIC2WebSocketEventFactory() - { - super(); - this.addMessageClass(EventOnTICData.NAME, EventOnTICData.class); - this.addMessageClass(EventOnError.NAME, EventOnError.class); - } -} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java deleted file mode 100644 index e501d05..0000000 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketMessageFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message.factory; - -import enedis.lab.util.message.factory.MessageFactory; - -/** - * TIC2WebSocket message factory - */ -public class TIC2WebSocketMessageFactory extends MessageFactory -{ - /** - * Default constructor - */ - public TIC2WebSocketMessageFactory() - { - super(new TIC2WebSocketRequestFactory(), new TIC2WebSocketResponseFactory(), new TIC2WebSocketEventFactory()); - } -} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java deleted file mode 100644 index e76db79..0000000 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketRequestFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message.factory; - -import enedis.lab.util.message.factory.RequestFactory; -import enedis.tic.service.message.RequestGetAvailableTICs; -import enedis.tic.service.message.RequestGetModemsInfo; -import enedis.tic.service.message.RequestReadTIC; -import enedis.tic.service.message.RequestSubscribeTIC; -import enedis.tic.service.message.RequestUnsubscribeTIC; - -/** - * TIC2WebSocket request factory - */ -public class TIC2WebSocketRequestFactory extends RequestFactory -{ - /** - * Default constructor - */ - public TIC2WebSocketRequestFactory() - { - super(); - this.addMessageClass(RequestGetAvailableTICs.NAME, RequestGetAvailableTICs.class); - this.addMessageClass(RequestGetModemsInfo.NAME, RequestGetModemsInfo.class); - this.addMessageClass(RequestReadTIC.NAME, RequestReadTIC.class); - this.addMessageClass(RequestSubscribeTIC.NAME, RequestSubscribeTIC.class); - this.addMessageClass(RequestUnsubscribeTIC.NAME, RequestUnsubscribeTIC.class); - } -} diff --git a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java b/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java deleted file mode 100644 index 13390e3..0000000 --- a/src/main/java/enedis/tic/service/message/factory/TIC2WebSocketResponseFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message.factory; - -import enedis.lab.util.message.factory.ResponseFactory; -import enedis.tic.service.message.ResponseGetAvailableTICs; -import enedis.tic.service.message.ResponseGetModemsInfo; -import enedis.tic.service.message.ResponseReadTIC; -import enedis.tic.service.message.ResponseSubscribeTIC; -import enedis.tic.service.message.ResponseUnsubscribeTIC; - -/** - * TIC2WebSocket request factory - */ -public class TIC2WebSocketResponseFactory extends ResponseFactory -{ - /** - * Default constructor - */ - public TIC2WebSocketResponseFactory() - { - super(); - this.addMessageClass(ResponseGetAvailableTICs.NAME, ResponseGetAvailableTICs.class); - this.addMessageClass(ResponseGetModemsInfo.NAME, ResponseGetModemsInfo.class); - this.addMessageClass(ResponseReadTIC.NAME, ResponseReadTIC.class); - this.addMessageClass(ResponseSubscribeTIC.NAME, ResponseSubscribeTIC.class); - this.addMessageClass(ResponseUnsubscribeTIC.NAME, ResponseUnsubscribeTIC.class); - } -} \ No newline at end of file diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java deleted file mode 100644 index 16e5518..0000000 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketChannelInitializer.java +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.netty; - -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.http.HttpObjectAggregator; -import io.netty.handler.codec.http.HttpServerCodec; -import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; -import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; -import io.netty.handler.stream.ChunkedWriteHandler; - -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; - -/** - * Channel initializer for TIC2WebSocket server - */ -public class TIC2WebSocketChannelInitializer extends ChannelInitializer -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final String WEBSOCKET_PATH = "/"; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private final TIC2WebSocketClientPool clientPool; - private final TIC2WebSocketRequestHandler requestHandler; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param clientPool the client pool - * @param requestHandler the request handler - */ - public TIC2WebSocketChannelInitializer(TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) - { - this.clientPool = clientPool; - this.requestHandler = requestHandler; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void initChannel(SocketChannel ch) throws Exception - { - ChannelPipeline pipeline = ch.pipeline(); - - // HTTP codec - pipeline.addLast(new HttpServerCodec()); - - // HTTP object aggregator - pipeline.addLast(new HttpObjectAggregator(65536)); - - // Chunked write handler for large messages - pipeline.addLast(new ChunkedWriteHandler()); - - // WebSocket compression - pipeline.addLast(new WebSocketServerCompressionHandler()); - - // WebSocket protocol handler - pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true)); - - // Custom TIC2WebSocket handler - pipeline.addLast(new TIC2WebSocketHandler(clientPool, requestHandler)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java deleted file mode 100644 index 65ae2a9..0000000 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketHandler.java +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.netty; - -import java.util.List; -import java.util.Optional; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; -import io.netty.handler.codec.http.websocketx.WebSocketFrame; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Event; -import enedis.lab.util.message.Message; -import enedis.lab.util.message.Request; -import enedis.lab.util.message.Response; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import enedis.tic.core.TICIdentifier; -import enedis.tic.service.client.TIC2WebSocketClient; -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.endpoint.EventSender; -import enedis.tic.service.endpoint.TIC2WebSocketEndPointErrorCode; -import enedis.tic.service.message.RequestUnsubscribeTIC; -import enedis.tic.service.message.factory.TIC2WebSocketMessageFactory; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; - -/** - * Netty WebSocket handler for TIC2WebSocket - */ -public class TIC2WebSocketHandler extends SimpleChannelInboundHandler implements EventSender -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final Logger logger = LogManager.getLogger(TIC2WebSocketHandler.class); - - private final TIC2WebSocketClientPool clientPool; - private final TIC2WebSocketRequestHandler requestHandler; - private final TIC2WebSocketMessageFactory factory; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param clientPool the client pool - * @param requestHandler the request handler - */ - public TIC2WebSocketHandler(TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) - { - this.clientPool = clientPool; - this.requestHandler = requestHandler; - this.factory = new TIC2WebSocketMessageFactory(); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// EventSender - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void sendEvent(Channel channel, Event event) - { - this.sendMessage(channel, event); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception - { - Channel channel = ctx.channel(); - String channelId = channel.id().asLongText(); - - logger.info("Open channel " + channelId); - - if (!clientPool.exists(channelId)) - { - logger.info("Client create with channel id : " + channelId); - clientPool.createClient(channel, this); - } - else - { - logger.error("Client with channel id : " + channelId + " already exists ! "); - } - - super.channelActive(ctx); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception - { - Channel channel = ctx.channel(); - String channelId = channel.id().asLongText(); - - logger.info("Close channel " + channelId); - - if (clientPool.exists(channelId)) - { - try - { - logger.info("Generate unsubscribe request"); - Request request = new RequestUnsubscribeTIC((List) null); - this.handleRequest(clientPool.getClient(channelId).get(), request); - - logger.info("Remove client with channel id : " + channelId); - clientPool.remove(channelId); - } - catch (DataDictionaryException e) - { - logger.error("Error during channel close", e); - } - } - else - { - logger.error("Client with channel id : " + channelId + " doesn't exist ! "); - } - - super.channelInactive(ctx); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception - { - Channel channel = ctx.channel(); - String channelId = channel.id().asLongText(); - - logger.error("Error on channel " + channelId, cause); - - ctx.close(); - } - - @Override - protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception - { - if (frame instanceof TextWebSocketFrame) - { - TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; - String text = textFrame.text(); - Channel channel = ctx.channel(); - String channelId = channel.id().asLongText(); - - logger.info("Message on channel " + channelId); - - TIC2WebSocketClient client = this.getClient(channel); - - Optional message = this.getMessage(channel, text); - if (!message.isPresent()) - { - return; - } - - Optional request = this.getRequest(channel, message.get()); - if (!request.isPresent()) - { - return; - } - - Response response = this.handleRequest(client, request.get()); - - this.sendMessage(channel, response); - } - else - { - // Handle other frame types if needed - logger.warn("Unsupported frame type: " + frame.getClass().getSimpleName()); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private TIC2WebSocketClient getClient(Channel channel) - { - String channelId = channel.id().asLongText(); - Optional clientOpt = clientPool.getClient(channelId); - if (!clientOpt.isPresent()) - { - return clientPool.createClient(channel, this); - } - return clientOpt.get(); - } - - private Optional getMessage(Channel channel, String text) - { - Message message = null; - TIC2WebSocketEndPointErrorCode errorCode = TIC2WebSocketEndPointErrorCode.NO_ERROR; - String errorMessage = ""; - - try - { - message = this.factory.getMessage(text); - } - catch (MessageInvalidFormatException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_FORMAT; - errorMessage = e.getMessage(); - } - catch (MessageInvalidTypeException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_INVALID; - errorMessage = e.getMessage(); - } - catch (MessageKeyNameDoesntExistException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_NAME_MISSING; - errorMessage = e.getMessage(); - } - catch (MessageKeyTypeDoesntExistException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.MESSAGE_TYPE_MISSING; - errorMessage = e.getMessage(); - } - catch (MessageInvalidContentException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.INVALID_MESSAGE_CONTENT; - errorMessage = e.getMessage(); - } - catch (UnsupportedMessageException e) - { - errorCode = TIC2WebSocketEndPointErrorCode.UNSUPPORTED_MESSAGE; - errorMessage = e.getMessage(); - } - - if (errorCode != TIC2WebSocketEndPointErrorCode.NO_ERROR) - { - logger.error("Error parsing message: " + errorMessage); - // TODO: Send error event - return Optional.empty(); - } - - return Optional.of(message); - } - - private Optional getRequest(Channel channel, Message message) - { - if (!(message instanceof Request)) - { - logger.error("Message is not a request"); - // TODO: Send error event - return Optional.empty(); - } - - return Optional.of((Request) message); - } - - private Response handleRequest(TIC2WebSocketClient client, Request request) - { - return requestHandler.handle(request, client); - } - - private void sendMessage(Channel channel, Message message) - { - try - { - String json = message.toJSON().toString(); - TextWebSocketFrame frame = new TextWebSocketFrame(json); - channel.writeAndFlush(frame); - - logger.debug("Sent message to channel {}: {}", channel.id().asLongText(), json); - } - catch (Exception e) - { - logger.error("Error sending message to channel " + channel.id().asLongText(), e); - } - } -} diff --git a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java b/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java deleted file mode 100644 index 2c22b8d..0000000 --- a/src/main/java/enedis/tic/service/netty/TIC2WebSocketServer.java +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.netty; - -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.logging.LoggingHandler; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; - -/** - * TIC2WebSocket Netty WebSocket Server - */ -public class TIC2WebSocketServer -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private static final Logger logger = LogManager.getLogger(TIC2WebSocketServer.class); - - private final String host; - private final int port; - private final TIC2WebSocketClientPool clientPool; - private final TIC2WebSocketRequestHandler requestHandler; - - private EventLoopGroup bossGroup; - private EventLoopGroup workerGroup; - private Channel serverChannel; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Constructor - * - * @param host the host to bind to - * @param port the port to bind to - * @param clientPool the client pool - * @param requestHandler the request handler - */ - public TIC2WebSocketServer(String host, int port, TIC2WebSocketClientPool clientPool, TIC2WebSocketRequestHandler requestHandler) - { - this.host = host; - this.port = port; - this.clientPool = clientPool; - this.requestHandler = requestHandler; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Start the WebSocket server - * - * @throws Exception if server startup fails - */ - public void start() throws Exception - { - logger.info("Starting TIC2WebSocket Netty server on {}:{}", host, port); - - bossGroup = new NioEventLoopGroup(1); - workerGroup = new NioEventLoopGroup(); - - try - { - ServerBootstrap bootstrap = new ServerBootstrap(); - bootstrap.group(bossGroup, workerGroup) - .channel(NioServerSocketChannel.class) - .handler(new LoggingHandler(LogLevel.INFO)) - .childHandler(new TIC2WebSocketChannelInitializer(clientPool, requestHandler)); - - ChannelFuture future = bootstrap.bind(host, port).sync(); - serverChannel = future.channel(); - - logger.info("TIC2WebSocket Netty server started successfully on {}:{}", host, port); - } - catch (Exception e) - { - logger.error("Failed to start TIC2WebSocket Netty server", e); - stop(); - throw e; - } - } - - /** - * Stop the WebSocket server - */ - public void stop() - { - logger.info("Stopping TIC2WebSocket Netty server"); - - try - { - if (serverChannel != null) - { - serverChannel.close().sync(); - } - } - catch (InterruptedException e) - { - logger.warn("Interrupted while closing server channel", e); - Thread.currentThread().interrupt(); - } - finally - { - if (bossGroup != null) - { - bossGroup.shutdownGracefully(); - } - if (workerGroup != null) - { - workerGroup.shutdownGracefully(); - } - } - - logger.info("TIC2WebSocket Netty server stopped"); - } - - /** - * Wait for the server to close - * - * @throws InterruptedException if interrupted while waiting - */ - public void waitForClose() throws InterruptedException - { - if (serverChannel != null) - { - serverChannel.closeFuture().sync(); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -} diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java deleted file mode 100644 index da0f444..0000000 --- a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.requesthandler; - -import enedis.lab.util.message.Request; -import enedis.lab.util.message.Response; -import enedis.tic.service.client.TIC2WebSocketClient; - -/** - * TIC2WebSocket request handler interface - */ -public interface TIC2WebSocketRequestHandler -{ - /** - * Handle all TIC2WebSocket request - * - * @param request - * @param client - * @return request response - */ - public Response handle(Request request, TIC2WebSocketClient client); -} diff --git a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java b/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java deleted file mode 100644 index 22f623b..0000000 --- a/src/main/java/enedis/tic/service/requesthandler/TIC2WebSocketRequestHandlerBase.java +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.requesthandler; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.util.message.Request; -import enedis.lab.util.message.Response; -import enedis.lab.util.message.ResponseBase; -import enedis.tic.core.TICCore; -import enedis.tic.core.TICCoreErrorCode; -import enedis.tic.core.TICCoreException; -import enedis.tic.core.TICCoreFrame; -import enedis.tic.core.TICIdentifier; -import enedis.tic.service.client.TIC2WebSocketClient; -import enedis.tic.service.endpoint.TIC2WebSocketEndPointErrorCode; -import enedis.tic.service.message.RequestGetAvailableTICs; -import enedis.tic.service.message.RequestGetModemsInfo; -import enedis.tic.service.message.RequestReadTIC; -import enedis.tic.service.message.RequestSubscribeTIC; -import enedis.tic.service.message.RequestUnsubscribeTIC; -import enedis.tic.service.message.ResponseGetAvailableTICs; -import enedis.tic.service.message.ResponseGetModemsInfo; -import enedis.tic.service.message.ResponseReadTIC; -import enedis.tic.service.message.ResponseSubscribeTIC; -import enedis.tic.service.message.ResponseUnsubscribeTIC; - -/** - * TIC2WebSocket client pool interface - */ -public class TIC2WebSocketRequestHandlerBase implements TIC2WebSocketRequestHandler -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Logger logger; - private TICCore ticCore; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Default constructor - * - * @param ticCore - */ - public TIC2WebSocketRequestHandlerBase(TICCore ticCore) - { - super(); - this.logger = LogManager.getLogger(this.getClass()); - this.ticCore = ticCore; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Response handle(Request request, TIC2WebSocketClient client) - { - Response response = null; - - switch (request.getName()) - { - case RequestGetAvailableTICs.NAME: - response = this.handleGetAvailableTICsRequest((RequestGetAvailableTICs) request); - break; - case RequestGetModemsInfo.NAME: - response = this.handleGetModemsInfoRequest((RequestGetModemsInfo) request); - break; - case RequestReadTIC.NAME: - response = this.handleReadTICRequest((RequestReadTIC) request); - break; - case RequestSubscribeTIC.NAME: - response = this.handleSubscribeTICRequest((RequestSubscribeTIC) request, client); - break; - case RequestUnsubscribeTIC.NAME: - response = this.handleUnsubscribeTICRequest((RequestUnsubscribeTIC) request, client); - break; - default: - this.logger.error("Request " + request.getName() + " not supported"); - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.UNSUPPORTED_MESSAGE, "Request " + request.getName() + " not supported"); - break; - } - - return response; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private Response handleGetAvailableTICsRequest(RequestGetAvailableTICs request) - { - List ticIdentifiers = this.ticCore.getAvailableTICs(); - - Response response = null; - try - { - response = new ResponseGetAvailableTICs(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, ticIdentifiers); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); - } - - return response; - } - - private Response handleGetModemsInfoRequest(RequestGetModemsInfo request) - { - List modemsInfo = this.ticCore.getModemsInfo(); - - Response response = null; - try - { - response = new ResponseGetModemsInfo(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, modemsInfo); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); - } - - return response; - } - - private Response handleReadTICRequest(RequestReadTIC request) - { - Response response = null; - try - { - TICCoreFrame frame = this.ticCore.readNextFrame(request.getData()); - response = new ResponseReadTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null, frame); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); - } - catch (TICCoreException e) - { - if(e.getErrorCode() == TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode()) - { - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.IDENTIFIER_NOT_FOUND, e.getMessage()); - } - else if(e.getErrorCode() == TICCoreErrorCode.DATA_READ_TIMEOUT.getCode()) - { - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.READ_TIMEOUT, e.getMessage()); - } - else - { - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.INTERNAL_ERROR, e.getMessage()); - } - } - - return response; - } - - private Response handleSubscribeTICRequest(RequestSubscribeTIC request, TIC2WebSocketClient client) - { - Response response = null; - Optional> ticIdentifiers = Optional.ofNullable(request.getData()); - - if (ticIdentifiers.isPresent()) - { - List newSubscriptions = this.getNewSubcriptions(this.ticCore.getIndentifiers(client), ticIdentifiers.get()); - if(!newSubscriptions.isEmpty()) - { - for(TICIdentifier identifier : ticIdentifiers.get()) - { - try - { - this.ticCore.subscribe(identifier,client); - try - { - response = new ResponseSubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } - catch (TICCoreException e) - { - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.SUBSCRIPTION_FAIL, e.getMessage()); - } - } - } - } - else - { - this.ticCore.subscribe(client); - try - { - response = new ResponseSubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } - - return response; - } - - private List getNewSubcriptions(List currentSubscriptions, List askedTicIdentifiers) - { - List newSubscriptions = new ArrayList(); - - for (TICIdentifier askedTicIdentifier : askedTicIdentifiers) - { - boolean newSub = true; - for (TICIdentifier currentSubscription : currentSubscriptions) - { - if (askedTicIdentifier.matches(currentSubscription)) - { - newSub = false; - break; - } - } - if (newSub) - { - newSubscriptions.add(askedTicIdentifier); - } - } - return newSubscriptions; - } - - private Response handleUnsubscribeTICRequest(RequestUnsubscribeTIC request, TIC2WebSocketClient client) - { - Response response = null; - Optional> ticIdentifiers = Optional.ofNullable(request.getData()); - - if (ticIdentifiers.isPresent()) - { - for(TICIdentifier identifier : ticIdentifiers.get()) - { - try - { - this.ticCore.unsubscribe(identifier,client); - try - { - response = new ResponseUnsubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } - catch (TICCoreException e) - { - response = this.createErrorResponse(request.getName(), TIC2WebSocketEndPointErrorCode.UNSUBSCRIPTION_FAIL, e.getMessage()); - } - } - } - else - { - this.ticCore.unsubscribe(client); - try - { - response = new ResponseUnsubscribeTIC(LocalDateTime.now(), TIC2WebSocketEndPointErrorCode.NO_ERROR.value(), null); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - } - } - - return response; - } - - private Response createErrorResponse(String name, TIC2WebSocketEndPointErrorCode code, String message) - { - try - { - return new ResponseBase(name, LocalDateTime.now(), code.value(), message, null); - } - catch (DataDictionaryException e) - { - this.logger.error(e.getMessage(), e); - return null; - } - } - -} From 5b2db1e05b8e232b2328ab4c5e6772ea80beaf64 Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Wed, 4 Feb 2026 16:38:38 +0100 Subject: [PATCH 39/59] fix: Remove enedis test package --- .../java/enedis/lab/io/PortFinderMock.java | 73 --- .../enedis/lab/io/tic/TICPortFinderMock.java | 102 ---- .../java/enedis/lab/mock/FunctionCall.java | 26 - .../java/enedis/tic/core/TICCoreBaseTest.java | 498 ------------------ .../tic/core/TICCoreReadNextFrameTask.java | 109 ---- .../enedis/tic/core/TICCoreStreamMock.java | 167 ------ .../tic/core/TICCoreSubscriberMock.java | 85 --- .../tic/core/TICCoreSubscriberOnDataCall.java | 78 --- .../core/TICCoreSubscriberOnErrorCall.java | 78 --- .../enedis/tic/core/TICIdentifierTest.java | 187 ------- .../TIC2WebSocketEventFactoryTest.java | 75 --- .../TIC2WebSocketMessageFactoryTest.java | 34 -- .../TIC2WebSocketRequestFactoryTest.java | 239 --------- .../TIC2WebSocketResponseFactoryTest.java | 161 ------ .../netty/TIC2WebSocketServerTest.java | 39 -- 15 files changed, 1951 deletions(-) delete mode 100644 src/test/java/enedis/lab/io/PortFinderMock.java delete mode 100644 src/test/java/enedis/lab/io/tic/TICPortFinderMock.java delete mode 100644 src/test/java/enedis/lab/mock/FunctionCall.java delete mode 100644 src/test/java/enedis/tic/core/TICCoreBaseTest.java delete mode 100644 src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java delete mode 100644 src/test/java/enedis/tic/core/TICCoreStreamMock.java delete mode 100644 src/test/java/enedis/tic/core/TICCoreSubscriberMock.java delete mode 100644 src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java delete mode 100644 src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java delete mode 100644 src/test/java/enedis/tic/core/TICIdentifierTest.java delete mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java delete mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java delete mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java delete mode 100644 src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java delete mode 100644 src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java diff --git a/src/test/java/enedis/lab/io/PortFinderMock.java b/src/test/java/enedis/lab/io/PortFinderMock.java deleted file mode 100644 index 4fb9c13..0000000 --- a/src/test/java/enedis/lab/io/PortFinderMock.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io; - -import java.util.ArrayList; -import java.util.List; - -import enedis.lab.mock.FunctionCall; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; - -@SuppressWarnings("javadoc") -public class PortFinderMock implements PortFinder -{ - public List findAllCalls = new ArrayList(); - public DataList descriptorList = new DataArrayList(); - - public PortFinderMock() - { - this.descriptorList = new DataArrayList(); - } - - @Override - public DataList findAll() - { - this.findAllCalls.add(new FunctionCall()); - return this.getDescriptors(); - } - - public DataList getDescriptors() - { - DataList descriptors = new DataArrayList(); - - synchronized (this.descriptorList) - { - descriptors.addAll(this.descriptorList); - } - - return descriptors; - } - - public DataList setDescriptors(DataList descriptors) - { - synchronized (this.descriptorList) - { - this.descriptorList.clear(); - this.descriptorList.addAll(descriptors); - } - - return descriptors; - } - - public void addDescriptor(T descriptor) - { - synchronized (this.descriptorList) - { - this.descriptorList.add(descriptor); - } - } - - public void removeDescriptor(T descriptor) - { - synchronized (this.descriptorList) - { - this.descriptorList.remove(descriptor); - } - } -} diff --git a/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java b/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java deleted file mode 100644 index 225f8f4..0000000 --- a/src/test/java/enedis/lab/io/tic/TICPortFinderMock.java +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.io.tic; - -import enedis.lab.io.PortFinderMock; -import enedis.lab.types.DataArrayList; -import enedis.lab.types.DataList; - -@SuppressWarnings("javadoc") -public class TICPortFinderMock extends PortFinderMock implements TICPortFinder -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public DataList nativeDescriptorList = new DataArrayList(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICPortFinderMock() - { - super(); - } - - public TICPortFinderMock(TICPortDescriptor... descriptors) - { - this.setDescriptors(DataArrayList.asList(descriptors)); - } - - @Override - public TICPortDescriptor findNative(String portName) - { - for (TICPortDescriptor descriptor : this.nativeDescriptorList) - { - if (descriptor.getPortName() == null && portName == null) - { - return descriptor; - } - if (descriptor.getPortName().equals(portName)) - { - return descriptor; - } - } - - return null; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/test/java/enedis/lab/mock/FunctionCall.java b/src/test/java/enedis/lab/mock/FunctionCall.java deleted file mode 100644 index 6a5ba81..0000000 --- a/src/test/java/enedis/lab/mock/FunctionCall.java +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.lab.mock; - -@SuppressWarnings("javadoc") -public class FunctionCall -{ - public long captureTime; - - public FunctionCall() - { - super(); - this.captureTime = System.nanoTime(); - } - - public FunctionCall(long captureTime) - { - super(); - this.captureTime = captureTime; - } -} diff --git a/src/test/java/enedis/tic/core/TICCoreBaseTest.java b/src/test/java/enedis/tic/core/TICCoreBaseTest.java deleted file mode 100644 index f8d7fff..0000000 --- a/src/test/java/enedis/tic/core/TICCoreBaseTest.java +++ /dev/null @@ -1,498 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.time.LocalDateTime; -import java.util.List; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import enedis.lab.io.tic.TICModemType; -import enedis.lab.io.tic.TICPortDescriptor; -import enedis.lab.io.tic.TICPortFinderMock; -import enedis.lab.mock.FunctionCall; -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionaryException; -import enedis.lab.types.datadictionary.DataDictionaryBase; -import enedis.lab.util.time.Time; -import enedis.lab.util.task.Task; - -@SuppressWarnings("javadoc") -public class TICCoreBaseTest -{ - private static final int NOTIFICATION_TIMEOUT = 1000; - - private TICPortFinderMock ticPortFinder; - private long plugNotifierPeriod; - private TICCoreBase ticCore; - - @Before - public void startTICCore() throws TICCoreException - { - this.ticPortFinder = new TICPortFinderMock(); - this.plugNotifierPeriod = 50; - this.ticCore = new TICCoreBase(this.ticPortFinder, this.plugNotifierPeriod, TICCoreStreamMock.class, TICMode.AUTO, null); - - this.ticCore.start(); - this.waitTaskRunning(this.ticCore); - } - - @After - public void stopTICCore() throws TICCoreException - { - this.ticCore.stop(); - TICCoreStreamMock.streams.clear(); - } - - @Test - public void test_getAvailableTICs() throws TICCoreException, DataDictionaryException - { - List availableTICs = this.ticCore.getAvailableTICs(); - Assert.assertNotNull(availableTICs); - Assert.assertEquals(0, availableTICs.size()); - - TICPortDescriptor descriptor1 = new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); - this.plugModem(descriptor1); - this.plugModem(descriptor2); - - availableTICs = this.ticCore.getAvailableTICs(); - Assert.assertNotNull(availableTICs); - Assert.assertEquals(2, availableTICs.size()); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("2", "COM4", null))); - - this.unplugModem(descriptor2); - - availableTICs = this.ticCore.getAvailableTICs(); - Assert.assertNotNull(availableTICs); - Assert.assertEquals(1, availableTICs.size()); - Assert.assertTrue(availableTICs.contains(new TICIdentifier("1", "COM3", null))); - } - - @Test - public void test_getModemsInfo() throws TICCoreException, DataDictionaryException - { - List modemsInfo = this.ticCore.getModemsInfo(); - Assert.assertNotNull(modemsInfo); - Assert.assertEquals(0, modemsInfo.size()); - - TICPortDescriptor descriptor1 = new TICPortDescriptor(null, "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = new TICPortDescriptor(null, "COM4", null, null, null, null, TICModemType.TELEINFO); - this.plugModem(descriptor1); - this.plugModem(descriptor2); - - modemsInfo = this.ticCore.getModemsInfo(); - Assert.assertNotNull(modemsInfo); - Assert.assertEquals(2, modemsInfo.size()); - Assert.assertTrue(modemsInfo.contains(descriptor1)); - Assert.assertTrue(modemsInfo.contains(descriptor2)); - - this.unplugModem(descriptor2); - modemsInfo = this.ticCore.getModemsInfo(); - Assert.assertNotNull(modemsInfo); - Assert.assertEquals(1, modemsInfo.size()); - Assert.assertTrue(modemsInfo.contains(descriptor1)); - } - - @Test - public void test_readNextFrame_ok() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); - task.start(); - this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); - TICCoreFrame frame = this.createFrame(identifier, TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); - stream.notifyOnData(frame); - this.waitTaskTerminated(task); - Assert.assertNull(task.exception); - Assert.assertNotNull(task.frame); - Assert.assertTrue(task.frame == frame); - } - - @Test - public void test_readNextFrame_error_OTHER_REASON() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier); - task.start(); - this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); - TICCoreError error = new TICCoreError(identifier, TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); - stream.notifyOnError(error); - this.waitTaskTerminated(task); - Assert.assertNull(task.frame); - Assert.assertNotNull(task.exception); - Assert.assertTrue(task.exception instanceof TICCoreException); - TICCoreException exception = (TICCoreException) task.exception; - Assert.assertEquals(error.getErrorCode(), exception.getErrorCode()); - Assert.assertEquals(error.getErrorMessage(), exception.getErrorInfo()); - } - - @Test - public void test_readNextFrame_error_STREAM_IDENTIFIER_NOT_FOUND() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, new TICIdentifier("2", "COM3", null)); - task.start(); - this.waitTaskRunning(task); - this.waitTaskTerminated(task); - Assert.assertNull(task.frame); - Assert.assertNotNull(task.exception); - Assert.assertTrue(task.exception instanceof TICCoreException); - TICCoreException exception = (TICCoreException) task.exception; - Assert.assertEquals(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); - } - - @Test - public void test_readNextFrame_timeout() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream = TICCoreStreamMock.streams.get(0); - Assert.assertEquals(identifier, stream.getIdentifier()); - - TICCoreReadNextFrameTask task = new TICCoreReadNextFrameTask(this.ticCore, identifier, 200); - task.start(); - this.waitTaskRunning(task); - this.waitReadNextFrameSubsbription(); - this.waitTaskTerminated(task); - Assert.assertNull(task.frame); - Assert.assertNotNull(task.exception); - Assert.assertTrue(task.exception instanceof TICCoreException); - TICCoreException exception = (TICCoreException) task.exception; - Assert.assertEquals(TICCoreErrorCode.DATA_READ_TIMEOUT.getCode(), exception.getErrorCode()); - } - - @Test - public void test_subscribe_any() throws TICCoreException, DataDictionaryException - { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - } - - @Test - public void test_unsubscribe_any() throws TICCoreException, DataDictionaryException - { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - this.ticCore.unsubscribe(subscriber); - } - - @Test - public void test_subscribe_withIdentifier_ok() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - } - - @Test - public void test_unsubscribe_withIdentifier_ok() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - this.ticCore.unsubscribe(identifier, subscriber); - } - - @Test - public void test_subscribe_withIdentifier_notFound() throws TICCoreException, DataDictionaryException - { - this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - TICIdentifier identifier = new TICIdentifier(null, "COM4", null); - try - { - this.ticCore.subscribe(identifier, subscriber); - Assert.fail("Subscribe should have trhown an exception since stream identifier does not match !"); - } - catch (Exception e) - { - Assert.assertTrue(e instanceof TICCoreException); - TICCoreException exception = (TICCoreException) e; - Assert.assertEquals(TICCoreErrorCode.STREAM_IDENTIFIER_NOT_FOUND.getCode(), exception.getErrorCode()); - } - } - - @Test - public void test_onError_whenUnplugModem() throws TICCoreException, DataDictionaryException - { - TICPortDescriptor descriptor1 = new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD); - TICPortDescriptor descriptor2 = new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO); - TICIdentifier identifier1 = this.plugModem(descriptor1); - TICIdentifier identifier2 = this.plugModem(descriptor2); - - TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber4 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber5 = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber1); - this.ticCore.subscribe(identifier1, subscriber2); - this.ticCore.subscribe(identifier1, subscriber3); - this.ticCore.subscribe(identifier2, subscriber4); - this.ticCore.subscribe(identifier2, subscriber5); - - this.unplugModem(descriptor1); - this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); - Assert.assertEquals(1, subscriber1.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber1.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber2.onErrorCalls, 1); - Assert.assertEquals(1, subscriber2.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber2.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber3.onErrorCalls, 1); - Assert.assertEquals(1, subscriber3.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber3.onErrorCalls.get(0).error.getErrorCode()); - Assert.assertEquals(0, subscriber4.onErrorCalls.size()); - Assert.assertEquals(0, subscriber5.onErrorCalls.size()); - - this.unplugModem(descriptor2); - this.waitSubscriberNotification(subscriber4.onErrorCalls, 1); - Assert.assertEquals(1, subscriber4.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber4.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber5.onErrorCalls, 1); - Assert.assertEquals(1, subscriber5.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber5.onErrorCalls.get(0).error.getErrorCode()); - this.waitSubscriberNotification(subscriber1.onErrorCalls, 1); - Assert.assertEquals(2, subscriber1.onErrorCalls.size()); - Assert.assertEquals(TICCoreErrorCode.STREAM_UNPLUGGED.getCode(), subscriber1.onErrorCalls.get(1).error.getErrorCode()); - Assert.assertEquals(1, subscriber2.onErrorCalls.size()); - Assert.assertEquals(1, subscriber3.onErrorCalls.size()); - } - - @Test - public void test_onData_any() throws TICCoreException, DataDictionaryException - { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - - this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreFrame frame1 = this.createFrame(stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); - stream1.notifyOnData(frame1); - this.waitSubscriberNotification(subscriber.onDataCalls, 1); - Assert.assertEquals(1, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - - this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreFrame frame2 = this.createFrame(stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); - stream2.notifyOnData(frame2); - this.waitSubscriberNotification(subscriber.onDataCalls, 2); - Assert.assertEquals(2, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - Assert.assertTrue(frame2 == subscriber.onDataCalls.get(1).frame); - } - - @Test - public void test_onData_withIdentifier() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreFrame frame1 = this.createFrame(stream1.getIdentifier(), TICMode.STANDARD, LocalDateTime.of(2020, 3, 3, 12, 59, 30, 0)); - stream1.notifyOnData(frame1); - this.waitSubscriberNotification(subscriber.onDataCalls, 1); - Assert.assertEquals(1, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - - this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.MICHAUD)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreFrame frame2 = this.createFrame(stream2.getIdentifier(), TICMode.HISTORIC, LocalDateTime.of(2017, 2, 14, 9, 37, 12, 0)); - stream2.notifyOnData(frame2); - this.waitSubscriberNotification(subscriber.onDataCalls, 1); - Assert.assertEquals(1, subscriber.onDataCalls.size()); - Assert.assertTrue(frame1 == subscriber.onDataCalls.get(0).frame); - } - - @Test - public void test_onError_any() throws TICCoreException, DataDictionaryException - { - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(subscriber); - - this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreError error1 = new TICCoreError(stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); - stream1.notifyOnError(error1); - this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - - this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreError error2 = new TICCoreError(stream2.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Error reading stream COM4"); - stream2.notifyOnError(error2); - this.waitSubscriberNotification(subscriber.onErrorCalls, 2); - Assert.assertEquals(2, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - Assert.assertTrue(error2 == subscriber.onErrorCalls.get(1).error); - } - - @Test - public void test_onError_withIdentifier() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - - TICCoreSubscriberMock subscriber = new TICCoreSubscriberMock(); - this.ticCore.subscribe(identifier, subscriber); - - Assert.assertEquals(1, TICCoreStreamMock.streams.size()); - TICCoreStreamMock stream1 = TICCoreStreamMock.streams.get(0); - TICCoreError error1 = new TICCoreError(stream1.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Cannot read stream"); - stream1.notifyOnError(error1); - this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - - this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - TICCoreStreamMock stream2 = TICCoreStreamMock.streams.get(1); - TICCoreError error2 = new TICCoreError(stream2.getIdentifier(), TICCoreErrorCode.OTHER_REASON.getCode(), "Error reading stream COM4"); - stream2.notifyOnError(error2); - this.waitSubscriberNotification(subscriber.onErrorCalls, 1); - Assert.assertEquals(1, subscriber.onErrorCalls.size()); - Assert.assertTrue(error1 == subscriber.onErrorCalls.get(0).error); - } - - @Test - public void test_getIdentifiers() throws TICCoreException, DataDictionaryException - { - TICIdentifier identifier1 = this.plugModem(new TICPortDescriptor("1", "COM3", null, null, null, null, TICModemType.MICHAUD)); - TICIdentifier identifier2 = this.plugModem(new TICPortDescriptor("2", "COM4", null, null, null, null, TICModemType.TELEINFO)); - - TICCoreSubscriberMock subscriber1 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber2 = new TICCoreSubscriberMock(); - TICCoreSubscriberMock subscriber3 = new TICCoreSubscriberMock(); - - this.ticCore.subscribe(subscriber1); - List indentifierList1 = this.ticCore.getIndentifiers(subscriber1); - Assert.assertNotNull(indentifierList1); - Assert.assertEquals(2, indentifierList1.size()); - Assert.assertTrue(indentifierList1.contains(identifier1)); - Assert.assertTrue(indentifierList1.contains(identifier2)); - - this.ticCore.subscribe(identifier1, subscriber2); - List indentifierList2 = this.ticCore.getIndentifiers(subscriber2); - Assert.assertNotNull(indentifierList2); - Assert.assertEquals(1, indentifierList2.size()); - Assert.assertTrue(indentifierList2.contains(identifier1)); - - this.ticCore.subscribe(identifier2, subscriber3); - List indentifierList3 = this.ticCore.getIndentifiers(subscriber3); - Assert.assertNotNull(indentifierList3); - Assert.assertEquals(1, indentifierList3.size()); - Assert.assertTrue(indentifierList3.contains(identifier2)); - - this.ticCore.unsubscribe(subscriber1); - indentifierList1 = this.ticCore.getIndentifiers(subscriber1); - Assert.assertNotNull(indentifierList1); - Assert.assertEquals(0, indentifierList1.size()); - - this.ticCore.unsubscribe(identifier1, subscriber2); - indentifierList2 = this.ticCore.getIndentifiers(subscriber2); - Assert.assertNotNull(indentifierList2); - Assert.assertEquals(0, indentifierList2.size()); - - this.ticCore.unsubscribe(identifier2, subscriber3); - indentifierList3 = this.ticCore.getIndentifiers(subscriber3); - Assert.assertNotNull(indentifierList3); - Assert.assertEquals(0, indentifierList3.size()); - } - - private TICIdentifier plugModem(TICPortDescriptor descriptor) throws DataDictionaryException - { - this.ticPortFinder.addDescriptor(descriptor); - this.waitPlugNotifierUpdate(); - - return new TICIdentifier(descriptor.getPortId(), descriptor.getPortName(), null); - } - - private void unplugModem(TICPortDescriptor descriptor) - { - this.ticPortFinder.removeDescriptor(descriptor); - this.waitPlugNotifierUpdate(); - } - - private TICCoreFrame createFrame(TICIdentifier identifier, TICMode mode, LocalDateTime localDateTime) throws DataDictionaryException - { - DataDictionaryBase content = new DataDictionaryBase(); - if (mode == TICMode.STANDARD) - { - content.set("ADSC", identifier.getSerialNumber()); - } - else - { - content.set("ADCO", identifier.getSerialNumber()); - } - TICCoreFrame frame = new TICCoreFrame(identifier, mode, localDateTime, content); - - return frame; - } - - private void waitPlugNotifierUpdate() - { - Time.sleep(2 * this.plugNotifierPeriod); - } - - private void waitTaskRunning(Task task) - { - while (!task.isRunning()) - { - Time.sleep(50); - } - } - - private void waitTaskTerminated(Task task) - { - while (task.isRunning()) - { - Time.sleep(50); - } - } - - private void waitReadNextFrameSubsbription() - { - Time.sleep(100); - } - - private void waitSubscriberNotification(List onCalls, int expectedSize) - { - long begin = System.nanoTime(); - long elapsed = 0; - while (onCalls.size() < expectedSize && elapsed < NOTIFICATION_TIMEOUT) - { - Time.sleep(50); - elapsed = (System.nanoTime() - begin) / 1000000; - } - } -} diff --git a/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java b/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java deleted file mode 100644 index d31601d..0000000 --- a/src/test/java/enedis/tic/core/TICCoreReadNextFrameTask.java +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.util.task.TaskBase; - -public class TICCoreReadNextFrameTask extends TaskBase -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCore ticCore; - public TICIdentifier identifier; - public TICCoreFrame frame; - public Exception exception; - public int timeout; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier) - { - this(ticCore, identifier, -1); - } - - public TICCoreReadNextFrameTask(TICCore ticCore, TICIdentifier identifier, int timeout) - { - super(); - this.ticCore = ticCore; - this.identifier = identifier; - this.timeout = timeout; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TaskBase - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void process() - { - try - { - if (this.timeout > 0) - { - this.frame = this.ticCore.readNextFrame(this.identifier, this.timeout); - } - else - { - this.frame = this.ticCore.readNextFrame(this.identifier); - } - } - catch (Exception exception) - { - this.exception = exception; - } - - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/test/java/enedis/tic/core/TICCoreStreamMock.java b/src/test/java/enedis/tic/core/TICCoreStreamMock.java deleted file mode 100644 index 4606620..0000000 --- a/src/test/java/enedis/tic/core/TICCoreStreamMock.java +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; - -import enedis.lab.protocol.tic.TICMode; -import enedis.lab.types.DataDictionaryException; - -public class TICCoreStreamMock implements TICCoreStream -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public static List streams = new ArrayList(); - public Collection subscribers; - public boolean running; - public TICIdentifier identifier; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreStreamMock(String portId, String portName, TICMode mode) throws DataDictionaryException - { - super(); - this.subscribers = new HashSet(); - this.running = false; - this.identifier = new TICIdentifier(portId, portName, null); - streams.add(this); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCoreStream - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public TICIdentifier getIdentifier() - { - return this.identifier; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Task - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public boolean isRunning() - { - return this.running; - } - - @Override - public void start() - { - this.running = true; - } - - @Override - public void stop() - { - this.running = false; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// Notifier - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public Collection getSubscribers() - { - return this.subscribers; - } - - @Override - public boolean hasSubscriber(TICCoreSubscriber subscriber) - { - return this.subscribers.contains(subscriber); - } - - @Override - public void subscribe(TICCoreSubscriber subscriber) - { - this.subscribers.add(subscriber); - } - - @Override - public void unsubscribe(TICCoreSubscriber subscriber) - { - this.subscribers.remove(subscriber); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public void notifyOnData(TICCoreFrame frame) - { - for (TICCoreSubscriber subscriber : this.subscribers) - { - subscriber.onData(frame); - } - } - - public void notifyOnError(TICCoreError error) - { - for (TICCoreSubscriber subscriber : this.subscribers) - { - subscriber.onError(error); - } - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java b/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java deleted file mode 100644 index c826a11..0000000 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberMock.java +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import java.util.ArrayList; -import java.util.List; - -public class TICCoreSubscriberMock implements TICCoreSubscriber -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public List onDataCalls = new ArrayList(); - public List onErrorCalls = new ArrayList(); - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// TICCoreSubscriber - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onData(TICCoreFrame frame) - { - this.onDataCalls.add(new TICCoreSubscriberOnDataCall(frame)); - } - - @Override - public void onError(TICCoreError error) - { - this.onErrorCalls.add(new TICCoreSubscriberOnErrorCall(error)); - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java b/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java deleted file mode 100644 index 55555a2..0000000 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberOnDataCall.java +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.mock.FunctionCall; - -@SuppressWarnings("javadoc") -public class TICCoreSubscriberOnDataCall extends FunctionCall -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreFrame frame; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreSubscriberOnDataCall(TICCoreFrame frame) - { - super(); - this.frame = frame; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java b/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java deleted file mode 100644 index 862a016..0000000 --- a/src/test/java/enedis/tic/core/TICCoreSubscriberOnErrorCall.java +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import enedis.lab.mock.FunctionCall; - -@SuppressWarnings("javadoc") -public class TICCoreSubscriberOnErrorCall extends FunctionCall -{ - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTANTS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// TYPES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// STATIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// ATTRIBUTES - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreError error; - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// CONSTRUCTORS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - public TICCoreSubscriberOnErrorCall(TICCoreError error) - { - super(); - this.error = error; - } - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// INTERFACE - /// interfaceName - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PUBLIC METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PROTECTED METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// PRIVATE METHODS - /// - /// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -} diff --git a/src/test/java/enedis/tic/core/TICIdentifierTest.java b/src/test/java/enedis/tic/core/TICIdentifierTest.java deleted file mode 100644 index af522a6..0000000 --- a/src/test/java/enedis/tic/core/TICIdentifierTest.java +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.core; - -import org.junit.Assert; -import org.junit.Test; - -import enedis.lab.types.DataDictionaryException; - -public class TICIdentifierTest -{ - @Test - public void test_constructor_full() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier("{1-1}", "COM3", "021976551632"); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - Assert.assertEquals("COM3", identifier.getPortName()); - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlyPortId() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - Assert.assertNull(identifier.getPortName()); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlyPortName() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, "COM3", null); - - Assert.assertNull(identifier.getPortId()); - Assert.assertEquals("COM3", identifier.getPortName()); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test - public void test_constructor_onlySerialNumber() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertNull(identifier.getPortId()); - Assert.assertNull(identifier.getPortName()); - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - } - - @Test(expected = DataDictionaryException.class) - public void test_constructor_empty() throws DataDictionaryException - { - new TICIdentifier(null, null, null); - } - - @Test - public void test_setPortId_null_notEmpty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, "021976551632"); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - identifier.setPortId(null); - Assert.assertNull(identifier.getPortId()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setPortId_null_empty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier("{1-1}", null, null); - - Assert.assertEquals("{1-1}", identifier.getPortId()); - identifier.setPortId(null); - } - - @Test - public void test_setPortName_null_notEmpty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); - - Assert.assertEquals("COM3", identifier.getPortName()); - identifier.setPortName(null); - Assert.assertNull(identifier.getPortName()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setPortName_null_empty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, "COM3", null); - - Assert.assertEquals("COM3", identifier.getPortName()); - identifier.setPortName(null); - } - - @Test - public void test_setSerialNumber_null_notEmpty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, "COM3", "021976551632"); - - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - identifier.setSerialNumber(null); - Assert.assertNull(identifier.getSerialNumber()); - } - - @Test(expected = DataDictionaryException.class) - public void test_setSerialNumber_null_empty() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertEquals("021976551632", identifier.getSerialNumber()); - identifier.setSerialNumber(null); - } - - @Test - public void test_matches_null() throws DataDictionaryException - { - TICIdentifier identifier = new TICIdentifier(null, null, "021976551632"); - - Assert.assertFalse(identifier.matches(null)); - } - - @Test - public void test_matches_same_serialNumber() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM4", "021976551632"); - - Assert.assertTrue(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_serialNumber() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM3", "021976551638"); - - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_same_portId() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-1}", "COM4", "021976551633"); - TICIdentifier identifier3 = new TICIdentifier("{1-1}", "COM5", null); - - Assert.assertTrue(identifier1.matches(identifier3)); - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_portId() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", null, null); - TICIdentifier identifier2 = new TICIdentifier("{1-7}", null, null); - - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_same_portName() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier("{1-1}", "COM3", "021976551632"); - TICIdentifier identifier2 = new TICIdentifier("{1-2}", "COM3", "021976551633"); - TICIdentifier identifier3 = new TICIdentifier("{1-3}", "COM3", null); - TICIdentifier identifier4 = new TICIdentifier(null, "COM3", null); - - Assert.assertTrue(identifier1.matches(identifier4)); - Assert.assertFalse(identifier1.matches(identifier3)); - Assert.assertFalse(identifier1.matches(identifier2)); - } - - @Test - public void test_matches_different_portName() throws DataDictionaryException - { - TICIdentifier identifier1 = new TICIdentifier(null, "COM3", null); - TICIdentifier identifier2 = new TICIdentifier(null, "COM8", null); - - Assert.assertFalse(identifier1.matches(identifier2)); - } -} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java deleted file mode 100644 index 2974e57..0000000 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketEventFactoryTest.java +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import org.junit.Assert; -import org.junit.Test; - -import enedis.lab.util.message.Message; -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import enedis.tic.service.message.factory.TIC2WebSocketEventFactory; - -@SuppressWarnings("javadoc") -public class TIC2WebSocketEventFactoryTest -{ - @Test - public void testGetMessage_EventOnTICData() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketEventFactory factory = new TIC2WebSocketEventFactory(); - - String text = "{\"type\":\"" + MessageType.EVENT.name() + "\",\"name\":\"" + EventOnTICData.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"data\":{\"captureDateTime\":\"01/01/2000 00:00:00\",\"mode\":\"STANDARD\",\"identifier\":{\"serialNumber\":\"010203040506\"},\"content\":{\"URMS\":230}}}"; - - Message message = factory.getMessage(text, EventOnTICData.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof EventOnTICData); - - EventOnTICData event = (EventOnTICData) message; - - Assert.assertEquals(MessageType.EVENT, event.getType()); - Assert.assertEquals(EventOnTICData.NAME, event.getName()); - Assert.assertNotNull(event.getData()); - - Message messageDecoded = factory.getMessage(event.toString(), EventOnTICData.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(event)); - } - - @Test - public void testGetMessage_EventOnError() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketEventFactory factory = new TIC2WebSocketEventFactory(); - - String text = "{\"type\":\"" + MessageType.EVENT.name() + "\",\"name\":\"" + EventOnError.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"data\":{\"errorCode\":\"-1\",\"errorMessage\":\"une erreur\",\"identifier\":{\"serialNumber\":\"010203040506\"}}}"; - - Message message = factory.getMessage(text, EventOnError.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof EventOnError); - - EventOnError event = (EventOnError) message; - - Assert.assertEquals(MessageType.EVENT, event.getType()); - Assert.assertEquals(EventOnError.NAME, event.getName()); - Assert.assertNotNull(event.getData()); - - Message messageDecoded = factory.getMessage(event.toString(), EventOnError.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(event)); - } -} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java deleted file mode 100644 index 0431191..0000000 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketMessageFactoryTest.java +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import org.junit.Test; - -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import enedis.tic.service.message.factory.TIC2WebSocketMessageFactory; - -@SuppressWarnings("javadoc") -public class TIC2WebSocketMessageFactoryTest -{ - @Test(expected = UnsupportedMessageException.class) - public void testGetMessage_UnsupportedRequest() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketMessageFactory factory = new TIC2WebSocketMessageFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"coucou\"}"; - - factory.getMessage(text); - } -} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java deleted file mode 100644 index 2ddd38c..0000000 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketRequestFactoryTest.java +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import org.junit.Assert; -import org.junit.Test; - -import enedis.lab.util.message.Message; -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import enedis.tic.service.message.factory.TIC2WebSocketRequestFactory; - -@SuppressWarnings("javadoc") -public class TIC2WebSocketRequestFactoryTest -{ - @Test - public void testGetMessage_RequestGetAvailableTics() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestGetAvailableTICs.NAME + "\"}"; - - Message message = factory.getMessage(text, RequestGetAvailableTICs.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestGetAvailableTICs); - - RequestGetAvailableTICs request = (RequestGetAvailableTICs) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestGetAvailableTICs.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestGetAvailableTICs.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestGetModemsInfo() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestGetModemsInfo.NAME + "\"}"; - - Message message = factory.getMessage(text, RequestGetModemsInfo.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestGetModemsInfo); - - RequestGetModemsInfo request = (RequestGetModemsInfo) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestGetModemsInfo.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestGetModemsInfo.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestReadTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestReadTIC.NAME + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; - - Message message = factory.getMessage(text, RequestReadTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestReadTIC); - - RequestReadTIC request = (RequestReadTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestReadTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestReadTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestSubscribeTIC_allTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestSubscribeTIC.NAME + "\"}"; - - Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestSubscribeTIC); - - RequestSubscribeTIC request = (RequestSubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestSubscribeTIC_oneTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestSubscribeTIC.NAME + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; - - Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestSubscribeTIC); - - RequestSubscribeTIC request = (RequestSubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestSubscribeTIC_severalTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestSubscribeTIC.NAME - + "\",\"data\":[{\"serialNumber\":\"010203040506\"},{\"portId\":\"1-1\"}]}"; - - Message message = factory.getMessage(text, RequestSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestSubscribeTIC); - - RequestSubscribeTIC request = (RequestSubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestSubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestUnsubscribeTIC_allTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestUnsubscribeTIC.NAME + "\"}"; - - Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestUnsubscribeTIC); - - RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestUnsubscribeTIC_oneTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestUnsubscribeTIC.NAME + "\",\"data\":{\"serialNumber\":\"010203040506\"}}"; - - Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestUnsubscribeTIC); - - RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } - - @Test - public void testGetMessage_RequestUnsubscribeTIC_severalTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketRequestFactory factory = new TIC2WebSocketRequestFactory(); - - String text = "{\"type\":\"" + MessageType.REQUEST.name() + "\",\"name\":\"" + RequestUnsubscribeTIC.NAME - + "\",\"data\":[{\"serialNumber\":\"010203040506\"},{\"portId\":\"1-1\"}]}"; - - Message message = factory.getMessage(text, RequestUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof RequestUnsubscribeTIC); - - RequestUnsubscribeTIC request = (RequestUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.REQUEST, request.getType()); - Assert.assertEquals(RequestUnsubscribeTIC.NAME, request.getName()); - Assert.assertNotNull(request.getData()); - - Message messageDecoded = factory.getMessage(request.toString(), RequestUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(request)); - } -} diff --git a/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java b/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java deleted file mode 100644 index dc8068e..0000000 --- a/src/test/java/enedis/tic/service/message/TIC2WebSocketResponseFactoryTest.java +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.message; - -import org.junit.Assert; -import org.junit.Test; - -import enedis.lab.util.message.Message; -import enedis.lab.util.message.MessageType; -import enedis.lab.util.message.exception.MessageInvalidContentException; -import enedis.lab.util.message.exception.MessageInvalidFormatException; -import enedis.lab.util.message.exception.MessageInvalidTypeException; -import enedis.lab.util.message.exception.MessageKeyNameDoesntExistException; -import enedis.lab.util.message.exception.MessageKeyTypeDoesntExistException; -import enedis.lab.util.message.exception.UnsupportedMessageException; -import enedis.tic.service.message.factory.TIC2WebSocketResponseFactory; - -@SuppressWarnings("javadoc") -public class TIC2WebSocketResponseFactoryTest -{ - @Test - public void testGetMessage_ResponseGetAvailableTics() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseGetAvailableTICs.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"data\":[{\"serialNumber\":\"010203040506\"}]}"; - - Message message = factory.getMessage(text, ResponseGetAvailableTICs.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseGetAvailableTICs); - - ResponseGetAvailableTICs response = (ResponseGetAvailableTICs) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseGetAvailableTICs.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - Assert.assertNotNull(response.getData()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseGetAvailableTICs.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseGetModemsInfo() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseGetModemsInfo.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"errorMessage\":\"error\"}"; - - Message message = factory.getMessage(text, ResponseGetModemsInfo.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseGetModemsInfo); - - ResponseGetModemsInfo response = (ResponseGetModemsInfo) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseGetModemsInfo.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertEquals("error", response.getErrorMessage()); - Assert.assertNull(response.getData()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseGetModemsInfo.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseReadTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseReadTIC.NAME - + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0,\"data\":{\"captureDateTime\":\"01/01/2000 00:00:00\",\"mode\":\"STANDARD\",\"identifier\":{\"serialNumber\":\"010203040506\"},\"content\":{\"URMS\":230}}}"; - - Message message = factory.getMessage(text, ResponseReadTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseReadTIC); - - ResponseReadTIC response = (ResponseReadTIC) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseReadTIC.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - Assert.assertNotNull(response.getData()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseReadTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseSubscribeTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseSubscribeTIC.NAME + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0}"; - - Message message = factory.getMessage(text, ResponseSubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseSubscribeTIC); - - ResponseSubscribeTIC response = (ResponseSubscribeTIC) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseSubscribeTIC.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseSubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } - - @Test - public void testGetMessage_ResponseUnsubscribeTIC_allTIC() throws MessageInvalidFormatException, MessageKeyTypeDoesntExistException, MessageKeyNameDoesntExistException, - MessageInvalidTypeException, UnsupportedMessageException, MessageInvalidContentException - { - TIC2WebSocketResponseFactory factory = new TIC2WebSocketResponseFactory(); - - String text = "{\"type\":\"" + MessageType.RESPONSE.name() + "\",\"name\":\"" + ResponseUnsubscribeTIC.NAME + "\",\"dateTime\":\"01/01/2000 00:00:00\",\"errorCode\":0}"; - - Message message = factory.getMessage(text, ResponseUnsubscribeTIC.NAME); - - Assert.assertNotNull(message); - Assert.assertTrue(message instanceof ResponseUnsubscribeTIC); - - ResponseUnsubscribeTIC response = (ResponseUnsubscribeTIC) message; - - Assert.assertEquals(MessageType.RESPONSE, response.getType()); - Assert.assertEquals(ResponseUnsubscribeTIC.NAME, response.getName()); - Assert.assertNotNull(response.getDateTime()); - Assert.assertEquals(0, response.getErrorCode().intValue()); - Assert.assertNull(response.getErrorMessage()); - - Message messageDecoded = factory.getMessage(response.toString(), ResponseUnsubscribeTIC.NAME); - Assert.assertNotNull(message); - Assert.assertTrue(messageDecoded.equals(response)); - } -} diff --git a/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java b/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java deleted file mode 100644 index 948283a..0000000 --- a/src/test/java/enedis/tic/service/netty/TIC2WebSocketServerTest.java +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2025 Enedis Smarties team -// -// SPDX-FileContributor: Jehan BOUSCH -// SPDX-FileContributor: Mathieu SABARTHES -// -// SPDX-License-Identifier: Apache-2.0 - -package enedis.tic.service.netty; - -import org.junit.Test; -import static org.junit.Assert.*; - -import enedis.tic.service.client.TIC2WebSocketClientPool; -import enedis.tic.service.client.TIC2WebSocketClientPoolBase; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandler; -import enedis.tic.service.requesthandler.TIC2WebSocketRequestHandlerBase; -import enedis.tic.core.TICCoreBase; -import enedis.lab.protocol.tic.TICMode; -import java.util.Arrays; - -/** - * Test for TIC2WebSocketServer - */ -public class TIC2WebSocketServerTest -{ - @Test - public void testServerInstantiation() - { - // Create mock dependencies - TIC2WebSocketClientPool clientPool = new TIC2WebSocketClientPoolBase(); - TIC2WebSocketRequestHandler requestHandler = new TIC2WebSocketRequestHandlerBase( - new TICCoreBase(TICMode.HISTORIC, Arrays.asList("COM1"))); - - // Test server creation - TIC2WebSocketServer server = new TIC2WebSocketServer("localhost", 8080, clientPool, requestHandler); - - assertNotNull("Server should not be null", server); - } -} From 66f79b4720854b8eb95390064686efaab2bbf56b Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Wed, 4 Feb 2026 16:39:41 +0100 Subject: [PATCH 40/59] fix: Delete unused setup script --- src/main/scripts/setup_TIC2WebSocket.sh | 92 ------------------------- 1 file changed, 92 deletions(-) delete mode 100644 src/main/scripts/setup_TIC2WebSocket.sh diff --git a/src/main/scripts/setup_TIC2WebSocket.sh b/src/main/scripts/setup_TIC2WebSocket.sh deleted file mode 100644 index 6a34c31..0000000 --- a/src/main/scripts/setup_TIC2WebSocket.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2025 Enedis Smarties team -# -# SPDX-FileContributor: Jehan BOUSCH -# SPDX-FileContributor: Mathieu SABARTHES -# -# SPDX-License-Identifier: Apache-2.0 - -############################################ VARIABLES ############################################ - -TIC2WebSocket_DIR=$(dirname "${BASH_SOURCE[0]}") -TIC2WebSocket_SCRIPT="${BASH_SOURCE[0]}" -TIC2WebSocket_PACKAGE="$TIC2WebSocket_DIR"/TIC2WebSocket-0.0-bin.zip -TIC2WebSocket_INSTALLDIR=/Applications/TIC2WebSocket - -############################################ FUNCTIONS ############################################ - -function TIC2WebSocket_install() -{ - sudo mkdir -p "$TIC2WebSocket_INSTALLDIR" - sudo unzip "$TIC2WebSocket_PACKAGE" -d "$TIC2WebSocket_INSTALLDIR" - sudo chmod -R 777 "$TIC2WebSocket_INSTALLDIR" -} - -function TIC2WebSocket_uninstall() -{ - sudo rm -Rf "$TIC2WebSocket_INSTALLDIR" -} - -function TIC2WebSocket_isInstalled() -{ - "$TIC2WebSocket_INSTALLDIR"/TIC2WebSocket.sh --version > /dev/null 2>&1 - if [ $? != 0 ]; then - echo false - return 0 - fi - echo true -} - -############################################## MAIN ############################################### - -option="$1" - -case "$option" in - install) - - if [ $(TIC2WebSocket_isInstalled) == "true" ]; then - echo "TIC2WebSocket already installed" - else - echo "Install TIC2WebSocket" - TIC2WebSocket_install - fi - ;; - - uninstall) - - if [ $(TIC2WebSocket_isInstalled) == "true" ]; then - echo "Uninstall TIC2WebSocket" - TIC2WebSocket_uninstall - else - echo "TIC2WebSocket already uninstalled" - fi - ;; - - reinstall) - - if [ $(TIC2WebSocket_isInstalled) == "true" ]; then - echo "Remove TIC2WebSocket" - TIC2WebSocket_uninstall - fi - echo "Install TIC2WebSocket" - TIC2WebSocket_install - ;; - - status) - - if [ $(TIC2WebSocket_isInstalled) == "true" ]; then - echo "TIC2WebSocket installed" - else - echo "TIC2WebSocket not installed" - fi - ;; - - *) - if [ "$option" != "" ]; then - echo "Usage: $(basename "$TIC2WebSocket_SCRIPT") {install|uninstallreinstall|status}" - exit 1 - fi - ;; - -esac From 0ebc4ed63085f2eb1514fa8db734a4fea701a80f Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Wed, 4 Feb 2026 16:54:21 +0100 Subject: [PATCH 41/59] chore: Format --- src/assembly/all.xml | 7 +++---- src/assembly/bin.xml | 7 +++---- src/assembly/doc.xml | 7 +++---- src/assembly/src.xml | 9 ++++----- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/assembly/all.xml b/src/assembly/all.xml index 1e81400..32d1fe0 100644 --- a/src/assembly/all.xml +++ b/src/assembly/all.xml @@ -8,9 +8,8 @@ SPDX-License-Identifier: Apache-2.0 --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd"> all zip @@ -99,4 +98,4 @@ SPDX-License-Identifier: Apache-2.0 - + \ No newline at end of file diff --git a/src/assembly/bin.xml b/src/assembly/bin.xml index efe8df6..8b408bf 100644 --- a/src/assembly/bin.xml +++ b/src/assembly/bin.xml @@ -8,9 +8,8 @@ SPDX-License-Identifier: Apache-2.0 --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd"> bin @@ -31,7 +30,7 @@ SPDX-License-Identifier: Apache-2.0 CHANGELOG.md CHANGELOG.md - + ${project.build.scriptSourceDirectory}/${project.name}.bat ${project.name}.bat diff --git a/src/assembly/doc.xml b/src/assembly/doc.xml index 9a2fe16..47ec092 100644 --- a/src/assembly/doc.xml +++ b/src/assembly/doc.xml @@ -8,9 +8,8 @@ SPDX-License-Identifier: Apache-2.0 --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd"> doc zip @@ -20,7 +19,7 @@ SPDX-License-Identifier: Apache-2.0 ${project.build.directory}/site - . + . true **/*.log diff --git a/src/assembly/src.xml b/src/assembly/src.xml index 9769f18..28de888 100644 --- a/src/assembly/src.xml +++ b/src/assembly/src.xml @@ -8,9 +8,8 @@ SPDX-License-Identifier: Apache-2.0 --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd"> src zip @@ -23,9 +22,9 @@ SPDX-License-Identifier: Apache-2.0 true **/*.log - **/var/** + **/var/** **/${project.build.directory}/** - + \ No newline at end of file From e5b4744eabeb38b83003ff08d0a24b8f38b5c136 Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Thu, 5 Feb 2026 16:19:43 +0100 Subject: [PATCH 42/59] fix: Update launchers, diagnostic and debug scripts --- src/assembly/all.xml | 77 +++++++++++++---- src/assembly/bin.xml | 77 +++++++++++++---- src/main/scripts/SerialPortPlugNotifier.sh | 14 ---- .../scripts/TIC2WebSocket_remote_debug.sh | 14 ---- src/main/scripts/TICPortPlugNotifier.sh | 14 ---- .../debug/TIC2WebSocket_remote_debug.sh | 23 ++++++ .../ModemFinder.bat} | 35 ++++---- src/main/scripts/diagnostic/ModemFinder.sh | 21 +++++ .../scripts/diagnostic/ModemPlugNotifier.bat | 21 +++++ .../scripts/diagnostic/ModemPlugNotifier.sh | 21 +++++ .../scripts/diagnostic/SerialPortFinder.bat | 21 +++++ .../scripts/diagnostic/SerialPortFinder.sh | 21 +++++ .../TICCore.bat} | 35 ++++---- .../TICCore.sh} | 9 +- .../scripts/{ => diagnostic}/TICPortDump.bat | 30 +++---- .../scripts/{ => diagnostic}/TICPortDump.ps1 | 82 +++++++++---------- .../scripts/{ => diagnostic}/TICPortDump.sh | 0 .../UsbPortFinder.bat} | 9 +- src/main/scripts/diagnostic/UsbPortFinder.sh | 21 +++++ src/main/scripts/launcher/TIC2WebSocket.bat | 23 ++++++ src/main/scripts/launcher/TIC2WebSocket.sh | 23 ++++++ 21 files changed, 427 insertions(+), 164 deletions(-) delete mode 100644 src/main/scripts/SerialPortPlugNotifier.sh delete mode 100644 src/main/scripts/TIC2WebSocket_remote_debug.sh delete mode 100644 src/main/scripts/TICPortPlugNotifier.sh create mode 100644 src/main/scripts/debug/TIC2WebSocket_remote_debug.sh rename src/main/scripts/{TICPortPlugNotifier.bat => diagnostic/ModemFinder.bat} (50%) create mode 100644 src/main/scripts/diagnostic/ModemFinder.sh create mode 100644 src/main/scripts/diagnostic/ModemPlugNotifier.bat create mode 100644 src/main/scripts/diagnostic/ModemPlugNotifier.sh create mode 100644 src/main/scripts/diagnostic/SerialPortFinder.bat create mode 100644 src/main/scripts/diagnostic/SerialPortFinder.sh rename src/main/scripts/{SerialPortPlugNotifier.bat => diagnostic/TICCore.bat} (50%) rename src/main/scripts/{TIC2WebSocket.sh => diagnostic/TICCore.sh} (53%) rename src/main/scripts/{ => diagnostic}/TICPortDump.bat (96%) rename src/main/scripts/{ => diagnostic}/TICPortDump.ps1 (95%) rename src/main/scripts/{ => diagnostic}/TICPortDump.sh (100%) rename src/main/scripts/{TIC2WebSocket.bat => diagnostic/UsbPortFinder.bat} (50%) create mode 100644 src/main/scripts/diagnostic/UsbPortFinder.sh create mode 100644 src/main/scripts/launcher/TIC2WebSocket.bat create mode 100644 src/main/scripts/launcher/TIC2WebSocket.sh diff --git a/src/assembly/all.xml b/src/assembly/all.xml index 32d1fe0..af81abf 100644 --- a/src/assembly/all.xml +++ b/src/assembly/all.xml @@ -16,6 +16,7 @@ SPDX-License-Identifier: Apache-2.0 tar.gz false + true @@ -23,6 +24,7 @@ SPDX-License-Identifier: Apache-2.0 false + CHANGELOG.md @@ -32,47 +34,86 @@ SPDX-License-Identifier: Apache-2.0 LICENCE.txt LICENCE.txt + + + + ${project.build.scriptSourceDirectory}/launcher/${project.name}.bat + launcher/${project.name}.bat + + + ${project.build.scriptSourceDirectory}/launcher/${project.name}.sh + launcher/${project.name}.sh + 0755 + + + + + ${project.build.scriptSourceDirectory}/debug/${project.name}_remote_debug.sh + debug/${project.name}_remote_debug.sh + 0755 + + + + + ${project.build.scriptSourceDirectory}/diagnostic/SerialPortFinder.bat + diagnostic/SerialPortFinder.bat + + + ${project.build.scriptSourceDirectory}/diagnostic/SerialPortFinder.sh + diagnostic/SerialPortFinder.sh + 0755 + - ${project.build.scriptSourceDirectory}/${project.name}.bat - ${project.name}.bat + ${project.build.scriptSourceDirectory}/diagnostic/UsbPortFinder.bat + diagnostic/UsbPortFinder.bat - ${project.build.scriptSourceDirectory}/${project.name}.sh - ${project.name}.sh + ${project.build.scriptSourceDirectory}/diagnostic/UsbPortFinder.sh + diagnostic/UsbPortFinder.sh 0755 - ${project.build.scriptSourceDirectory}/SerialPortPlugNotifier.bat - SerialPortPlugNotifier.bat + ${project.build.scriptSourceDirectory}/diagnostic/ModemFinder.bat + diagnostic/ModemFinder.bat - ${project.build.scriptSourceDirectory}/SerialPortPlugNotifier.sh - SerialPortPlugNotifier.sh + ${project.build.scriptSourceDirectory}/diagnostic/ModemFinder.sh + diagnostic/ModemFinder.sh 0755 - ${project.build.scriptSourceDirectory}/TICPortPlugNotifier.bat - TICPortPlugNotifier.bat + ${project.build.scriptSourceDirectory}/diagnostic/ModemPlugNotifier.bat + diagnostic/ModemPlugNotifier.bat - ${project.build.scriptSourceDirectory}/TICPortPlugNotifier.sh - TICPortPlugNotifier.sh + ${project.build.scriptSourceDirectory}/diagnostic/ModemPlugNotifier.sh + diagnostic/ModemPlugNotifier.sh 0755 - ${project.build.scriptSourceDirectory}/TICPortDump.bat - TICPortDump.bat + ${project.build.scriptSourceDirectory}/diagnostic/TICCore.bat + diagnostic/TICCore.bat - ${project.build.scriptSourceDirectory}/TICPortDump.ps1 - TICPortDump.ps1 + ${project.build.scriptSourceDirectory}/diagnostic/TICCore.sh + diagnostic/TICCore.sh + 0755 + + + ${project.build.scriptSourceDirectory}/diagnostic/TICPortDump.bat + diagnostic/TICPortDump.bat - ${project.build.scriptSourceDirectory}/TICPortDump.sh - TICPortDump.sh + ${project.build.scriptSourceDirectory}/diagnostic/TICPortDump.ps1 + diagnostic/TICPortDump.ps1 + + + ${project.build.scriptSourceDirectory}/diagnostic/TICPortDump.sh + diagnostic/TICPortDump.sh 0755 + ${basedir}/src/main/resources/config diff --git a/src/assembly/bin.xml b/src/assembly/bin.xml index 8b408bf..2a355ec 100644 --- a/src/assembly/bin.xml +++ b/src/assembly/bin.xml @@ -18,6 +18,7 @@ SPDX-License-Identifier: Apache-2.0 tar.gz false + true @@ -25,52 +26,92 @@ SPDX-License-Identifier: Apache-2.0 false + CHANGELOG.md CHANGELOG.md + + + + ${project.build.scriptSourceDirectory}/launcher/${project.name}.bat + launcher/${project.name}.bat + + + ${project.build.scriptSourceDirectory}/launcher/${project.name}.sh + launcher/${project.name}.sh + 0755 + + + + + ${project.build.scriptSourceDirectory}/debug/${project.name}_remote_debug.sh + debug/${project.name}_remote_debug.sh + 0755 + + + + + ${project.build.scriptSourceDirectory}/diagnostic/SerialPortFinder.bat + diagnostic/SerialPortFinder.bat + + + ${project.build.scriptSourceDirectory}/diagnostic/SerialPortFinder.sh + diagnostic/SerialPortFinder.sh + 0755 + - ${project.build.scriptSourceDirectory}/${project.name}.bat - ${project.name}.bat + ${project.build.scriptSourceDirectory}/diagnostic/UsbPortFinder.bat + diagnostic/UsbPortFinder.bat - ${project.build.scriptSourceDirectory}/${project.name}.sh - ${project.name}.sh + ${project.build.scriptSourceDirectory}/diagnostic/UsbPortFinder.sh + diagnostic/UsbPortFinder.sh 0755 - ${project.build.scriptSourceDirectory}/SerialPortPlugNotifier.bat - SerialPortPlugNotifier.bat + ${project.build.scriptSourceDirectory}/diagnostic/ModemFinder.bat + diagnostic/ModemFinder.bat - ${project.build.scriptSourceDirectory}/SerialPortPlugNotifier.sh - SerialPortPlugNotifier.sh + ${project.build.scriptSourceDirectory}/diagnostic/ModemFinder.sh + diagnostic/ModemFinder.sh 0755 - ${project.build.scriptSourceDirectory}/TICPortPlugNotifier.bat - TICPortPlugNotifier.bat + ${project.build.scriptSourceDirectory}/diagnostic/ModemPlugNotifier.bat + diagnostic/ModemPlugNotifier.bat - ${project.build.scriptSourceDirectory}/TICPortPlugNotifier.sh - TICPortPlugNotifier.sh + ${project.build.scriptSourceDirectory}/diagnostic/ModemPlugNotifier.sh + diagnostic/ModemPlugNotifier.sh 0755 - ${project.build.scriptSourceDirectory}/TICPortDump.bat - TICPortDump.bat + ${project.build.scriptSourceDirectory}/diagnostic/TICCore.bat + diagnostic/TICCore.bat - ${project.build.scriptSourceDirectory}/TICPortDump.ps1 - TICPortDump.ps1 + ${project.build.scriptSourceDirectory}/diagnostic/TICCore.sh + diagnostic/TICCore.sh + 0755 + + + ${project.build.scriptSourceDirectory}/diagnostic/TICPortDump.bat + diagnostic/TICPortDump.bat - ${project.build.scriptSourceDirectory}/TICPortDump.sh - TICPortDump.sh + ${project.build.scriptSourceDirectory}/diagnostic/TICPortDump.ps1 + diagnostic/TICPortDump.ps1 + + + ${project.build.scriptSourceDirectory}/diagnostic/TICPortDump.sh + diagnostic/TICPortDump.sh 0755 + ${basedir}/src/main/resources/config diff --git a/src/main/scripts/SerialPortPlugNotifier.sh b/src/main/scripts/SerialPortPlugNotifier.sh deleted file mode 100644 index 7617085..0000000 --- a/src/main/scripts/SerialPortPlugNotifier.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2025 Enedis Smarties team -# -# SPDX-FileContributor: Jehan BOUSCH -# SPDX-FileContributor: Mathieu SABARTHES -# -# SPDX-License-Identifier: Apache-2.0 - -# Get script directory -SCRIPT_DIRECTORY=$(dirname "$0") - -# Run executable -java -cp "$SCRIPT_DIRECTORY/lib/*" enedis.lab.io.serialport.SerialPortPlugNotifier $* \ No newline at end of file diff --git a/src/main/scripts/TIC2WebSocket_remote_debug.sh b/src/main/scripts/TIC2WebSocket_remote_debug.sh deleted file mode 100644 index 5ca9c68..0000000 --- a/src/main/scripts/TIC2WebSocket_remote_debug.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2025 Enedis Smarties team -# -# SPDX-FileContributor: Jehan BOUSCH -# SPDX-FileContributor: Mathieu SABARTHES -# -# SPDX-License-Identifier: Apache-2.0 - -# Get script directory -SCRIPT_DIRECTORY=$(dirname "$0") - -# Run executable -java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=0.0.0.0:8000 -DlogDir="$SCRIPT_DIRECTORY/var/log" -DconfigDir="$SCRIPT_DIRECTORY/var/config" -cp "$SCRIPT_DIRECTORY/lib/*" tic.service.app.TIC2WebSocketApplication $* \ No newline at end of file diff --git a/src/main/scripts/TICPortPlugNotifier.sh b/src/main/scripts/TICPortPlugNotifier.sh deleted file mode 100644 index 2747dce..0000000 --- a/src/main/scripts/TICPortPlugNotifier.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2025 Enedis Smarties team -# -# SPDX-FileContributor: Jehan BOUSCH -# SPDX-FileContributor: Mathieu SABARTHES -# -# SPDX-License-Identifier: Apache-2.0 - -# Get script directory -SCRIPT_DIRECTORY=$(dirname "$0") - -# Run executable -java -cp "$SCRIPT_DIRECTORY/lib/*" enedis.lab.io.tic.TICPortPlugNotifier $* \ No newline at end of file diff --git a/src/main/scripts/debug/TIC2WebSocket_remote_debug.sh b/src/main/scripts/debug/TIC2WebSocket_remote_debug.sh new file mode 100644 index 0000000..606ca45 --- /dev/null +++ b/src/main/scripts/debug/TIC2WebSocket_remote_debug.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +# Get script directory +SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) + +# Get distribution root directory (parent of script directory) +ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") + +# Define log directory and config file +LOG_DIRECTORY="$ROOT_DIRECTORY/var/log" +CONFIG_FILE="$ROOT_DIRECTORY/var/config/TIC2WebSocketConfiguration.json" +CLASSPATH="$ROOT_DIRECTORY/lib/*" +MAIN_CLASS=tic.service.TIC2WebSocketApplication + +# Run executable +java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=0.0.0.0:8000 -DlogDir="$LOG_DIRECTORY" -DconfigFile="$CONFIG_FILE" -cp "$CLASSPATH" $MAIN_CLASS $* \ No newline at end of file diff --git a/src/main/scripts/TICPortPlugNotifier.bat b/src/main/scripts/diagnostic/ModemFinder.bat similarity index 50% rename from src/main/scripts/TICPortPlugNotifier.bat rename to src/main/scripts/diagnostic/ModemFinder.bat index 3121547..f9b5f7f 100644 --- a/src/main/scripts/TICPortPlugNotifier.bat +++ b/src/main/scripts/diagnostic/ModemFinder.bat @@ -1,14 +1,21 @@ -REM Copyright (C) 2025 Enedis Smarties team -REM -REM SPDX-FileContributor: Jehan BOUSCH -REM SPDX-FileContributor: Mathieu SABARTHES -REM -REM SPDX-License-Identifier: Apache-2.0 - -@echo off - -REM Get script directory -set SCRIPT_DIRECTORY=%~dp0 - -REM Run executable -java -cp "%SCRIPT_DIRECTORY%/lib/*" enedis.lab.io.tic.TICPortPlugNotifier %* \ No newline at end of file +REM Copyright (C) 2025 Enedis Smarties team +REM +REM SPDX-FileContributor: Jehan BOUSCH +REM SPDX-FileContributor: Mathieu SABARTHES +REM +REM SPDX-License-Identifier: Apache-2.0 + +@echo off + +REM Get script directory +set SCRIPT_DIRECTORY=%~dp0 + +REM Get distribution root directory (parent of script directory) +for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ + +REM Define classpath and main class +set CLASSPATH=%ROOT_DIRECTORY%lib\* +set MAIN_CLASS=tic.diagnostic.modem.ModemFinderApp + +REM Run executable +java -cp "%CLASSPATH%" %MAIN_CLASS% %* diff --git a/src/main/scripts/diagnostic/ModemFinder.sh b/src/main/scripts/diagnostic/ModemFinder.sh new file mode 100644 index 0000000..fcdd897 --- /dev/null +++ b/src/main/scripts/diagnostic/ModemFinder.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +# Get script directory +SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) + +# Get distribution root directory (parent of script directory) +ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") + +# Define classpath and main class +CLASSPATH="$ROOT_DIRECTORY/lib/*" +MAIN_CLASS=tic.diagnostic.modem.ModemFinderApp + +# Run executable +java -cp "$CLASSPATH" $MAIN_CLASS $* diff --git a/src/main/scripts/diagnostic/ModemPlugNotifier.bat b/src/main/scripts/diagnostic/ModemPlugNotifier.bat new file mode 100644 index 0000000..a5bf39b --- /dev/null +++ b/src/main/scripts/diagnostic/ModemPlugNotifier.bat @@ -0,0 +1,21 @@ +REM Copyright (C) 2025 Enedis Smarties team +REM +REM SPDX-FileContributor: Jehan BOUSCH +REM SPDX-FileContributor: Mathieu SABARTHES +REM +REM SPDX-License-Identifier: Apache-2.0 + +@echo off + +REM Get script directory +set SCRIPT_DIRECTORY=%~dp0 + +REM Get distribution root directory (parent of script directory) +for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ + +REM Define classpath and main class +set CLASSPATH=%ROOT_DIRECTORY%lib\* +set MAIN_CLASS=tic.diagnostic.modem.ModemPlugNotifierApp + +REM Run executable +java -cp "%CLASSPATH%" %MAIN_CLASS% %* \ No newline at end of file diff --git a/src/main/scripts/diagnostic/ModemPlugNotifier.sh b/src/main/scripts/diagnostic/ModemPlugNotifier.sh new file mode 100644 index 0000000..458dc50 --- /dev/null +++ b/src/main/scripts/diagnostic/ModemPlugNotifier.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +# Get script directory +SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) + +# Get distribution root directory (parent of script directory) +ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") + +# Define classpath and main class +CLASSPATH="$ROOT_DIRECTORY/lib/*" +MAIN_CLASS=tic.diagnostic.modem.ModemPlugNotifierApp + +# Run executable +java -cp "$CLASSPATH" $MAIN_CLASS $* \ No newline at end of file diff --git a/src/main/scripts/diagnostic/SerialPortFinder.bat b/src/main/scripts/diagnostic/SerialPortFinder.bat new file mode 100644 index 0000000..910c085 --- /dev/null +++ b/src/main/scripts/diagnostic/SerialPortFinder.bat @@ -0,0 +1,21 @@ +REM Copyright (C) 2025 Enedis Smarties team +REM +REM SPDX-FileContributor: Jehan BOUSCH +REM SPDX-FileContributor: Mathieu SABARTHES +REM +REM SPDX-License-Identifier: Apache-2.0 + +@echo off + +REM Get script directory +set SCRIPT_DIRECTORY=%~dp0 + +REM Get distribution root directory (parent of script directory) +for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ + +REM Define classpath and main class +set CLASSPATH=%ROOT_DIRECTORY%lib\* +set MAIN_CLASS=tic.diagnostic.serialport.SerialPortFinderApp + +REM Run executable +java -cp "%CLASSPATH%" %MAIN_CLASS% %* \ No newline at end of file diff --git a/src/main/scripts/diagnostic/SerialPortFinder.sh b/src/main/scripts/diagnostic/SerialPortFinder.sh new file mode 100644 index 0000000..7de8ce3 --- /dev/null +++ b/src/main/scripts/diagnostic/SerialPortFinder.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +# Get script directory +SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) + +# Get distribution root directory (parent of script directory) +ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") + +# Define classpath and main class +CLASSPATH="$ROOT_DIRECTORY/lib/*" +MAIN_CLASS=tic.diagnostic.serialport.SerialPortFinderApp + +# Run executable +java -cp "$CLASSPATH" $MAIN_CLASS $* \ No newline at end of file diff --git a/src/main/scripts/SerialPortPlugNotifier.bat b/src/main/scripts/diagnostic/TICCore.bat similarity index 50% rename from src/main/scripts/SerialPortPlugNotifier.bat rename to src/main/scripts/diagnostic/TICCore.bat index 78d1907..7eaffee 100644 --- a/src/main/scripts/SerialPortPlugNotifier.bat +++ b/src/main/scripts/diagnostic/TICCore.bat @@ -1,14 +1,21 @@ -REM Copyright (C) 2025 Enedis Smarties team -REM -REM SPDX-FileContributor: Jehan BOUSCH -REM SPDX-FileContributor: Mathieu SABARTHES -REM -REM SPDX-License-Identifier: Apache-2.0 - -@echo off - -REM Get script directory -set SCRIPT_DIRECTORY=%~dp0 - -REM Run executable -java -cp "%SCRIPT_DIRECTORY%/lib/*" enedis.lab.io.serialport.SerialPortPlugNotifier %* \ No newline at end of file +REM Copyright (C) 2025 Enedis Smarties team +REM +REM SPDX-FileContributor: Jehan BOUSCH +REM SPDX-FileContributor: Mathieu SABARTHES +REM +REM SPDX-License-Identifier: Apache-2.0 + +@echo off + +REM Get script directory +set SCRIPT_DIRECTORY=%~dp0 + +REM Get distribution root directory (parent of script directory) +for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ + +REM Define classpath and main class +set CLASSPATH=%ROOT_DIRECTORY%lib\* +set MAIN_CLASS=tic.diagnostic.core.TICCoreApp + +REM Run executable +java -cp "%CLASSPATH%" %MAIN_CLASS% %* diff --git a/src/main/scripts/TIC2WebSocket.sh b/src/main/scripts/diagnostic/TICCore.sh similarity index 53% rename from src/main/scripts/TIC2WebSocket.sh rename to src/main/scripts/diagnostic/TICCore.sh index d69dc26..72832ba 100644 --- a/src/main/scripts/TIC2WebSocket.sh +++ b/src/main/scripts/diagnostic/TICCore.sh @@ -10,5 +10,12 @@ # Get script directory SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) +# Get distribution root directory (parent of script directory) +ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") + +# Define classpath and main class +CLASSPATH="$ROOT_DIRECTORY/lib/*" +MAIN_CLASS=tic.diagnostic.core.TICCoreApp + # Run executable -java -DlogDir="$SCRIPT_DIRECTORY/var/log" -DconfigFile="$SCRIPT_DIRECTORY/var/config/TIC2WebSocketConfiguration.json" -cp "$SCRIPT_DIRECTORY/lib/*" enedis.tic.service.TIC2WebSocketApplication $* \ No newline at end of file +java -cp "$CLASSPATH" $MAIN_CLASS $* diff --git a/src/main/scripts/TICPortDump.bat b/src/main/scripts/diagnostic/TICPortDump.bat similarity index 96% rename from src/main/scripts/TICPortDump.bat rename to src/main/scripts/diagnostic/TICPortDump.bat index e3519bd..bd424bd 100644 --- a/src/main/scripts/TICPortDump.bat +++ b/src/main/scripts/diagnostic/TICPortDump.bat @@ -1,16 +1,16 @@ -REM Copyright (C) 2025 Enedis Smarties team -REM -REM SPDX-FileContributor: Jehan BOUSCH -REM SPDX-FileContributor: Mathieu SABARTHES -REM -REM SPDX-License-Identifier: Apache-2.0 - -@echo off - -REM Get script directory -set SCRIPT_DIRECTORY=%~dp0 -REM Get script name -set SCRIPT_NAME=%~n0 - -REM Run powershell script +REM Copyright (C) 2025 Enedis Smarties team +REM +REM SPDX-FileContributor: Jehan BOUSCH +REM SPDX-FileContributor: Mathieu SABARTHES +REM +REM SPDX-License-Identifier: Apache-2.0 + +@echo off + +REM Get script directory +set SCRIPT_DIRECTORY=%~dp0 +REM Get script name +set SCRIPT_NAME=%~n0 + +REM Run powershell script powershell -File %SCRIPT_DIRECTORY%/%SCRIPT_NAME%.ps1 %* \ No newline at end of file diff --git a/src/main/scripts/TICPortDump.ps1 b/src/main/scripts/diagnostic/TICPortDump.ps1 similarity index 95% rename from src/main/scripts/TICPortDump.ps1 rename to src/main/scripts/diagnostic/TICPortDump.ps1 index 6b6eb87..5a330c2 100644 --- a/src/main/scripts/TICPortDump.ps1 +++ b/src/main/scripts/diagnostic/TICPortDump.ps1 @@ -1,42 +1,42 @@ -# Copyright (C) 2025 Enedis Smarties team -# -# SPDX-FileContributor: Jehan BOUSCH -# SPDX-FileContributor: Mathieu SABARTHES -# -# SPDX-License-Identifier: Apache-2.0 - -if($args.count -lt 1) -{ - Write-Error 'Usage: ' + $MyInvocation.MyCommand.Name + ' portName [ticMode]' -ErrorAction Stop -} -$portName = $args[0] -if($args.count -lt 2) -{ - $ticMode = "STANDARD" -} -else -{ - $ticMode = $args[1] -} - -if($ticMode -eq "STANDARD") -{ - $baudrate = 9600 -} -elseif($ticMode -eq "HISTORIC") -{ - $baudrate = 1200 -} -else -{ - Write-Error 'The tic mode "' + $ticMode + '" is unknown (only "STANDARD" or "HISTORIC" is allowed)!' -ErrorAction Stop -} - -$port = new-Object System.IO.Ports.SerialPort $portName,$baudrate,Even,7,one -$port.open() -while($port.IsOpen) -{ - $line = $port.ReadLine() - Write-Output $line -} +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +if($args.count -lt 1) +{ + Write-Error 'Usage: ' + $MyInvocation.MyCommand.Name + ' portName [ticMode]' -ErrorAction Stop +} +$portName = $args[0] +if($args.count -lt 2) +{ + $ticMode = "STANDARD" +} +else +{ + $ticMode = $args[1] +} + +if($ticMode -eq "STANDARD") +{ + $baudrate = 9600 +} +elseif($ticMode -eq "HISTORIC") +{ + $baudrate = 1200 +} +else +{ + Write-Error 'The tic mode "' + $ticMode + '" is unknown (only "STANDARD" or "HISTORIC" is allowed)!' -ErrorAction Stop +} + +$port = new-Object System.IO.Ports.SerialPort $portName,$baudrate,Even,7,one +$port.open() +while($port.IsOpen) +{ + $line = $port.ReadLine() + Write-Output $line +} $port.close() \ No newline at end of file diff --git a/src/main/scripts/TICPortDump.sh b/src/main/scripts/diagnostic/TICPortDump.sh similarity index 100% rename from src/main/scripts/TICPortDump.sh rename to src/main/scripts/diagnostic/TICPortDump.sh diff --git a/src/main/scripts/TIC2WebSocket.bat b/src/main/scripts/diagnostic/UsbPortFinder.bat similarity index 50% rename from src/main/scripts/TIC2WebSocket.bat rename to src/main/scripts/diagnostic/UsbPortFinder.bat index f5316e0..5c52e3a 100644 --- a/src/main/scripts/TIC2WebSocket.bat +++ b/src/main/scripts/diagnostic/UsbPortFinder.bat @@ -10,5 +10,12 @@ REM SPDX-License-Identifier: Apache-2.0 REM Get script directory set SCRIPT_DIRECTORY=%~dp0 +REM Get distribution root directory (parent of script directory) +for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ + +REM Define classpath and main class +set CLASSPATH=%ROOT_DIRECTORY%lib\* +set MAIN_CLASS=tic.diagnostic.usb.UsbPortFinderApp + REM Run executable -java -DlogDir="%SCRIPT_DIRECTORY%var\log" -DconfigFile="%SCRIPT_DIRECTORY%var\config\TIC2WebSocketConfiguration.json" -cp "%SCRIPT_DIRECTORY%/lib/*" enedis.tic.service.TIC2WebSocketApplication %* \ No newline at end of file +java -cp "%CLASSPATH%" %MAIN_CLASS% %* diff --git a/src/main/scripts/diagnostic/UsbPortFinder.sh b/src/main/scripts/diagnostic/UsbPortFinder.sh new file mode 100644 index 0000000..6e4c983 --- /dev/null +++ b/src/main/scripts/diagnostic/UsbPortFinder.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +# Get script directory +SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) + +# Get distribution root directory (parent of script directory) +ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") + +# Define classpath and main class +CLASSPATH="$ROOT_DIRECTORY/lib/*" +MAIN_CLASS=tic.diagnostic.usb.UsbPortFinderApp + +# Run executable +java -cp "$CLASSPATH" $MAIN_CLASS $* diff --git a/src/main/scripts/launcher/TIC2WebSocket.bat b/src/main/scripts/launcher/TIC2WebSocket.bat new file mode 100644 index 0000000..edd3854 --- /dev/null +++ b/src/main/scripts/launcher/TIC2WebSocket.bat @@ -0,0 +1,23 @@ +REM Copyright (C) 2025 Enedis Smarties team +REM +REM SPDX-FileContributor: Jehan BOUSCH +REM SPDX-FileContributor: Mathieu SABARTHES +REM +REM SPDX-License-Identifier: Apache-2.0 + +@echo off + +REM Get script directory +set SCRIPT_DIRECTORY=%~dp0 + +REM Get distribution root directory (parent of script directory) +for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ + +REM Define log directory and config file +set LOG_DIRECTORY=%ROOT_DIRECTORY%var\log +set CONFIG_FILE=%ROOT_DIRECTORY%var\config\TIC2WebSocketConfiguration.json +set CLASSPATH=%ROOT_DIRECTORY%lib\* +set MAIN_CLASS=tic.service.TIC2WebSocketApplication + +REM Run executable +java -DlogDir="%LOG_DIRECTORY%" -DconfigFile="%CONFIG_FILE%" -cp "%CLASSPATH%" %MAIN_CLASS% %* \ No newline at end of file diff --git a/src/main/scripts/launcher/TIC2WebSocket.sh b/src/main/scripts/launcher/TIC2WebSocket.sh new file mode 100644 index 0000000..34c3216 --- /dev/null +++ b/src/main/scripts/launcher/TIC2WebSocket.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Copyright (C) 2025 Enedis Smarties team +# +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + +# Get script directory +SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) + +# Get distribution root directory (parent of script directory) +ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") + +# Define log directory and config file +LOG_DIRECTORY="$ROOT_DIRECTORY/var/log" +CONFIG_FILE="$ROOT_DIRECTORY/var/config/TIC2WebSocketConfiguration.json" +CLASSPATH="$ROOT_DIRECTORY/lib/*" +MAIN_CLASS=tic.service.TIC2WebSocketApplication + +# Run executable +java -DlogDir="$LOG_DIRECTORY" -DconfigFile="$CONFIG_FILE" -cp "$CLASSPATH" $MAIN_CLASS $* \ No newline at end of file From 64689adc3b9322e73d9a5639367d70fd46f72439 Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Thu, 5 Feb 2026 19:06:13 +0100 Subject: [PATCH 43/59] feat: Add tester to output package --- src/assembly/all.xml | 6 ++++++ src/assembly/bin.xml | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/assembly/all.xml b/src/assembly/all.xml index af81abf..c315035 100644 --- a/src/assembly/all.xml +++ b/src/assembly/all.xml @@ -126,10 +126,16 @@ SPDX-License-Identifier: Apache-2.0 **/*.log **/.settings/** + docs/** **/var/** **/${project.build.directory}/** + + ${basedir}/docs + tester + true + ${project.build.directory}/site doc diff --git a/src/assembly/bin.xml b/src/assembly/bin.xml index 2a355ec..6324def 100644 --- a/src/assembly/bin.xml +++ b/src/assembly/bin.xml @@ -118,6 +118,11 @@ SPDX-License-Identifier: Apache-2.0 var/config true + + ${basedir}/docs + tester + true + ${basedir}/LICENSES From 3584093d2872d44e6232dd309464e82c11c408dd Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Mon, 9 Feb 2026 11:30:48 +0100 Subject: [PATCH 44/59] fix: Update output directories and classpath in scripts --- src/assembly/all.xml | 13 ++++++++++--- src/assembly/bin.xml | 7 ++++--- .../scripts/debug/TIC2WebSocket_remote_debug.sh | 9 ++++++--- src/main/scripts/diagnostic/ModemFinder.bat | 5 ++++- src/main/scripts/diagnostic/ModemFinder.sh | 5 ++++- src/main/scripts/diagnostic/ModemPlugNotifier.bat | 5 ++++- src/main/scripts/diagnostic/ModemPlugNotifier.sh | 5 ++++- src/main/scripts/diagnostic/SerialPortFinder.bat | 5 ++++- src/main/scripts/diagnostic/SerialPortFinder.sh | 5 ++++- src/main/scripts/diagnostic/TICCore.bat | 5 ++++- src/main/scripts/diagnostic/TICCore.sh | 5 ++++- src/main/scripts/diagnostic/UsbPortFinder.bat | 5 ++++- src/main/scripts/diagnostic/UsbPortFinder.sh | 5 ++++- src/main/scripts/launcher/TIC2WebSocket.bat | 9 ++++++--- src/main/scripts/launcher/TIC2WebSocket.sh | 9 ++++++--- 15 files changed, 72 insertions(+), 25 deletions(-) diff --git a/src/assembly/all.xml b/src/assembly/all.xml index c315035..97bb4c8 100644 --- a/src/assembly/all.xml +++ b/src/assembly/all.xml @@ -20,7 +20,7 @@ SPDX-License-Identifier: Apache-2.0 true - lib + app/lib false @@ -28,7 +28,7 @@ SPDX-License-Identifier: Apache-2.0 CHANGELOG.md - CHANGELOG.md + app/CHANGELOG.md LICENCE.txt @@ -117,7 +117,12 @@ SPDX-License-Identifier: Apache-2.0 ${basedir}/src/main/resources/config - var/config + app/var/config + true + + + ${basedir}/LICENSES + app/LICENSES true @@ -126,6 +131,8 @@ SPDX-License-Identifier: Apache-2.0 **/*.log **/.settings/** + CHANGELOG.md + LICENSES/** docs/** **/var/** **/${project.build.directory}/** diff --git a/src/assembly/bin.xml b/src/assembly/bin.xml index 6324def..b590484 100644 --- a/src/assembly/bin.xml +++ b/src/assembly/bin.xml @@ -22,7 +22,7 @@ SPDX-License-Identifier: Apache-2.0 true - lib + app/lib false @@ -30,7 +30,7 @@ SPDX-License-Identifier: Apache-2.0 CHANGELOG.md - CHANGELOG.md + app/CHANGELOG.md @@ -115,7 +115,7 @@ SPDX-License-Identifier: Apache-2.0 ${basedir}/src/main/resources/config - var/config + app/var/config true @@ -125,6 +125,7 @@ SPDX-License-Identifier: Apache-2.0 ${basedir}/LICENSES + app/LICENSES \ No newline at end of file diff --git a/src/main/scripts/debug/TIC2WebSocket_remote_debug.sh b/src/main/scripts/debug/TIC2WebSocket_remote_debug.sh index 606ca45..69643b1 100644 --- a/src/main/scripts/debug/TIC2WebSocket_remote_debug.sh +++ b/src/main/scripts/debug/TIC2WebSocket_remote_debug.sh @@ -13,10 +13,13 @@ SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) # Get distribution root directory (parent of script directory) ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") +# Define app directory +APP_DIRECTORY="$ROOT_DIRECTORY/app" + # Define log directory and config file -LOG_DIRECTORY="$ROOT_DIRECTORY/var/log" -CONFIG_FILE="$ROOT_DIRECTORY/var/config/TIC2WebSocketConfiguration.json" -CLASSPATH="$ROOT_DIRECTORY/lib/*" +LOG_DIRECTORY="$APP_DIRECTORY/var/log" +CONFIG_FILE="$APP_DIRECTORY/var/config/TIC2WebSocketConfiguration.json" +CLASSPATH="$APP_DIRECTORY/lib/*" MAIN_CLASS=tic.service.TIC2WebSocketApplication # Run executable diff --git a/src/main/scripts/diagnostic/ModemFinder.bat b/src/main/scripts/diagnostic/ModemFinder.bat index f9b5f7f..4efa621 100644 --- a/src/main/scripts/diagnostic/ModemFinder.bat +++ b/src/main/scripts/diagnostic/ModemFinder.bat @@ -13,8 +13,11 @@ set SCRIPT_DIRECTORY=%~dp0 REM Get distribution root directory (parent of script directory) for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ +REM Define app directory +set APP_DIRECTORY=%ROOT_DIRECTORY%app\ + REM Define classpath and main class -set CLASSPATH=%ROOT_DIRECTORY%lib\* +set CLASSPATH=%APP_DIRECTORY%lib\* set MAIN_CLASS=tic.diagnostic.modem.ModemFinderApp REM Run executable diff --git a/src/main/scripts/diagnostic/ModemFinder.sh b/src/main/scripts/diagnostic/ModemFinder.sh index fcdd897..010b075 100644 --- a/src/main/scripts/diagnostic/ModemFinder.sh +++ b/src/main/scripts/diagnostic/ModemFinder.sh @@ -13,8 +13,11 @@ SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) # Get distribution root directory (parent of script directory) ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") +# Define app directory +APP_DIRECTORY="$ROOT_DIRECTORY/app" + # Define classpath and main class -CLASSPATH="$ROOT_DIRECTORY/lib/*" +CLASSPATH="$APP_DIRECTORY/lib/*" MAIN_CLASS=tic.diagnostic.modem.ModemFinderApp # Run executable diff --git a/src/main/scripts/diagnostic/ModemPlugNotifier.bat b/src/main/scripts/diagnostic/ModemPlugNotifier.bat index a5bf39b..b1c95d6 100644 --- a/src/main/scripts/diagnostic/ModemPlugNotifier.bat +++ b/src/main/scripts/diagnostic/ModemPlugNotifier.bat @@ -13,8 +13,11 @@ set SCRIPT_DIRECTORY=%~dp0 REM Get distribution root directory (parent of script directory) for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ +REM Define app directory +set APP_DIRECTORY=%ROOT_DIRECTORY%app\ + REM Define classpath and main class -set CLASSPATH=%ROOT_DIRECTORY%lib\* +set CLASSPATH=%APP_DIRECTORY%lib\* set MAIN_CLASS=tic.diagnostic.modem.ModemPlugNotifierApp REM Run executable diff --git a/src/main/scripts/diagnostic/ModemPlugNotifier.sh b/src/main/scripts/diagnostic/ModemPlugNotifier.sh index 458dc50..e6cc87f 100644 --- a/src/main/scripts/diagnostic/ModemPlugNotifier.sh +++ b/src/main/scripts/diagnostic/ModemPlugNotifier.sh @@ -13,8 +13,11 @@ SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) # Get distribution root directory (parent of script directory) ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") +# Define app directory +APP_DIRECTORY="$ROOT_DIRECTORY/app" + # Define classpath and main class -CLASSPATH="$ROOT_DIRECTORY/lib/*" +CLASSPATH="$APP_DIRECTORY/lib/*" MAIN_CLASS=tic.diagnostic.modem.ModemPlugNotifierApp # Run executable diff --git a/src/main/scripts/diagnostic/SerialPortFinder.bat b/src/main/scripts/diagnostic/SerialPortFinder.bat index 910c085..c22930e 100644 --- a/src/main/scripts/diagnostic/SerialPortFinder.bat +++ b/src/main/scripts/diagnostic/SerialPortFinder.bat @@ -13,8 +13,11 @@ set SCRIPT_DIRECTORY=%~dp0 REM Get distribution root directory (parent of script directory) for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ +REM Define app directory +set APP_DIRECTORY=%ROOT_DIRECTORY%app\ + REM Define classpath and main class -set CLASSPATH=%ROOT_DIRECTORY%lib\* +set CLASSPATH=%APP_DIRECTORY%lib\* set MAIN_CLASS=tic.diagnostic.serialport.SerialPortFinderApp REM Run executable diff --git a/src/main/scripts/diagnostic/SerialPortFinder.sh b/src/main/scripts/diagnostic/SerialPortFinder.sh index 7de8ce3..e8b7bd6 100644 --- a/src/main/scripts/diagnostic/SerialPortFinder.sh +++ b/src/main/scripts/diagnostic/SerialPortFinder.sh @@ -13,8 +13,11 @@ SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) # Get distribution root directory (parent of script directory) ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") +# Define app directory +APP_DIRECTORY="$ROOT_DIRECTORY/app" + # Define classpath and main class -CLASSPATH="$ROOT_DIRECTORY/lib/*" +CLASSPATH="$APP_DIRECTORY/lib/*" MAIN_CLASS=tic.diagnostic.serialport.SerialPortFinderApp # Run executable diff --git a/src/main/scripts/diagnostic/TICCore.bat b/src/main/scripts/diagnostic/TICCore.bat index 7eaffee..ad270ea 100644 --- a/src/main/scripts/diagnostic/TICCore.bat +++ b/src/main/scripts/diagnostic/TICCore.bat @@ -13,8 +13,11 @@ set SCRIPT_DIRECTORY=%~dp0 REM Get distribution root directory (parent of script directory) for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ +REM Define app directory +set APP_DIRECTORY=%ROOT_DIRECTORY%app\ + REM Define classpath and main class -set CLASSPATH=%ROOT_DIRECTORY%lib\* +set CLASSPATH=%APP_DIRECTORY%lib\* set MAIN_CLASS=tic.diagnostic.core.TICCoreApp REM Run executable diff --git a/src/main/scripts/diagnostic/TICCore.sh b/src/main/scripts/diagnostic/TICCore.sh index 72832ba..abc5cad 100644 --- a/src/main/scripts/diagnostic/TICCore.sh +++ b/src/main/scripts/diagnostic/TICCore.sh @@ -13,8 +13,11 @@ SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) # Get distribution root directory (parent of script directory) ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") +# Define app directory +APP_DIRECTORY="$ROOT_DIRECTORY/app" + # Define classpath and main class -CLASSPATH="$ROOT_DIRECTORY/lib/*" +CLASSPATH="$APP_DIRECTORY/lib/*" MAIN_CLASS=tic.diagnostic.core.TICCoreApp # Run executable diff --git a/src/main/scripts/diagnostic/UsbPortFinder.bat b/src/main/scripts/diagnostic/UsbPortFinder.bat index 5c52e3a..e4f41ca 100644 --- a/src/main/scripts/diagnostic/UsbPortFinder.bat +++ b/src/main/scripts/diagnostic/UsbPortFinder.bat @@ -13,8 +13,11 @@ set SCRIPT_DIRECTORY=%~dp0 REM Get distribution root directory (parent of script directory) for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ +REM Define app directory +set APP_DIRECTORY=%ROOT_DIRECTORY%app\ + REM Define classpath and main class -set CLASSPATH=%ROOT_DIRECTORY%lib\* +set CLASSPATH=%APP_DIRECTORY%lib\* set MAIN_CLASS=tic.diagnostic.usb.UsbPortFinderApp REM Run executable diff --git a/src/main/scripts/diagnostic/UsbPortFinder.sh b/src/main/scripts/diagnostic/UsbPortFinder.sh index 6e4c983..211b899 100644 --- a/src/main/scripts/diagnostic/UsbPortFinder.sh +++ b/src/main/scripts/diagnostic/UsbPortFinder.sh @@ -13,8 +13,11 @@ SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) # Get distribution root directory (parent of script directory) ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") +# Define app directory +APP_DIRECTORY="$ROOT_DIRECTORY/app" + # Define classpath and main class -CLASSPATH="$ROOT_DIRECTORY/lib/*" +CLASSPATH="$APP_DIRECTORY/lib/*" MAIN_CLASS=tic.diagnostic.usb.UsbPortFinderApp # Run executable diff --git a/src/main/scripts/launcher/TIC2WebSocket.bat b/src/main/scripts/launcher/TIC2WebSocket.bat index edd3854..71a161f 100644 --- a/src/main/scripts/launcher/TIC2WebSocket.bat +++ b/src/main/scripts/launcher/TIC2WebSocket.bat @@ -13,10 +13,13 @@ set SCRIPT_DIRECTORY=%~dp0 REM Get distribution root directory (parent of script directory) for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ +REM Define app directory +set APP_DIRECTORY=%ROOT_DIRECTORY%app\ + REM Define log directory and config file -set LOG_DIRECTORY=%ROOT_DIRECTORY%var\log -set CONFIG_FILE=%ROOT_DIRECTORY%var\config\TIC2WebSocketConfiguration.json -set CLASSPATH=%ROOT_DIRECTORY%lib\* +set LOG_DIRECTORY=%APP_DIRECTORY%var\log +set CONFIG_FILE=%APP_DIRECTORY%var\config\TIC2WebSocketConfiguration.json +set CLASSPATH=%APP_DIRECTORY%lib\* set MAIN_CLASS=tic.service.TIC2WebSocketApplication REM Run executable diff --git a/src/main/scripts/launcher/TIC2WebSocket.sh b/src/main/scripts/launcher/TIC2WebSocket.sh index 34c3216..502d9ca 100644 --- a/src/main/scripts/launcher/TIC2WebSocket.sh +++ b/src/main/scripts/launcher/TIC2WebSocket.sh @@ -13,10 +13,13 @@ SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) # Get distribution root directory (parent of script directory) ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") +# Define app directory +APP_DIRECTORY="$ROOT_DIRECTORY/app" + # Define log directory and config file -LOG_DIRECTORY="$ROOT_DIRECTORY/var/log" -CONFIG_FILE="$ROOT_DIRECTORY/var/config/TIC2WebSocketConfiguration.json" -CLASSPATH="$ROOT_DIRECTORY/lib/*" +LOG_DIRECTORY="$APP_DIRECTORY/var/log" +CONFIG_FILE="$APP_DIRECTORY/var/config/TIC2WebSocketConfiguration.json" +CLASSPATH="$APP_DIRECTORY/lib/*" MAIN_CLASS=tic.service.TIC2WebSocketApplication # Run executable From db1c20a06073690b84b4295ad2588d00697eea7d Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Mon, 9 Feb 2026 16:12:10 +0100 Subject: [PATCH 45/59] fix: Update launcher script paths to use app directory --- src/assembly/all.xml | 4 ++-- src/assembly/bin.xml | 4 ++-- src/main/scripts/launcher/TIC2WebSocket.bat | 7 ++----- src/main/scripts/launcher/TIC2WebSocket.sh | 7 ++----- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/assembly/all.xml b/src/assembly/all.xml index 97bb4c8..86768ec 100644 --- a/src/assembly/all.xml +++ b/src/assembly/all.xml @@ -38,11 +38,11 @@ SPDX-License-Identifier: Apache-2.0 ${project.build.scriptSourceDirectory}/launcher/${project.name}.bat - launcher/${project.name}.bat + app/${project.name}.bat ${project.build.scriptSourceDirectory}/launcher/${project.name}.sh - launcher/${project.name}.sh + app/${project.name}.sh 0755 diff --git a/src/assembly/bin.xml b/src/assembly/bin.xml index b590484..42b17b9 100644 --- a/src/assembly/bin.xml +++ b/src/assembly/bin.xml @@ -36,11 +36,11 @@ SPDX-License-Identifier: Apache-2.0 ${project.build.scriptSourceDirectory}/launcher/${project.name}.bat - launcher/${project.name}.bat + app/${project.name}.bat ${project.build.scriptSourceDirectory}/launcher/${project.name}.sh - launcher/${project.name}.sh + app/${project.name}.sh 0755 diff --git a/src/main/scripts/launcher/TIC2WebSocket.bat b/src/main/scripts/launcher/TIC2WebSocket.bat index 71a161f..0dae77c 100644 --- a/src/main/scripts/launcher/TIC2WebSocket.bat +++ b/src/main/scripts/launcher/TIC2WebSocket.bat @@ -10,11 +10,8 @@ REM SPDX-License-Identifier: Apache-2.0 REM Get script directory set SCRIPT_DIRECTORY=%~dp0 -REM Get distribution root directory (parent of script directory) -for %%I in ("%SCRIPT_DIRECTORY%..") do set ROOT_DIRECTORY=%%~fI\ - -REM Define app directory -set APP_DIRECTORY=%ROOT_DIRECTORY%app\ +REM App directory is the script directory +set APP_DIRECTORY=%SCRIPT_DIRECTORY% REM Define log directory and config file set LOG_DIRECTORY=%APP_DIRECTORY%var\log diff --git a/src/main/scripts/launcher/TIC2WebSocket.sh b/src/main/scripts/launcher/TIC2WebSocket.sh index 502d9ca..187665e 100644 --- a/src/main/scripts/launcher/TIC2WebSocket.sh +++ b/src/main/scripts/launcher/TIC2WebSocket.sh @@ -10,11 +10,8 @@ # Get script directory SCRIPT_DIRECTORY=$(dirname $(realpath "$0")) -# Get distribution root directory (parent of script directory) -ROOT_DIRECTORY=$(realpath "$SCRIPT_DIRECTORY/..") - -# Define app directory -APP_DIRECTORY="$ROOT_DIRECTORY/app" +# App directory is the script directory +APP_DIRECTORY="$SCRIPT_DIRECTORY" # Define log directory and config file LOG_DIRECTORY="$APP_DIRECTORY/var/log" From db98e8506f332a477cbb470a8956bb0bb40704dc Mon Sep 17 00:00:00 2001 From: Matthieu SABARTHES Date: Thu, 12 Feb 2026 10:42:44 +0100 Subject: [PATCH 46/59] chore: Add documentation files and Docker setup for MkDocs and PlantUML --- docs/Dockerfile | 4 ++++ docs/README.en.md | 10 ++++++++++ docs/README.md | 10 ++++++++++ docs/docker-compose.yml | 16 ++++++++++++++++ docs/docs/en/index.md | 1 + docs/docs/fr/index.md | 1 + docs/mkdocs.yml | 23 +++++++++++++++++++++++ docs/requirements.txt | 2 ++ 8 files changed, 67 insertions(+) create mode 100644 docs/Dockerfile create mode 100644 docs/README.en.md create mode 100644 docs/README.md create mode 100644 docs/docker-compose.yml create mode 100644 docs/docs/en/index.md create mode 100644 docs/docs/fr/index.md create mode 100644 docs/mkdocs.yml create mode 100644 docs/requirements.txt diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 0000000..54fc120 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,4 @@ +FROM squidfunk/mkdocs-material + +COPY requirements.txt . +RUN pip install -r requirements.txt diff --git a/docs/README.en.md b/docs/README.en.md new file mode 100644 index 0000000..4a5eb5d --- /dev/null +++ b/docs/README.en.md @@ -0,0 +1,10 @@ +[🇫🇷 Français](README.md) | [🇺🇸 English](README.en.md) + +# Developer setup + +From the project `docs` folder: + +- Start (PlantUML server + MkDocs server): `docker-compose up --build` +- Stop: `docker-compose down` + +Then visit: http://localhost:8000/ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e99abb4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,10 @@ +[🇫🇷 Français](README.md) | [🇺🇸 English](README.en.md) + +# Configuration pour les développeurs + +Depuis le dossier `docs` à la racine du projet : + +- Démarrer (serveur PlantUML + serveur MkDocs) : `docker-compose up --build` +- Arrêter : `docker-compose down` + +Puis visiter : http://localhost:8000/ diff --git a/docs/docker-compose.yml b/docs/docker-compose.yml new file mode 100644 index 0000000..b04193a --- /dev/null +++ b/docs/docker-compose.yml @@ -0,0 +1,16 @@ +services: + plantuml: + image: plantuml/plantuml-server:tomcat + ports: + - "8080:8080" + + mkdocs: + build: . + working_dir: /docs + volumes: + - ./:/docs + ports: + - "8000:8000" + command: mkdocs serve --config-file mkdocs.yml --dev-addr 0.0.0.0:8000 + depends_on: + - plantuml diff --git a/docs/docs/en/index.md b/docs/docs/en/index.md new file mode 100644 index 0000000..27b8a5c --- /dev/null +++ b/docs/docs/en/index.md @@ -0,0 +1 @@ +# TIC2WebSocket diff --git a/docs/docs/fr/index.md b/docs/docs/fr/index.md new file mode 100644 index 0000000..27b8a5c --- /dev/null +++ b/docs/docs/fr/index.md @@ -0,0 +1 @@ +# TIC2WebSocket diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 0000000..33c41d9 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,23 @@ +site_name: TIC2WebSocket +site_url: "" + +docs_dir: docs +site_dir: site + +theme: + name: material + +nav: + - Français: fr/index.md + - English: en/index.md + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - plantuml_markdown: + server: "http://plantuml:8080/plantuml" + format: svg + +plugins: + - search diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..4481a40 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +# Extra MkDocs extensions beyond squidfunk/mkdocs-material +plantuml-markdown From b1fb7415c942a1e920d410ad72694d0a51102826 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:50:36 +0000 Subject: [PATCH 47/59] docs: Add documentation --- docs/README.en.md | 2 +- docs/README.md | 4 +- docs/docker-compose.yml | 2 +- docs/docs/en/development/architecture.md | 144 ++++++++ docs/docs/en/development/contributing.md | 21 ++ docs/docs/en/development/index.md | 20 ++ docs/docs/en/getting-started/index.md | 10 + docs/docs/en/getting-started/installation.md | 47 +++ docs/docs/en/getting-started/launch.md | 95 +++++ docs/docs/en/getting-started/prerequisites.md | 12 + docs/docs/en/getting-started/ws-tester.md | 77 ++++ docs/docs/en/index.md | 22 ++ docs/docs/en/usage/api.md | 331 ++++++++++++++++++ docs/docs/en/usage/configuration.md | 56 +++ docs/docs/en/usage/index.md | 23 ++ docs/docs/fr/development/architecture.md | 139 ++++++++ docs/docs/fr/development/contributing.md | 18 + docs/docs/fr/development/index.md | 18 + docs/docs/fr/getting-started/index.md | 9 + docs/docs/fr/getting-started/installation.md | 45 +++ docs/docs/fr/getting-started/launch.md | 95 +++++ docs/docs/fr/getting-started/prerequisites.md | 12 + docs/docs/fr/getting-started/ws-tester.md | 77 ++++ docs/docs/fr/index.md | 20 ++ docs/docs/fr/usage/api.md | 331 ++++++++++++++++++ docs/docs/fr/usage/configuration.md | 56 +++ docs/docs/fr/usage/index.md | 23 ++ docs/docs/stylesheets/puml-fix.css | 25 ++ docs/mkdocs.yml | 102 +++++- docs/requirements.txt | 2 + 30 files changed, 1823 insertions(+), 15 deletions(-) create mode 100644 docs/docs/en/development/architecture.md create mode 100644 docs/docs/en/development/contributing.md create mode 100644 docs/docs/en/development/index.md create mode 100644 docs/docs/en/getting-started/index.md create mode 100644 docs/docs/en/getting-started/installation.md create mode 100644 docs/docs/en/getting-started/launch.md create mode 100644 docs/docs/en/getting-started/prerequisites.md create mode 100644 docs/docs/en/getting-started/ws-tester.md create mode 100644 docs/docs/en/usage/api.md create mode 100644 docs/docs/en/usage/configuration.md create mode 100644 docs/docs/en/usage/index.md create mode 100644 docs/docs/fr/development/architecture.md create mode 100644 docs/docs/fr/development/contributing.md create mode 100644 docs/docs/fr/development/index.md create mode 100644 docs/docs/fr/getting-started/index.md create mode 100644 docs/docs/fr/getting-started/installation.md create mode 100644 docs/docs/fr/getting-started/launch.md create mode 100644 docs/docs/fr/getting-started/prerequisites.md create mode 100644 docs/docs/fr/getting-started/ws-tester.md create mode 100644 docs/docs/fr/usage/api.md create mode 100644 docs/docs/fr/usage/configuration.md create mode 100644 docs/docs/fr/usage/index.md create mode 100644 docs/docs/stylesheets/puml-fix.css diff --git a/docs/README.en.md b/docs/README.en.md index 4a5eb5d..db4200c 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -7,4 +7,4 @@ From the project `docs` folder: - Start (PlantUML server + MkDocs server): `docker-compose up --build` - Stop: `docker-compose down` -Then visit: http://localhost:8000/ +Then visit: http://localhost:8000/tic2websocket/ (the `/` root redirects to this path) diff --git a/docs/README.md b/docs/README.md index e99abb4..052e037 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ Depuis le dossier `docs` à la racine du projet : -- Démarrer (serveur PlantUML + serveur MkDocs) : `docker-compose up --build` +- Démarrer (serveur PlantUML + serveur MkDocs) : `docker-compose up --build` (ou `docker compose up --build`) - Arrêter : `docker-compose down` -Puis visiter : http://localhost:8000/ +Puis visiter : http://localhost:8000/tic2websocket/ (la racine `/` redirige vers ce chemin) diff --git a/docs/docker-compose.yml b/docs/docker-compose.yml index b04193a..30f3675 100644 --- a/docs/docker-compose.yml +++ b/docs/docker-compose.yml @@ -11,6 +11,6 @@ services: - ./:/docs ports: - "8000:8000" - command: mkdocs serve --config-file mkdocs.yml --dev-addr 0.0.0.0:8000 + command: serve --config-file mkdocs.yml --dev-addr 0.0.0.0:8000 depends_on: - plantuml diff --git a/docs/docs/en/development/architecture.md b/docs/docs/en/development/architecture.md new file mode 100644 index 0000000..9a76d35 --- /dev/null +++ b/docs/docs/en/development/architecture.md @@ -0,0 +1,144 @@ +# Architecture + +This page describes the technical architecture of **TIC2WebSocket**: its components, execution flows, responsibilities, and extension points. + +## System goal + +`TIC2WebSocket` exposes a JSON WebSocket interface to: + +- discover available TICs; +- read a TIC frame on demand; +- subscribe to one or more TIC streams; +- receive real-time events (`OnTICData`, `OnError`). + +The service bridges the **hardware/serial port** world (TIC modems) and **WebSocket** application clients. + +## Context view + +```plantuml +@startuml +title TIC2WebSocket - Context view +skinparam shadowing false +left to right direction +skinparam rectangle { + RoundCorner 12 +} + +actor "Business client\n(WebSocket)" as Client +actor "Host OS" as OS +actor "Meter / TIC modem" as Meter + +rectangle "TIC2WebSocket" as App { + component "JSON WebSocket API" as WS + component "TICCore" as Core +} + +Client --> WS : Requests +WS --> Client : Responses + events + +Core --> OS : Port discovery\n(USB, serial) +Core --> Meter : Read TIC frames +WS <--> Core +@enduml +``` + +## Main components + +The application is organized into several Java packages, each with clear responsibilities. + +### Diagnostic package + +- **TICCoreApp**: application for global TIC core tests and diagnostics +- **ModemFinderApp**: modem discovery test application +- **ModemPlugNotifierApp**: plug/unplug notification test application +- **SerialPortFinderApp**: serial port discovery test application +- **UsbPortFinderApp**: USB port discovery test application + +### Utility package + +- **Codec**: JSON encoding/decoding, TIC frames, etc. +- **Message**: generic message models (Request, Response, Event) +- **Task**: asynchronous task management +- **Time**: date and time utilities + +### io package + +- **Modem**: TIC modem management (serial port, USB) +- **ModemFinder**: dynamic discovery of connected modems +- **PlugNotifier**: plug/unplug event notifications + +### frame package + +- **TICFrame**: representation of a decoded TIC frame +- **TICFrameCodec**: TIC frame encoding/decoding + +### stream package + +- **TICCoreStream**: TIC data stream management (reading, decoding) +- **TICStreamReader**: reads a TIC stream from a modem +- **TICStreamModeDetector**: detects the TIC mode (historic, standard) + +### core package + +- **TICCoreBase**: application core, subscription management, notifications + +### service package + +- **TIC2WebSocketApplication**: TIC2WebSocket application entry point +- **TIC2WebSocketServer**: WebSocket server (Netty) +- **TIC2WebSocketHandler**: main WebSocket request handler +- **TIC2WebSocketRequestHandlerBase**: base class for business request handlers +- **TIC2WebSocketClientPool**: connected WebSocket clients management + +## Execution flows + +### Startup + +1. The CLI is parsed (`picocli`) and configuration is loaded. +2. `TICCoreBase` is instantiated with the TIC mode and optional ports. +3. The Netty WebSocket server starts. +4. The core enables modem plug/unplug monitoring. + +### Handling a WebSocket request + +1. The handler receives a WebSocket text frame. +2. JSON is decoded into a `Message` then transformed into a `Request`. +3. `TIC2WebSocketRequestHandlerBase` performs the business operation. +4. The JSON response is sent back to the client. + +### Broadcasting TIC events + +1. A stream reads and decodes a new TIC frame. +2. `TICCoreBase` receives `onData` / `onError`. +3. Subscribers filtered by `TICIdentifier` are resolved. +4. Each client receives a WebSocket event. + +## Sequence: subscribe then events + +```plantuml +@startuml +title SubscribeTIC + OnTICData broadcast +skinparam shadowing false + +actor "WS client" as C +participant "TIC2WebSocketHandler" as H +participant "RequestHandlerBase" as RH +participant "TICCoreBase" as Core +participant "TICCoreStream" as Stream +participant "TIC2WebSocketClient" as WSC + +C -> H : REQUEST SubscribeTIC(identifier) +H -> RH : handle(request, client) +RH -> Core : subscribe(identifier, client) +Core --> RH : ok +RH --> H : RESPONSE SubscribeTIC(errorCode=0) +H --> C : RESPONSE SubscribeTIC + +loop for each frame + Stream -> Core : onData(frame) + Core -> WSC : onData(frame) + WSC -> H : sendEvent(OnTICData) + H --> C : EVENT OnTICData +end +@enduml +``` diff --git a/docs/docs/en/development/contributing.md b/docs/docs/en/development/contributing.md new file mode 100644 index 0000000..9cfde40 --- /dev/null +++ b/docs/docs/en/development/contributing.md @@ -0,0 +1,21 @@ +# Contributing + +Thanks for your interest in contributing to the TIC2WebSocket project. Contributions are welcome! + +## REUSE compliance + +This project is compliant with [REUSE](https://reuse.software/), meaning all source and documentation files include clear information about licensing and copyright. This makes reuse, distribution, and legal compliance easier. + +For more information about REUSE compliance and best practices, please visit the official website: [REUSE.software](https://reuse.software/). + +## Commit and branch conventions + +We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages. This improves the readability of Git history and helps automate versioning and changelogs. Please follow this convention when contributing. + +We also apply [Conventional Branches](https://www.conventionalcommits.org/en/v1.0.0/#conventional-branches) for branch naming. Please refer to the official documentation to name your branches appropriately before submitting a pull request. + +## License + +This project is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). + +You are free to use, modify, and distribute this software under the terms of that license. diff --git a/docs/docs/en/development/index.md b/docs/docs/en/development/index.md new file mode 100644 index 0000000..04060a3 --- /dev/null +++ b/docs/docs/en/development/index.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 1 +title: Development +--- + +# Development + +This section documents how to work on **TIC2WebSocket** locally, run checks, and contribute safely. + +## Architecture + +See the dedicated page for an overview of the architecture and design decisions: + +- [Architecture](architecture.md) + +## Contributing + +See the dedicated page for guidelines on contributing to the project: + +- [Contributing](contributing.md) diff --git a/docs/docs/en/getting-started/index.md b/docs/docs/en/getting-started/index.md new file mode 100644 index 0000000..c1f9197 --- /dev/null +++ b/docs/docs/en/getting-started/index.md @@ -0,0 +1,10 @@ +# Getting Started + +This section explains how to get started quickly with TIC2WebSocket. + +## Quick steps + +1. Check the required hardware and software. [Prerequisites](prerequisites.md) +2. Install TIC2WebSocket by following the installation instructions. [Installation](installation.md) +3. Start the TIC2WebSocket server. [Launch](launch.md) +4. Use the WebSocket tester to send requests and receive TIC data. [WebSocket Tester](ws-tester.md) diff --git a/docs/docs/en/getting-started/installation.md b/docs/docs/en/getting-started/installation.md new file mode 100644 index 0000000..db3ccfd --- /dev/null +++ b/docs/docs/en/getting-started/installation.md @@ -0,0 +1,47 @@ +# Installation + +## Build the project + +### Clone the repository + +```bash +git clone https://github.com/Enedis-OSS/TIC2WebSocket.git +``` + +### Run the Maven build + +```bash +cd TIC2WebSocket +mvn clean package +``` + +### Verify generated artifacts + +```bash +cd target +ls +``` + +You should see the generated deliverables, including `TIC2WebSocket-VERSION-bin.zip` or `TIC2WebSocket-VERSION-bin.tar.gz`. + +## Run the documentation locally + +### Go to the `docs/` folder + +```bash +cd docs +``` + +### Start the documentation Docker services + +```bash +docker-compose up --build +``` + +### Open the documentation + +```text +http://localhost:8000 +``` + +You should see the TIC2WebSocket documentation in your browser. diff --git a/docs/docs/en/getting-started/launch.md b/docs/docs/en/getting-started/launch.md new file mode 100644 index 0000000..c1f9f9a --- /dev/null +++ b/docs/docs/en/getting-started/launch.md @@ -0,0 +1,95 @@ +# Launch + +## Start the TIC2WebSocket server + +### Linux/macOS + +Once the TIC2WebSocket archive has been generated, extract it into a directory of your choice. + +Set the `APPLICATION_HOME` and `VERSION` environment variables to indicate the installation folder path and the TIC2WebSocket version. + +```bash +export APPLICATION_HOME=/path/to/folder +export VERSION=1.0.0 +``` + +```bash +tar -xzf TIC2WebSocket-$VERSION-bin.tar.gz -C $APPLICATION_HOME --strip-components=1 +cd $APPLICATION_HOME +``` + +Show the TIC2WebSocket launcher help with: + +```bash +./TIC2WebSocket.sh --help +``` + +You should see the help options, confirming the server is ready to be started. + +Stop the server with Ctrl+C. + +To start the TIC2WebSocket server, use: + +```bash +./TIC2WebSocket.sh --verbose=INFO --configFile=var/config/TIC2WebSocketConfiguration.json +``` + +You should see the server logs in the console, indicating it is running and ready to accept WebSocket connections. + +```text +Loading configuration file var/config/TIC2WebSocketConfiguration.json +TIC2WebSocket initialized +TIC2WebSocket starting +Starting TICCore +Starting TIC2WebSocket Netty server on localhost:19584 +TIC2WebSocket Netty server started successfully on localhost:19584 +TIC2WebSocket started +``` + +### Windows + +Once the TIC2WebSocket archive has been generated, extract it into a directory of your choice. + +Set the `APPLICATION_HOME` and `VERSION` environment variables to indicate the installation folder path and the TIC2WebSocket version. + +```powershell +$env:APPLICATION_HOME = "C:\path\to\folder" +$env:VERSION = "1.0.0" +``` + +```powershell +Unzip-File -Path TIC2WebSocket-$env:VERSION-bin.zip -DestinationPath $env:APPLICATION_HOME +cd $env:APPLICATION_HOME +``` + +Show the TIC2WebSocket launcher help with: + +```powershell +.\TIC2WebSocket.bat --help +``` + +You should see the help options, confirming the server is ready to be started. + +Stop the server with Ctrl+C. + +To start the TIC2WebSocket server, use: + +```powershell +.\TIC2WebSocket.bat --verbose=INFO --configFile=var/config/TIC2WebSocketConfiguration.json +``` + +You should see the server logs in the console, indicating it is running and ready to accept WebSocket connections. + +```text +Loading configuration file var/config/TIC2WebSocketConfiguration.json +TIC2WebSocket initialized +TIC2WebSocket starting +Starting TICCore +Starting TIC2WebSocket Netty server on localhost:19584 +TIC2WebSocket Netty server started successfully on localhost:19584 +TIC2WebSocket started +``` + +## Test the WebSocket connection + +Once the TIC2WebSocket server is started, you can test the WebSocket connection using the WebSocket client embedded in the documentation. Follow the instructions in [WebSocket Tester](ws-tester.md) to connect to the TIC2WebSocket server and receive TIC data in real time. diff --git a/docs/docs/en/getting-started/prerequisites.md b/docs/docs/en/getting-started/prerequisites.md new file mode 100644 index 0000000..a6c5df5 --- /dev/null +++ b/docs/docs/en/getting-started/prerequisites.md @@ -0,0 +1,12 @@ +# Prerequisites + +## Environment + +- Java 8 +- Maven +- Docker / Docker Compose (optional, for local documentation) + +## Sources + +- Project GitHub repository +- Access to configuration files diff --git a/docs/docs/en/getting-started/ws-tester.md b/docs/docs/en/getting-started/ws-tester.md new file mode 100644 index 0000000..c97971c --- /dev/null +++ b/docs/docs/en/getting-started/ws-tester.md @@ -0,0 +1,77 @@ +# WebSocket Tester + +The project includes a ready-to-use WebSocket client interface: `ws-tester.html`. + +This tester helps you quickly validate a TIC2WebSocket instance without having to develop a dedicated client. + +## Role in the project + +Use the tester to: + +- verify that the WebSocket server is reachable, +- discover available TIC streams, +- generate valid JSON requests for the main API commands, +- inspect incoming responses/events in real time, +- reproduce integration issues while troubleshooting. + +## Open the tester + +Open `ws-tester.html` in your browser. + +By default, it targets `ws://localhost:19584/`. + +You can prefill the connection using query parameters: + +- full URL: `?url=ws://localhost:19584/` (aliases: `wsUrl`, `ws`) +- host/port: `?host=localhost&port=19584` (host aliases: `address`, `addr`) + +## Main usage flow + +1. Click **Connect**. +2. Choose a request preset. +3. Fill in identifiers if needed. +4. Check the generated JSON payload. +5. Click **Send** and watch the logs. +6. Click **Disconnect** when done. + +## Request presets and identifiers + +Available presets: + +- `GetModemsInfo` +- `GetAvailableTICs` +- `ReadTIC` +- `SubscribeTIC` +- `UnsubscribeTIC` + +Identifier fields are optional except for `ReadTIC`, which requires an identifier. + +Priority rule applied by the tester: + +1. `serialNumber` +2. `portId` +3. `portName` + +When a higher-priority identifier is active, lower-priority fields are automatically disabled. + +For `SubscribeTIC` / `UnsubscribeTIC`: + +- no identifier selected -> the request applies to all streams, +- one identifier selected -> the request targets that stream. + +When receiving a `GetAvailableTICs` response, the tester automatically fills identifier fields using the first returned entry. + +## Logs and diagnostics + +- **OUT** lines: JSON sent by the client. +- **IN** lines: messages received from the server. +- **ERR** lines: client-side validation errors or socket errors. + +`EVENT` messages are grouped by event name + identifier to keep logs readable in stream mode. + +## Best practices + +- Start with `GetAvailableTICs` before `ReadTIC`/`SubscribeTIC`. +- Keep JSON indentation at `2` for easier debugging. +- Use **Clear logs** before reproducing an issue. +- Keep useful request/response pairs when reporting a problem. diff --git a/docs/docs/en/index.md b/docs/docs/en/index.md index 27b8a5c..184b541 100644 --- a/docs/docs/en/index.md +++ b/docs/docs/en/index.md @@ -1 +1,23 @@ # TIC2WebSocket + +Welcome to the documentation for the **TIC2WebSocket** project. + +## Introduction + +TIC2WebSocket is a Java application that exposes TIC ("Téléinformation Client") data through a WebSocket interface. It allows developers to access TIC data in real time, making integrations with other systems and applications easier. + +## Documentation structure + +The documentation is organized into several sections to guide you through using TIC2WebSocket: + +- [Getting Started](getting-started/index.md): Quick-start guide for TIC2WebSocket. + - [Prerequisites](getting-started/prerequisites.md): Required hardware and software. + - [Installation](getting-started/installation.md): Detailed installation instructions. + - [Launch](getting-started/launch.md): How to start the TIC2WebSocket server. + - [WebSocket Tester](getting-started/ws-tester.md): How to use the built-in WebSocket tester. +- [Usage](usage/index.md): Common usage patterns and client-side guidance. + - [API Reference](usage/api.md): WebSocket API documentation exposed by TIC2WebSocket. + - [Configuration](usage/configuration.md): Advanced configuration details. +- [Development](development/index.md): How to contribute to TIC2WebSocket. + - [Architecture](development/architecture.md): Project architecture overview. + - [Contributing](development/contributing.md): How to contribute code, report bugs, and propose improvements. diff --git a/docs/docs/en/usage/api.md b/docs/docs/en/usage/api.md new file mode 100644 index 0000000..d8fa59b --- /dev/null +++ b/docs/docs/en/usage/api.md @@ -0,0 +1,331 @@ +# API Reference + +Communication with TIC2WebSocket is based on JSON messages exchanged over WebSocket. + +## GetAvailableTICs + +This request returns the list of available TIC streams. + +### Request + +```json +{ + "type": "REQUEST", + "name": "GetAvailableTICs" +} +``` + +### Response + +The response contains a list of TIC information objects: + +- serial number; +- serial port name; +- physical USB port identifier. + +```json +{ + "type": "RESPONSE", + "name": "GetAvailableTICs", + "dateTime": "10/06/2021 14:01:00", + "errorCode": 0, + "data": [ + { + "serialNumber": "021762010203", + "portName": "/dev/ttyUSB0", + "portId": "1-1" + }, + { + "serialNumber": "041775010507", + "portName": "/dev/ttyUSB2", + "portId": "1-2" + } + ] +} +``` + +## GetModemsInfo + +This request returns modem information for connected TIC modems. + +Modem recognition is based on the USB pair `vendorId` + `productId`. + +Supported modem types: + +| Modem type | Vendor ID (hex) | Product ID (hex) | Vendor ID (dec) | Product ID (dec) | +| --- | --- | --- | --- | --- | +| `MICHAUD` | `0x0403` | `0x6001` | `1027` | `24577` | +| `TELEINFO` | `0x0403` | `0x6015` | `1027` | `24597` | + +### Request + +```json +{ + "type": "REQUEST", + "name": "GetModemsInfo" +} +``` + +### Response + +The response contains a list of modem information objects: + +- serial port name; +- modem type; +- product identifier (USB PID), if available; +- vendor identifier (USB VID), if available; +- product name, if available; +- manufacturer name, if available; +- modem serial number, if available; +- physical serial port identifier, if available. + +```json +{ + "type": "RESPONSE", + "name": "GetModemsInfo", + "dateTime": "16/06/2021 15:53:32", + "errorCode": 0, + "data": [ + { + "portName": "COM3", + "modemType": "MICHAUD", + "productId": 24597, + "vendorId": 1027, + "productName": null, + "manufacturerName": "FTDI", + "serialNumber": "DA2VYTKGA", + "portId": null + } + ] +} +``` + +## ReadTIC + +This request reads one TIC frame. + +### Request + +```json +{ + "type": "REQUEST", + "name": "ReadTIC", + "data": { + "serialNumber": "010203040506" + } +} +``` + +The `data` field must contain a TIC identifier defined in [TIC identifier format](#tic-identifier-format). + +### Response + +```json +{ + "type": "RESPONSE", + "name": "ReadTIC", + "dateTime": "10/06/2021 14:01:00", + "errorCode": 0, + "data": { + "mode": "STANDARD", + "captureDateTime": "10/06/2021 14:01:00", + "ticIdentifier": { + "serialNumber": "021762010203", + "portName": "/dev/ttyUSB0", + "portId": "1-1" + }, + "content": { + "ADSC": "010203040506", + "URMS1": 230 + } + } +} +``` + +## SubscribeTIC + +This request subscribes to one or more TIC streams. Once subscribed, the client receives events for each new TIC frame or stream-related error. + +### Request + +Several request formats are available. + +Subscribe to one TIC stream: + +```json +{ + "type": "REQUEST", + "name": "SubscribeTIC", + "data": { + "serialNumber": "010203040506" + } +} +``` + +Subscribe to multiple TIC streams: + +```json +{ + "type": "REQUEST", + "name": "SubscribeTIC", + "data": [ + { + "serialNumber": "010203040506" + }, + { + "portId": "1-1" + } + ] +} +``` + +Subscribe to all TIC streams: + +```json +{ + "type": "REQUEST", + "name": "SubscribeTIC" +} +``` + +The `data` field must contain one TIC identifier (or a list of TIC identifiers) defined in [TIC identifier format](#tic-identifier-format). + +### Response + +Success response: + +```json +{ + "type": "RESPONSE", + "name": "SubscribeTIC", + "dateTime": "10/06/2021 14:01:00", + "errorCode": 0 +} +``` + +If the requested TIC stream does not exist: + +```json +{ + "type": "RESPONSE", + "name": "SubscribeTIC", + "dateTime": "10/06/2021 14:01:00", + "errorCode": -7, + "errorMessage": "The given TIC identifier doesn't match with current TIC stream" +} +``` + +## `OnTICData` event + +This event is generated for each new TIC frame and sent to all clients subscribed to that TIC. + +```json +{ + "type": "EVENT", + "dateTime": "10/06/2021 14:01:00", + "name": "OnTICData", + "data": { + "mode": "STANDARD", + "captureDateTime": "10/06/2021 14:01:00", + "ticIdentifier": { + "serialNumber": "021762010203", + "portName": "/dev/ttyUSB0", + "portId": "1-1" + }, + "content": { + "ADSC": "010203040506", + "URMS1": 230 + } + } +} +``` + +## `OnError` event + +This event is generated when an error occurs on the TIC stream and sent to all clients subscribed to that TIC. + +```json +{ + "type": "EVENT", + "dateTime": "10/06/2021 14:01:00", + "name": "OnError", + "data": { + "errorCode": -5, + "errorMessage": "TIC frame reading timeout", + "ticIdentifier": { + "serialNumber": "021762010203", + "portName": "/dev/ttyUSB0", + "portId": "1-1" + } + } +} +``` + +## UnsubscribeTIC + +This request unsubscribes from one or more TIC streams. + +### Request + +Several request formats are available. + +Unsubscribe from one TIC stream: + +```json +{ + "type": "REQUEST", + "name": "UnsubscribeTIC", + "data": { + "serialNumber": "010203040506" + } +} +``` + +Unsubscribe from multiple TIC streams: + +```json +{ + "type": "REQUEST", + "name": "UnsubscribeTIC", + "data": [ + { + "serialNumber": "010203040506" + }, + { + "portId": "1-1" + } + ] +} +``` + +Unsubscribe from all TIC streams: + +```json +{ + "type": "REQUEST", + "name": "UnsubscribeTIC" +} +``` + +For targeted unsubscription, the `data` field must contain one TIC identifier (or a list of TIC identifiers) defined in [TIC identifier format](#tic-identifier-format). + +## TIC identifier format + +A TIC identifier is an object containing one of the following fields: + +- `serialNumber` +- `portId` +- `portName` + +Depending on the request, you can provide a single identifier or a list of identifiers. + +### Response + +```json +{ + "type": "RESPONSE", + "name": "UnsubscribeTIC", + "dateTime": "16/06/2021 15:53:32", + "errorCode": 0 +} +``` diff --git a/docs/docs/en/usage/configuration.md b/docs/docs/en/usage/configuration.md new file mode 100644 index 0000000..ab7e9a8 --- /dev/null +++ b/docs/docs/en/usage/configuration.md @@ -0,0 +1,56 @@ +# Configuration + +## Location + +The `TIC2WebSocketConfiguration.json` configuration file is located in the `var/config` subdirectory of the TIC2WebSocket installation directory. + +## Structure + +The configuration file uses JSON and allows you to change: + +- the listening port of the WebSocket server, +- the TIC mode (optional), +- the list of native serial ports (optional). + +> ℹ️ The USB port list is not configurable, because TIC2WebSocket detects them automatically. + +Example configuration file: + +```json +{ + "serverPort": 19584, + "ticMode": "AUTO", + "ticPortNames": ["/dev/ttyAMA0"] +} +``` + +## `serverPort` parameter + +`serverPort` configures the TCP listening port of the TIC2WebSocket WebSocket server, between `1` and `65535`. + +The default value is `19584`. + +## `ticMode` parameter + +`ticMode` configures the TIC stream mode: + +- `"AUTO"`: automatic detection (default), +- `"STANDARD"`: Standard TIC mode at 9600 baud, +- `"HISTORIC"`: Historic TIC mode at 1200 baud. + +## `ticPortNames` parameter + +`ticPortNames` configures the list of native serial ports used to read TIC. + +When set, the list must: + +- contain at least one element, +- contain unique elements, +- contain no null element, +- contain only strings with at least one character. + +Example: + +```json +"ticPortNames": ["/dev/ttyAMA0"] +``` diff --git a/docs/docs/en/usage/index.md b/docs/docs/en/usage/index.md new file mode 100644 index 0000000..f2eeda4 --- /dev/null +++ b/docs/docs/en/usage/index.md @@ -0,0 +1,23 @@ +# Usage + +This section explains how to use TIC2WebSocket from the client side. + +## Recommended approach + +1. Open a WebSocket connection to the TIC2WebSocket server. +2. Send a `GetAvailableTICs` request to discover available streams. +3. Subscribe to the desired stream(s) using `SubscribeTIC`. +4. Handle `OnTICData` and `OnError` events. +5. Unsubscribe using `UnsubscribeTIC` before closing the client. + +## API reference + +See the dedicated page for the full list of requests and events: + +- [API Reference](api.md) + +## WebSocket tester + +To quickly test requests/events using the built-in client UI: + +- [WS Tester](../getting-started/ws-tester.md) diff --git a/docs/docs/fr/development/architecture.md b/docs/docs/fr/development/architecture.md new file mode 100644 index 0000000..11c2f05 --- /dev/null +++ b/docs/docs/fr/development/architecture.md @@ -0,0 +1,139 @@ +# Architecture + +Cette page décrit l’architecture technique de **TIC2WebSocket** : ses composants, ses flux d’exécution, ses responsabilités et ses points d’extension. + +## Objectif du système + +`TIC2WebSocket` expose une interface WebSocket JSON pour : + +- découvrir les TIC disponibles ; +- lire une trame TIC à la demande ; +- s’abonner à un ou plusieurs flux TIC ; +- recevoir des événements temps réel (`OnTICData`, `OnError`). + +Le service fait le pont entre le monde **matériel/port série** (modems TIC) et des clients applicatifs **WebSocket**. + +## Vue contexte + +```plantuml +@startuml +title TIC2WebSocket - Vue contexte +skinparam shadowing false +left to right direction +skinparam rectangle { + RoundCorner 12 +} + +actor "Client métier\n(WebSocket)" as Client +actor "OS hôte" as OS +actor "Compteur / modem TIC" as Meter + +rectangle "TIC2WebSocket" as App { + component "API WebSocket JSON" as WS + component "TICCore" as Core +} + +Client --> WS : Requêtes +WS --> Client : Réponses + événements + +Core --> OS : Détection ports\n(USB, série) +Core --> Meter : Lecture trames TIC +WS <--> Core +@enduml +``` + +## Composants principaux + +L'application est organisée en plusieurs packages Java, chacun avec des responsabilités claires. + +### Package diagnostic + +- **TICCoreApp** : application pour tests et diagnostics globaux du core TIC +- **ModemFinderApp** : application de test de détection de modems +- **ModemPlugNotifierApp** : application de test de notification de plug/unplug +- **SerialPortFinderApp** : application de test de détection de ports série +- **UsbPortFinderApp** : application de test de détection de ports USB + +### Package utilitaire + +- **Codec** : encodage/décodage JSON, trames TIC, etc +- **Message** : modèles de messages génériques (Request, Response, Event) +- **Task** : gestion de tâches asynchrones +- **Time** : utilitaires de temps et de date + +### Package io +- **Modem** : gestion d’un modem TIC (port série, USB) +- **ModemFinder** : détection dynamique des modems connectés +- **PlugNotifier** : notification des événements de plug/unplug + +### Package frame +- **TICFrame** : représentation d’une trame TIC décodée +- **TICFrameCodec** : encodage/décodage de trames TIC + +### Package stream +- **TICCoreStream** : gestion d’un flux de données TIC (lecture, décodage) +- **TICStreamReader** : lecture d’un flux TIC à partir d’un modem +- **TICStreamModeDetector** : détection du mode TIC (historique, standard) + +### Package core +- **TICCoreBase** : cœur de l’application, gestion des abonnements, notifications + +### Package service +- **TIC2WebSocketApplication** : point d’entrée de l’application TIC2WebSocket +- **TIC2WebSocketServer** : serveur WebSocket (Netty) +- **TIC2WebSocketHandler** : handler principal des requêtes WebSocket +- **TIC2WebSocketRequestHandlerBase** : base pour les handlers de requêtes métier +- **TIC2WebSocketClientPool** : gestion des clients WebSocket connectés + +## Flux d’exécution + +### Démarrage + +1. La CLI est parsée (`picocli`) et la configuration est chargée. +2. `TICCoreBase` est instancié avec le mode TIC et les ports éventuels. +3. Le serveur Netty WebSocket démarre. +4. Le core active la surveillance de plug/unplug modem. + +### Traitement d’une requête WebSocket + +1. Le handler reçoit une trame texte WebSocket. +2. Le JSON est décodé en `Message` puis transformé en `Request`. +3. `TIC2WebSocketRequestHandlerBase` exécute l’opération métier. +4. La réponse JSON est renvoyée au client. + +### Diffusion d’événements TIC + +1. Un stream lit et décode une nouvelle trame TIC. +2. `TICCoreBase` reçoit `onData` / `onError`. +3. Les abonnés filtrés par `TICIdentifier` sont résolus. +4. Chaque client reçoit un événement WebSocket. + +## Séquence : abonnement puis événements + +```plantuml +@startuml +title SubscribeTIC + diffusion OnTICData +skinparam shadowing false + +actor "Client WS" as C +participant "TIC2WebSocketHandler" as H +participant "RequestHandlerBase" as RH +participant "TICCoreBase" as Core +participant "TICCoreStream" as Stream +participant "TIC2WebSocketClient" as WSC + +C -> H : REQUEST SubscribeTIC(identifier) +H -> RH : handle(request, client) +RH -> Core : subscribe(identifier, client) +Core --> RH : ok +RH --> H : RESPONSE SubscribeTIC(errorCode=0) +H --> C : RESPONSE SubscribeTIC + +loop à chaque trame + Stream -> Core : onData(frame) + Core -> WSC : onData(frame) + WSC -> H : sendEvent(OnTICData) + H --> C : EVENT OnTICData +end +@enduml +``` diff --git a/docs/docs/fr/development/contributing.md b/docs/docs/fr/development/contributing.md new file mode 100644 index 0000000..747525e --- /dev/null +++ b/docs/docs/fr/development/contributing.md @@ -0,0 +1,18 @@ +# Contribuer + +Merci de votre intérêt pour contribuer au projet TIC2WebSocket. Vos contributions sont les bienvenues ! + +## Conformité REUSE + +Ce projet est conforme à [REUSE](https://reuse.software/), ce qui signifie que tous les fichiers source et de documentation incluent des informations claires sur la licence et les droits d'auteur. Cela garantit une réutilisation, une distribution et une conformité légale plus faciles du code. Pour plus d'informations sur la conformité REUSE et les meilleures pratiques, veuillez visiter le site officiel [REUSE.software](https://reuse.software/). + +## Convention de commits et de branches + +Nous utilisons la convention [Conventional Commits](https://www.conventionalcommits.org/fr/v1.0.0/) pour la rédaction des messages de commit. Cela permet d'assurer une meilleure lisibilité de l'historique git et facilite l'automatisation des versions et des changelogs. Merci de suivre cette convention lors de vos contributions. + +De plus, nous appliquons la convention [Conventional Branches](https://www.conventionalcommits.org/fr/v1.0.0/#conventional-branches) pour le nommage des branches. Veuillez consulter la documentation officielle pour nommer vos branches de façon appropriée avant de soumettre une pull request. + +## Licence + +Ce projet est sous licence [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). +Vous êtes libre d'utiliser, de modifier et de distribuer ce logiciel conformément aux termes de cette licence. diff --git a/docs/docs/fr/development/index.md b/docs/docs/fr/development/index.md new file mode 100644 index 0000000..71ebee2 --- /dev/null +++ b/docs/docs/fr/development/index.md @@ -0,0 +1,18 @@ +--- +sidebar_position: 1 +title: Development +--- + +# Development + +This section documents how to work on **TIC2WebSocket** locally, run checks, and contribute safely. + +## Architecture + +See the dedicated page for an overview of the architecture and design decisions: + +- [Architecture](architecture.md) + +## Contributing +See the dedicated page for guidelines on contributing to the project: +- [Contributing](contributing.md) \ No newline at end of file diff --git a/docs/docs/fr/getting-started/index.md b/docs/docs/fr/getting-started/index.md new file mode 100644 index 0000000..a00b150 --- /dev/null +++ b/docs/docs/fr/getting-started/index.md @@ -0,0 +1,9 @@ +# Démarrage + +Cette section explique comment démarrer rapidement avec TIC2WebSocket. + +## Étapes rapides +1. Vérifiez les prérequis matériels et logiciels. [Prérequis](prerequisites.md) +2. Installez TIC2WebSocket en suivant les instructions d'installation. [Installation](installation.md) +3. Lancez le serveur TIC2WebSocket. [Lancement](launch.md) +4. Utilisez le testeur WebSocket pour envoyer des requêtes et recevoir des données TIC. [Testeur WebSocket](ws-tester.md) diff --git a/docs/docs/fr/getting-started/installation.md b/docs/docs/fr/getting-started/installation.md new file mode 100644 index 0000000..f7285e3 --- /dev/null +++ b/docs/docs/fr/getting-started/installation.md @@ -0,0 +1,45 @@ +# Installation + +## Build du projet + +### Cloner le dépôt + +``` +git clone https://github.com/Enedis-OSS/TIC2WebSocket.git +``` + +### Lancer le build Maven + +``` +cd TIC2WebSocket +mvn clean package +``` + +### Vérifier la génération des artefacts + +``` +cd target +ls +``` +Vous devriez voir les livrables générés, notamment `TIC2WebSocket-VERSION-bin.zip` ou `TIC2WebSocket-VERSION-bin.tar.gz`. + +## Lancement de la documentation + +### Aller dans le dossier `docs/` + +``` +cd docs +``` + +### Démarrer les services Docker de documentation + +``` +docker-compose up --build +``` + +### Accéder à la documentation + +``` +http://localhost:8000 +``` +Vous devriez voir la documentation TIC2WebSocket s'afficher dans votre navigateur. diff --git a/docs/docs/fr/getting-started/launch.md b/docs/docs/fr/getting-started/launch.md new file mode 100644 index 0000000..7abfb37 --- /dev/null +++ b/docs/docs/fr/getting-started/launch.md @@ -0,0 +1,95 @@ +# Lancement + +## Lancer le serveur TIC2WebSocket + +### Linux/MacOS + +Une fois le dossier compressé de TIC2WebSocket généré, extraire le contenu du dossier dans un répertoire de votre choix. + +Définir les variables d'environnement `APPLICATION_HOME` et `VERSION` pour indiquer le chemin vers le dossier d'installation et la version de TIC2WebSocket. + +```bash +export APPLICATION_HOME=/chemin/vers/dossier +export VERSION=1.0.0 +``` + +```bash +tar -xzf TIC2WebSocket-$VERSION-bin.tar.gz -C $APPLICATION_HOME --strip-components=1 +cd $APPLICATION_HOME +``` + +Afficher l'aide de TIC2WebSocket via le lanceur avec la commande suivante : + +```bash +./TIC2WebSocket.sh --help +``` + +Vous devriez voir les options d'aide du lanceur TIC2WebSocket s'afficher, confirmant que le serveur est prêt à être lancé. + +Le serveur s'arrête avec Ctrl+C. + +Pour démarrer le serveur TIC2WebSocket, utilisez la commande suivante : + +```bash +./TIC2WebSocket.sh --verbose=INFO --configFile=var/config/TIC2WebSocketConfiguration.json +``` + +Vous devriez voir les logs du serveur TIC2WebSocket s'afficher dans la console, indiquant que le serveur est en cours d'exécution et prêt à accepter les connexions WebSocket. + +``` +Loading configuration file var/config/TIC2WebSocketConfiguration.json +TIC2WebSocket initialized +TIC2WebSocket starting +Starting TICCore +Starting TIC2WebSocket Netty server on localhost:19584 +TIC2WebSocket Netty server started successfully on localhost:19584 +TIC2WebSocket started +``` + +### Windows + +Une fois le dossier compressé de TIC2WebSocket généré, extraire le contenu du dossier dans un répertoire de votre choix. + +Définir les variables d'environnement `APPLICATION_HOME` et `VERSION` pour indiquer le chemin vers le dossier d'installation et la version de TIC2WebSocket. + +```powershell +$env:APPLICATION_HOME = "C:\chemin\vers\dossier" +$env:VERSION = "1.0.0" +``` + +```powershell +Unzip-File -Path TIC2WebSocket-$env:VERSION-bin.zip -DestinationPath $env:APPLICATION_HOME +cd $env:APPLICATION_HOME +``` + +Afficher l'aide de TIC2WebSocket via le lanceur avec la commande suivante : + +```powershell +.\TIC2WebSocket.bat --help +``` + +Vous devriez voir les options d'aide du lanceur TIC2WebSocket s'afficher, confirmant que le serveur est prêt à être lancé. + +Le serveur s'arrête avec Ctrl+C. + +Pour démarrer le serveur TIC2WebSocket, utilisez la commande suivante : + +```powershell +.\TIC2WebSocket.bat --verbose=INFO --configFile=var/config/TIC2WebSocketConfiguration.json +``` + +Vous devriez voir les logs du serveur TIC2WebSocket s'afficher dans la console, indiquant que le serveur est en cours d'exécution et prêt à accepter les connexions WebSocket. + +``` +Loading configuration file var/config/TIC2WebSocketConfiguration.json +TIC2WebSocket initialized +TIC2WebSocket starting +Starting TICCore +Starting TIC2WebSocket Netty server on localhost:19584 +TIC2WebSocket Netty server started successfully on localhost:19584 +TIC2WebSocket started +``` + +## Tester la connexion WebSocket + +Une fois le serveur TIC2WebSocket démarré, vous pouvez tester la connexion WebSocket en utilisant le client WebSocket intégré à la documentation. Suivez les instructions du guide [Testeur WebSocket](ws-tester.md) pour vous connecter au serveur TIC2WebSocket et recevoir les données TIC en temps réel. \ No newline at end of file diff --git a/docs/docs/fr/getting-started/prerequisites.md b/docs/docs/fr/getting-started/prerequisites.md new file mode 100644 index 0000000..c870450 --- /dev/null +++ b/docs/docs/fr/getting-started/prerequisites.md @@ -0,0 +1,12 @@ +# Prérequis + +## Environnement + +- Java 8 +- Maven +- Docker / Docker Compose (optionnel pour la doc locale) + +## Sources + +- Dépôt GitHub du projet +- Accès aux fichiers de configuration diff --git a/docs/docs/fr/getting-started/ws-tester.md b/docs/docs/fr/getting-started/ws-tester.md new file mode 100644 index 0000000..44f6cc1 --- /dev/null +++ b/docs/docs/fr/getting-started/ws-tester.md @@ -0,0 +1,77 @@ +# WebSocket Tester + +Le projet inclut une interface cliente WebSocket prête à l'emploi : `ws-tester.html`. + +Ce testeur permet de valider rapidement une instance TIC2WebSocket, sans développer de client spécifique. + +## Rôle dans le projet + +Utilisez le testeur pour : + +- vérifier que le serveur WebSocket est joignable, +- découvrir les flux TIC disponibles, +- générer des requêtes JSON valides pour les principales commandes API, +- inspecter les réponses/évènements entrants en temps réel, +- reproduire des problèmes d'intégration lors du dépannage. + +## Ouvrir le testeur + +Ouvrez `ws-tester.html` dans votre navigateur. + +Par défaut, il cible `ws://localhost:19584/`. + +Vous pouvez préremplir la connexion avec des paramètres de requête : + +- URL complète : `?url=ws://localhost:19584/` (alias : `wsUrl`, `ws`) +- Hôte/port : `?host=localhost&port=19584` (alias hôte : `address`, `addr`) + +## Flux d'utilisation principal + +1. Cliquez sur **Connect**. +2. Choisissez un preset de requête. +3. Renseignez les identifiants si nécessaire. +4. Vérifiez le payload JSON généré. +5. Cliquez sur **Send** et surveillez les logs. +6. Cliquez sur **Disconnect** en fin de test. + +## Presets de requêtes et identifiants + +Presets disponibles : + +- `GetModemsInfo` +- `GetAvailableTICs` +- `ReadTIC` +- `SubscribeTIC` +- `UnsubscribeTIC` + +Les champs d'identifiant sont optionnels sauf pour `ReadTIC`, qui exige un identifiant. + +Règle de priorité appliquée par le testeur : + +1. `serialNumber` +2. `portId` +3. `portName` + +Lorsqu'un identifiant de priorité supérieure est actif, les champs de priorité inférieure sont désactivés automatiquement. + +Pour `SubscribeTIC` / `UnsubscribeTIC` : + +- aucun identifiant sélectionné -> la requête s'applique à tous les flux, +- un identifiant sélectionné -> la requête cible ce flux. + +Lors de la réception d'une réponse `GetAvailableTICs`, le testeur remplit automatiquement les champs d'identifiants avec la première entrée retournée. + +## Logs et diagnostic + +- Lignes **OUT** : JSON envoyé par le client. +- Lignes **IN** : messages reçus du serveur. +- Lignes **ERR** : erreurs de validation client ou erreurs socket. + +Les messages `EVENT` sont regroupés par nom d'évènement + identifiant pour garder des logs lisibles en mode flux. + +## Bonnes pratiques + +- Commencez par `GetAvailableTICs` avant `ReadTIC`/`SubscribeTIC`. +- Conservez une indentation JSON à `2` pour faciliter le debug. +- Utilisez **Clear logs** avant de reproduire un incident. +- Conservez les couples requête/réponse utiles lors d'un signalement. \ No newline at end of file diff --git a/docs/docs/fr/index.md b/docs/docs/fr/index.md index 27b8a5c..042c4fd 100644 --- a/docs/docs/fr/index.md +++ b/docs/docs/fr/index.md @@ -1 +1,21 @@ # TIC2WebSocket + +Bienvenue dans la documentation du projet TIC2WebSocket. + +## Introduction +TIC2WebSocket est une application Java qui expose les données TIC (Téléinformation Client) via une interface WebSocket. Elle permet aux développeurs d'accéder facilement aux données TIC en temps réel, facilitant ainsi l'intégration avec d'autres systèmes et applications. + +## Structure de la documentation +La documentation est organisée en plusieurs sections pour vous guider à travers les différentes étapes de l'utilisation de TIC2WebSocket : + +- [Démarrage](getting-started/index.md) : Guide pour démarrer rapidement avec TIC2WebSocket. + - [Prérequis](getting-started/prerequisites.md) : Liste des prérequis matériels et logiciels nécessaires. + - [Installation](getting-started/installation.md) : Instructions détaillées pour installer TIC2WebSocket. + - [Lancement](getting-started/launch.md) : Comment lancer le serveur TIC2WebSocket. + - [Testeur WebSocket](getting-started/ws-tester.md) : Guide pour utiliser le testeur WebSocket intégré. +- [Utilisation](usage/index.md) : Exemples d'utilisation et cas d'usage courants. + - [Référence API](usage/api.md) : Documentation de l'API WebSocket exposée par TIC2WebSocket. + - [Configuration](usage/configuration.md) : Détails sur la configuration avancée de TIC2WebSocket. +- [Développement](development/index.md) : Guide pour contribuer au projet TIC2WebSocket. + - [Architecture](development/architecture.md) : Description de l'architecture du projet TIC2WebSocket. + - [Contribuer](development/contributing.md) : Comment contribuer au code, signaler des bugs, et proposer des améliorations. \ No newline at end of file diff --git a/docs/docs/fr/usage/api.md b/docs/docs/fr/usage/api.md new file mode 100644 index 0000000..9360b4d --- /dev/null +++ b/docs/docs/fr/usage/api.md @@ -0,0 +1,331 @@ +# Référence API + +La communication avec TIC2WebSocket repose sur des messages JSON échangés via WebSocket. + +## GetAvailableTICs + +Cette requête permet de récupérer la liste des flux TIC disponibles. + +### Requête + +```json +{ + "type": "REQUEST", + "name": "GetAvailableTICs" +} +``` + +### Réponse + +La réponse contient une liste de structures avec les informations TIC : + +- numéro de série ; +- nom du port série ; +- identifiant du port USB physique. + +```json +{ + "type": "RESPONSE", + "name": "GetAvailableTICs", + "dateTime": "10/06/2021 14:01:00", + "errorCode": 0, + "data": [ + { + "serialNumber": "021762010203", + "portName": "/dev/ttyUSB0", + "portId": "1-1" + }, + { + "serialNumber": "041775010507", + "portName": "/dev/ttyUSB2", + "portId": "1-2" + } + ] +} +``` + +## GetModemsInfo + +Cette requête permet de récupérer la liste des informations des modems TIC connectés. + +La reconnaissance des modems se fait à partir de la paire USB `vendorId` + `productId`. + +Types de modems pris en charge : + +| Type de modem | Vendor ID (hex) | Product ID (hex) | Vendor ID (dec) | Product ID (dec) | +| --- | --- | --- | --- | --- | +| `MICHAUD` | `0x0403` | `0x6001` | `1027` | `24577` | +| `TELEINFO` | `0x0403` | `0x6015` | `1027` | `24597` | + +### Requête + +```json +{ + "type": "REQUEST", + "name": "GetModemsInfo" +} +``` + +### Réponse + +La réponse contient une liste de structures avec les informations des modems : + +- nom du port série ; +- type de modem ; +- identifiant produit (USB PID), si disponible ; +- identifiant vendeur (USB VID), si disponible ; +- nom du produit, si disponible ; +- nom du fabricant, si disponible ; +- numéro de série du modem, si disponible ; +- identifiant physique du port série, si disponible. + +```json +{ + "type": "RESPONSE", + "name": "GetModemsInfo", + "dateTime": "16/06/2021 15:53:32", + "errorCode": 0, + "data": [ + { + "portName": "COM3", + "modemType": "MICHAUD", + "productId": 24597, + "vendorId": 1027, + "productName": null, + "manufacturerName": "FTDI", + "serialNumber": "DA2VYTKGA", + "portId": null + } + ] +} +``` + +## ReadTIC + +Cette requête permet de lire une trame TIC. + +### Requête + +```json +{ + "type": "REQUEST", + "name": "ReadTIC", + "data": { + "serialNumber": "010203040506" + } +} +``` + +Le champ `data` doit contenir un identifiant TIC défini dans le [format d'identifiant TIC](#format-didentifiant-tic). + +### Réponse + +```json +{ + "type": "RESPONSE", + "name": "ReadTIC", + "dateTime": "10/06/2021 14:01:00", + "errorCode": 0, + "data": { + "mode": "STANDARD", + "captureDateTime": "10/06/2021 14:01:00", + "ticIdentifier": { + "serialNumber": "021762010203", + "portName": "/dev/ttyUSB0", + "portId": "1-1" + }, + "content": { + "ADSC": "010203040506", + "URMS1": 230 + } + } +} +``` + +## SubscribeTIC + +Cette requête permet de s'abonner à un flux TIC. Une fois abonné, le client reçoit des évènements à chaque nouvelle trame TIC ou erreur liée au flux TIC. + +### Requête + +Plusieurs formats sont possibles. + +Abonnement à un flux TIC : + +```json +{ + "type": "REQUEST", + "name": "SubscribeTIC", + "data": { + "serialNumber": "010203040506" + } +} +``` + +Abonnement à plusieurs flux TIC : + +```json +{ + "type": "REQUEST", + "name": "SubscribeTIC", + "data": [ + { + "serialNumber": "010203040506" + }, + { + "portId": "1-1" + } + ] +} +``` + +Abonnement à tous les flux TIC : + +```json +{ + "type": "REQUEST", + "name": "SubscribeTIC" +} +``` + +Le champ `data` doit contenir un identifiant TIC (ou une liste d'identifiants TIC) défini dans le [format d'identifiant TIC](#format-didentifiant-tic). + +### Réponse + +Réponse en cas de succès : + +```json +{ + "type": "RESPONSE", + "name": "SubscribeTIC", + "dateTime": "10/06/2021 14:01:00", + "errorCode": 0 +} +``` + +Si le flux TIC demandé n'existe pas : + +```json +{ + "type": "RESPONSE", + "name": "SubscribeTIC", + "dateTime": "10/06/2021 14:01:00", + "errorCode": -7, + "errorMessage": "The given TIC identifier doesn't match with current TIC stream" +} +``` + +## Évènement `OnTICData` + +Cet évènement est généré à chaque nouvelle trame TIC et envoyé à tous les clients abonnés à cette TIC. + +```json +{ + "type": "EVENT", + "dateTime": "10/06/2021 14:01:00", + "name": "OnTICData", + "data": { + "mode": "STANDARD", + "captureDateTime": "10/06/2021 14:01:00", + "ticIdentifier": { + "serialNumber": "021762010203", + "portName": "/dev/ttyUSB0", + "portId": "1-1" + }, + "content": { + "ADSC": "010203040506", + "URMS1": 230 + } + } +} +``` + +## Évènement `OnError` + +Cet évènement est généré lorsqu'une erreur survient sur le flux TIC et envoyé à tous les clients abonnés à cette TIC. + +```json +{ + "type": "EVENT", + "dateTime": "10/06/2021 14:01:00", + "name": "OnError", + "data": { + "errorCode": -5, + "errorMessage": "TIC frame reading timeout", + "ticIdentifier": { + "serialNumber": "021762010203", + "portName": "/dev/ttyUSB0", + "portId": "1-1" + } + } +} +``` + +## UnsubscribeTIC + +Cette requête permet de se désabonner d'un flux TIC. + +### Requête + +Plusieurs formats sont possibles. + +Désabonnement d'un flux TIC : + +```json +{ + "type": "REQUEST", + "name": "UnsubscribeTIC", + "data": { + "serialNumber": "010203040506" + } +} +``` + +Désabonnement de plusieurs flux TIC : + +```json +{ + "type": "REQUEST", + "name": "UnsubscribeTIC", + "data": [ + { + "serialNumber": "010203040506" + }, + { + "portId": "1-1" + } + ] +} +``` + +Désabonnement de tous les flux TIC : + +```json +{ + "type": "REQUEST", + "name": "UnsubscribeTIC" +} +``` + +Dans le cas d'un désabonnement ciblé, le champ `data` doit contenir un identifiant TIC (ou une liste d'identifiants TIC) défini dans le [format d'identifiant TIC](#format-didentifiant-tic). + +## Format d'identifiant TIC + +Un identifiant TIC est un objet contenant un des champs suivants : + +- `serialNumber` +- `portId` +- `portName` + +Selon la requête, vous pouvez fournir un identifiant unique ou une liste d'identifiants. + +### Réponse + +```json +{ + "type": "RESPONSE", + "name": "UnsubscribeTIC", + "dateTime": "16/06/2021 15:53:32", + "errorCode": 0 +} +``` \ No newline at end of file diff --git a/docs/docs/fr/usage/configuration.md b/docs/docs/fr/usage/configuration.md new file mode 100644 index 0000000..a94a60b --- /dev/null +++ b/docs/docs/fr/usage/configuration.md @@ -0,0 +1,56 @@ +# Configuration + +## Emplacement + +Le fichier de configuration `TIC2WebSocketConfiguration.json` se trouve dans le sous-répertoire `var/config` du répertoire d'installation de TIC2WebSocket. + +## Structure + +Le fichier de configuration utilise le format JSON et vous permet de modifier les paramètres suivants : + +- le port d'écoute du serveur WebSocket ; +- le mode TIC (optionnel) ; +- la liste des ports série natifs (optionnel). + +> ℹ️ La liste des ports USB n'est pas configurable, car TIC2WebSocket les détecte automatiquement. + +Exemple de fichier de configuration : + +```json +{ + "serverPort": 19584, + "ticMode": "AUTO", + "ticPortNames": ["/dev/ttyAMA0"] +} +``` + +## Paramètre `serverPort` + +Le paramètre `serverPort` permet de configurer le port TCP d'écoute du serveur WebSocket TIC2WebSocket, entre `1` et `65535`. + +La valeur par défaut est `19584`. + +## Paramètre `ticMode` + +Le paramètre `ticMode` permet de configurer le mode de flux TIC : + +- `"AUTO"` : détection automatique (valeur par défaut) ; +- `"STANDARD"` : mode TIC Standard à 9600 bauds ; +- `"HISTORIC"` : mode TIC Historique à 1200 bauds. + +## Paramètre `ticPortNames` + +Le paramètre `ticPortNames` permet de configurer la liste des ports série natifs utilisés pour lire la TIC. + +Lorsque ce paramètre est défini, la liste doit : + +- contenir au moins un élément ; +- contenir des éléments uniques ; +- ne contenir aucun élément nul ; +- contenir uniquement des chaînes d'au moins un caractère. + +Exemple : + +```json +"ticPortNames": ["/dev/ttyAMA0"] +``` diff --git a/docs/docs/fr/usage/index.md b/docs/docs/fr/usage/index.md new file mode 100644 index 0000000..249f5b0 --- /dev/null +++ b/docs/docs/fr/usage/index.md @@ -0,0 +1,23 @@ +# Utilisation + +Cette section présente l'utilisation de TIC2WebSocket côté client. + +## Démarche recommandée + +1. Ouvrir une connexion WebSocket vers le serveur TIC2WebSocket. +2. Envoyer une requête `GetAvailableTICs` pour découvrir les flux disponibles. +3. S'abonner au(x) flux voulu(s) avec `SubscribeTIC`. +4. Traiter les évènements `OnTICData` et `OnError`. +5. Se désabonner avec `UnsubscribeTIC` avant fermeture du client. + +## Référence API + +Consultez la page dédiée à la référence complète des requêtes et évènements : + +- [Référence API](api.md) + +## Testeur WebSocket + +Pour tester rapidement les requêtes/évènements avec l'interface cliente intégrée : + +- [WS Tester](../getting-started/ws-tester.md) diff --git a/docs/docs/stylesheets/puml-fix.css b/docs/docs/stylesheets/puml-fix.css new file mode 100644 index 0000000..1abfaca --- /dev/null +++ b/docs/docs/stylesheets/puml-fix.css @@ -0,0 +1,25 @@ +/* + * PlantUML (mkdocs-puml / plantuml plugin) renders both light and dark SVGs. + * The upstream puml.css only hides variants when Material palette attributes + * (data-md-color-scheme) exist or when prefers-color-scheme is dark. + * + * When neither applies (common on fresh load or when no palette is configured), + * both variants become visible. This file enforces a sane default. + */ + +/* Default: show light, hide dark */ +body .puml.light { display: block; } +body .puml.dark { display: none; } + +/* OS/browser dark mode */ +@media (prefers-color-scheme: dark) { + body .puml.light { display: none; } + body .puml.dark { display: block; } +} + +/* MkDocs Material explicit palette (if enabled) */ +body[data-md-color-scheme="default"] .puml.light { display: block; } +body[data-md-color-scheme="default"] .puml.dark { display: none; } + +body[data-md-color-scheme="slate"] .puml.light { display: none; } +body[data-md-color-scheme="slate"] .puml.dark { display: block; } diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 33c41d9..312d532 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,23 +1,103 @@ +# SPDX-FileCopyrightText: 2025 Enedis Smarties team +# SPDX-FileContributor: Jehan BOUSCH +# SPDX-FileContributor: Mathieu SABARTHES +# +# SPDX-License-Identifier: Apache-2.0 + site_name: TIC2WebSocket -site_url: "" +site_url: "https://enedis-oss.github.io/tic2websocket/" +site_description: Documentation TIC2WebSocket +site_author: Enedis OSS -docs_dir: docs -site_dir: site - -theme: - name: material +repo_name: Enedis-OSS/tic2websocket +repo_url: https://github.com/Enedis-OSS/TIC2WebSocket +# Site navigation nav: - - Français: fr/index.md - - English: en/index.md + - Overview: index.md + - Getting Started: + - Overview: getting-started/index.md + - Prerequisites: getting-started/prerequisites.md + - Installation: getting-started/installation.md + - Launch: getting-started/launch.md + - WS Tester: getting-started/ws-tester.md + - Usage: + - Overview: usage/index.md + - API Reference: usage/api.md + - Configuration: usage/configuration.md + - Development: + - Overview: development/index.md + - Architecture: development/architecture.md + - Contributing: development/contributing.md markdown_extensions: - admonition - pymdownx.details - pymdownx.superfences - - plantuml_markdown: - server: "http://plantuml:8080/plantuml" - format: svg + # - plantuml_markdown: + # server: "http://plantuml:8080/plantuml" + # format: svg plugins: + - plantuml: + puml_url: http://plantuml:8080 + puml_keyword: plantuml + theme: + enabled: true + light: default/light + dark: default/dark - search + - i18n: + docs_structure: folder + fallback_to_default: true + reconfigure_material: true + reconfigure_search: true + languages: + - locale: fr + default: true + name: Français + build: true + nav_translations: + Overview: Vue d'ensemble + Getting Started: Démarrage + Prerequisites: Prérequis + Installation: Installation + Configuration: Configuration + Usage: Utilisation + API Reference: Référence API + WS Tester: WS Tester + Architecture: Architecture + Contributing: Contribuer + Launch: Lancement + Development: Développement + + - locale: en + name: English + build: true + nav_translations: + Overview: Overview + Getting Started: Getting Started + Prerequisites: Prerequisites + Installation: Installation + Configuration: Configuration + Usage: Usage + API Reference: API Reference + WS Tester: WS Tester + Architecture: Architecture + Contributing: Contributing + Launch: Launch + Development: Development + +theme: + name: material + language: fr + font: + text: Ubuntu + features: + - navigation.sections + - navigation.expand + - navigation.indexes + - toc.integrate + +extra_css: + - stylesheets/puml-fix.css diff --git a/docs/requirements.txt b/docs/requirements.txt index 4481a40..f2876d9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,4 @@ # Extra MkDocs extensions beyond squidfunk/mkdocs-material plantuml-markdown +mkdocs-puml +mkdocs-static-i18n From 4e4fa3d5c1244f6020e366e597bd2ba1beef7371 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:52:24 +0000 Subject: [PATCH 48/59] chore: Update .gitignore to exclude docs/site directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8787fe0..b699d06 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ src/main/resources/TIC2WebSocket.properties target/ var/ +docs/site/ \ No newline at end of file From 2592ad3eb2bd144553fb937850bfaaa4d86f7936 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:28:47 +0000 Subject: [PATCH 49/59] docs: Update README files for improved structure and clarity --- README.fr.md | 102 +++----------------------------------------------- README.md | 104 ++++----------------------------------------------- 2 files changed, 13 insertions(+), 193 deletions(-) diff --git a/README.fr.md b/README.fr.md index 3de3070..aa008c3 100644 --- a/README.fr.md +++ b/README.fr.md @@ -16,13 +16,11 @@ ## Sommaire -* [Introduction](#introduction) -* [Installation](#installation) -* [Exemple d'utilisation](#usage_example) -* [Documentation](#documentation) -* [Contribuer](#contrib) -* [Support](#support) -* [Contributeurs](#contributors) +- [TIC2WebSocket](#tic2websocket) + - [Sommaire](#sommaire) + - [ Introduction](#-introduction) + - [ Documentation](#-documentation) + - [ Contributeurs](#-contributeurs) ## Introduction @@ -30,97 +28,9 @@ L'application **TIC2WebSocket** est utilisée comme interface générique pour a TIC2WebSocket fournit une API basée sur WebSocket pour gérer les données TIC, permettant une communication en temps réel et un échange de données pour les informations de télémétrie client. -## Installation - -### Prérequis - -Pour générer/installer l'application, vous avez besoin des prérequis suivants : - -- [Java](https://www.java.com/download/) -- [Maven](https://maven.apache.org/download.cgi) - -### Installation - -Pour installer l'application **TIC2WebSocket**, suivez ces étapes : - -#### Générer les fichiers cible -Lorsque vous êtes à la racine du projet, tapez simplement la commande suivante : - -```bash -mvn clean package -``` - -En conséquence, le répertoire *target* est créé, contenant le fichier .zip de sortie. - -#### Extraire le fichier .zip -Décompressez le fichier .zip dans le dossier de votre choix. - -🐧 **Linux** - -Extraire le zip -```bash -unzip target/TIC2WebSocket-VERSION-bin -d /chemin/vers/votre/dossier -``` - -💻 **Windows** - -Extrayez `target/TIC2WebSocket-VERSION-bin` dans le dossier de votre choix. - -### Démarrage de l'Application - -Pour démarrer l'application **TIC2WebSocket**, exécutez le script de lancement : - -🐧 **Linux** - -Extraire le zip -```bash -cd /chemin/vers/votre/dossier -./TIC2WebSocket.sh -``` - -💻 **Windows** - -Exécutez `TIC2WebSocket.bat` - -### Aide - -Ajoutez l'option `--help` pour obtenir des informations de base sur l'utilisation du lanceur. -```bash -./TIC2WebSocket.sh --help -``` - ## Documentation -### Page HTML de test WebSocket - -Une page HTML autonome est disponible pour tester rapidement l’API WebSocket : - -- Fichier : [docs/ws-tester.html](docs/ws-tester.html) -- URL par défaut : `ws://localhost:19584/` - -Fonctionnement : - -- Ouvrez la page dans un navigateur (optionnel : servez le dossier `docs/` via un serveur HTTP statique). -- Renseignez l’URL WebSocket (ou host/port), puis cliquez sur `Connect`. -- Choisissez une requête prédéfinie (`GetModemsInfo`, `GetAvailableTICs`, `ReadTIC`, `SubscribeTIC`, `UnsubscribeTIC`) puis cliquez sur `Send`. -- Les filtres d’identifiant (SerialNumber / PortId / PortName) permettent de cibler un TIC. -- Les messages entrants s’affichent dans Logs ; les messages de type `EVENT` sont regroupés par `(nom d’événement + identifiant)`. - -## Contribuer ? - -![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) - -Vous n'avez pas besoin d'être développeur pour contribuer, ni de faire beaucoup, vous pouvez simplement : -* Améliorer la documentation, -* Corriger une faute d'orthographe, -* [Signaler un bug](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose) -* [Demander une fonctionnalité](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose) -* [Nous donner des conseils ou des idées](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose), -* etc. - -Pour vous aider à démarrer, nous vous invitons à lire [Contributing](CONTRIBUTING.md), qui vous donne les règles et conventions de code à respecter - -Pour contribuer à cette documentation (README, CONTRIBUTING, etc.), nous nous conformons à la [CommonMark Spec](https://spec.commonmark.org/) +Pour plus d'informations sur le fonctionnement de TIC2WebSocket, consultez la [documentation officielle](https://enedis-oss.github.io/tic2websocket/). ## Contributeurs diff --git a/README.md b/README.md index 5a75266..f62174e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ --> # TIC2WebSocket -*Tele Information Client (TIC) WebSocket Interface* +*WebSocket interface for TIC (Tele Information Client) data* [![REUSE status](https://api.reuse.software/badge/git.fsfe.org/reuse/api)](https://api.reuse.software/info/git.fsfe.org/reuse/api) @@ -16,13 +16,9 @@ ## Summary -* [Introduction](#introduction) -* [Installation](#installation) -* [Usage Example](#usage_example) -* [Documentation](#documentation) -* [Contributing](#contrib) -* [Support](#support) -* [Contributors](#contributors) +- [Introduction](#introduction) +- [Documentation](#documentation) +- [Contributors](#contributors) ## Introduction @@ -30,97 +26,11 @@ The **TIC2WebSocket** application is used as a generic interface for accessing t TIC2WebSocket provides a WebSocket-based API for managing and interfacing with TIC data, enabling real-time communication and data exchange for client telemetry information. -## Installation - -### Prerequisites - -To generate/install the application, you need the following prerequisites: - -- [Java](https://www.java.com/download/) -- [Maven](https://maven.apache.org/download.cgi) - -### Installation - -To install the **TIC2WebSocket** application, follow these steps: - -#### Generate target files -When you are at the project root, simply type the following command: - -```bash -mvn clean package -``` - -As a result, the *target* directory is created, containing the output .zip file. - -#### Extract .zip file -Unzip the .zip file into the folder of your choice. - -🐧 **Linux** - -Extract zip -```bash -unzip target/TIC2WebSocket-VERSION-bin -d /path/to/your/folder -``` - -💻 **Windows** - -Extract `target/TIC2WebSocket-VERSION-bin` in the folder of your choice. - -### Starting the Application - -To start the **TIC2WebSocket** application, execute the launch script: - -🐧 **Linux** - -Extract zip -```bash -cd /path/to/your/folder -./TIC2WebSocket.sh -``` - -💻 **Windows** - -Run `TIC2WebSocket.bat` - -### Help - -Add `--help` option to get basic information on how to use the launcher. -```bash -./TIC2WebSocket.sh --help -``` - ## Documentation -### WebSocket tester (HTML) - -A small, self-contained HTML page is available to quickly test the WebSocket API: - -- File: [docs/ws-tester.html](docs/ws-tester.html) -- Default URL: `ws://localhost:19584/` - -How it works: - -- Open the page in a browser (optionally serve the `docs/` folder with any static server). -- Set the WebSocket URL (or host/port), click `Connect`. -- Pick a request preset (`GetModemsInfo`, `GetAvailableTICs`, `ReadTIC`, `SubscribeTIC`, `UnsubscribeTIC`) and click `Send`. -- Optional identifier filters (SerialNumber / PortId / PortName) help target a specific TIC. -- Incoming messages are displayed in the Logs panel; `EVENT` messages are grouped by `(event name + identifier)` for readability. - -## Contributing ? - -![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) - -You don't need to be a developer to contribute, nor do much, you can simply: -* Enhance documentation, -* Correct a spelling, -* [Report a bug](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose) -* [Ask a feature](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose) -* [Give us advices or ideas](https://github.com/Enedis-OSS/TIC2WebSocket/issues/new/choose), -* etc. - -To help you start, we invite you to read [Contributing](CONTRIBUTING.md), which gives you rules and code conventions to respect +For more information about TIC2WebSocket, see the official documentation: -To contribute to this documentation (README, CONTRIBUTING, etc.), we conform to the [CommonMark Spec](https://spec.commonmark.org/) +- https://enedis-oss.github.io/tic2websocket/ ## Contributors @@ -128,4 +38,4 @@ Core contributors : * **Jehan BOUSCH** () * **Mathieu SABARTHES** () -We strive to provide a benevolent environment and support any [contribution](#contrib). +We strive to provide a benevolent environment and support any contribution. From ef6594b2bb002df965dd1a69dff2a36e56e9a1cc Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:36:48 +0000 Subject: [PATCH 50/59] docs: Update REUSE.toml to include documentation file annotations --- REUSE.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/REUSE.toml b/REUSE.toml index b5c7366..a2fec55 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -17,4 +17,12 @@ path = [ "**/*.svg" ] SPDX-FileCopyrightText = "2025 Enedis Smarties team " +SPDX-License-Identifier = "Apache-2.0" + +# Documentation files are covered via REUSE annotations (no per-file header) +[[annotations]] +path = [ + "docs/**" +] +SPDX-FileCopyrightText = "2025 Enedis Smarties team " SPDX-License-Identifier = "Apache-2.0" \ No newline at end of file From a044a91baed2d85fc275e9cbf9a8c87097744c73 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:44:52 +0000 Subject: [PATCH 51/59] docs: Update documentation links and enhance Docker setup for GitHub Pages deployment --- README.fr.md | 2 +- README.md | 4 +--- docs/Dockerfile | 4 ++++ docs/README.md | 16 ++++++++++++++++ docs/docker-compose.yml | 19 ++++++++++++++++--- 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/README.fr.md b/README.fr.md index aa008c3..91d797d 100644 --- a/README.fr.md +++ b/README.fr.md @@ -30,7 +30,7 @@ TIC2WebSocket fournit une API basée sur WebSocket pour gérer les données TIC, ## Documentation -Pour plus d'informations sur le fonctionnement de TIC2WebSocket, consultez la [documentation officielle](https://enedis-oss.github.io/tic2websocket/). +Pour plus d'informations sur le fonctionnement de TIC2WebSocket, consultez la [documentation officielle](https://enedis-oss.github.io/TIC2WebSocket/). ## Contributeurs diff --git a/README.md b/README.md index 98b7cf7..de56b6e 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,7 @@ TIC2WebSocket provides a WebSocket-based API for managing and interfacing with T ## Documentation -For more information about TIC2WebSocket, see the official documentation: - -- https://enedis-oss.github.io/tic2websocket/ +For more information about TIC2WebSocket, see the [official documentation](https://enedis-oss.github.io/TIC2WebSocket/). ## Contributors diff --git a/docs/Dockerfile b/docs/Dockerfile index 54fc120..3da9510 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,4 +1,8 @@ FROM squidfunk/mkdocs-material +# Required because this repository is configured with Git LFS hooks. +# `mkdocs gh-deploy` triggers a `git push` which runs the pre-push hook. +RUN apk add --no-cache git-lfs + COPY requirements.txt . RUN pip install -r requirements.txt diff --git a/docs/README.md b/docs/README.md index 052e037..54178e9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,3 +8,19 @@ Depuis le dossier `docs` à la racine du projet : - Arrêter : `docker-compose down` Puis visiter : http://localhost:8000/tic2websocket/ (la racine `/` redirige vers ce chemin) + +## Déployer sur GitHub Pages + +La commande `mkdocs gh-deploy` pousse sur la branche `gh-pages` et nécessite une authentification GitHub. + +GitHub ne supporte plus l'authentification Git via HTTPS avec le mot de passe du compte : utiliser SSH. + +Déploiement en SSH : + +- Passer le remote en SSH : `git remote set-url origin git@github.com:Enedis-OSS/TIC2WebSocket.git` +- Générer une clé SSH (déjà ignorée via `var/`) : + - `mkdir -p ../var/ssh && chmod 700 ../var/ssh` + - `ssh-keygen -t ed25519 -f ../var/ssh/id_ed25519_tic2websocket -C "tic2websocket-mkdocs-gh-pages" -N ""` + - `ssh-keyscan -H github.com > ../var/ssh/known_hosts` +- Ajouter la clé publique `../var/ssh/id_ed25519_tic2websocket.pub` sur GitHub (Settings → SSH and GPG keys) +- Déployer : `docker-compose run --rm mkdocs gh-deploy` diff --git a/docs/docker-compose.yml b/docs/docker-compose.yml index 30f3675..d3ab320 100644 --- a/docs/docker-compose.yml +++ b/docs/docker-compose.yml @@ -6,11 +6,24 @@ services: mkdocs: build: . - working_dir: /docs + # Mount the whole repository so `mkdocs gh-deploy` can find the git worktree (.git) + working_dir: /repo/docs + entrypoint: + - /bin/sh + - -c + - | + git config --global --add safe.directory /repo >/dev/null 2>&1 || true + exec mkdocs "$$@" + - -- + environment: + # SSH-based deployment: mount a key and use it for git pushes + - GIT_SSH_COMMAND=ssh -i /root/.ssh/id_ed25519_tic2websocket -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/root/.ssh/known_hosts volumes: - - ./:/docs + - ../:/repo + # SSH keys for deployment live in ../var/ssh (already ignored by .gitignore) + - ../var/ssh:/root/.ssh:ro ports: - "8000:8000" - command: serve --config-file mkdocs.yml --dev-addr 0.0.0.0:8000 + command: serve --config-file /repo/docs/mkdocs.yml --dev-addr 0.0.0.0:8000 depends_on: - plantuml From b12cb4a7494ac6a9649a9a0093e89e2cad16c2be Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:12:21 +0000 Subject: [PATCH 52/59] docs: Update prerequisites to specify Java version as 8 or higher --- docs/docs/en/getting-started/prerequisites.md | 2 +- docs/docs/fr/getting-started/prerequisites.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/en/getting-started/prerequisites.md b/docs/docs/en/getting-started/prerequisites.md index a6c5df5..5f3c47d 100644 --- a/docs/docs/en/getting-started/prerequisites.md +++ b/docs/docs/en/getting-started/prerequisites.md @@ -2,7 +2,7 @@ ## Environment -- Java 8 +- Java 8 or higher - Maven - Docker / Docker Compose (optional, for local documentation) diff --git a/docs/docs/fr/getting-started/prerequisites.md b/docs/docs/fr/getting-started/prerequisites.md index c870450..7f0a781 100644 --- a/docs/docs/fr/getting-started/prerequisites.md +++ b/docs/docs/fr/getting-started/prerequisites.md @@ -2,7 +2,7 @@ ## Environnement -- Java 8 +- Java 8 ou supérieur - Maven - Docker / Docker Compose (optionnel pour la doc locale) From d73a3572bd2f96d11c8927c4bc98b97b7bbc338b Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:15:36 +0000 Subject: [PATCH 53/59] docs: Modify launch step for clarity in getting started guides --- docs/docs/en/getting-started/index.md | 8 ++++---- docs/docs/fr/getting-started/index.md | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/docs/en/getting-started/index.md b/docs/docs/en/getting-started/index.md index c1f9197..6807631 100644 --- a/docs/docs/en/getting-started/index.md +++ b/docs/docs/en/getting-started/index.md @@ -4,7 +4,7 @@ This section explains how to get started quickly with TIC2WebSocket. ## Quick steps -1. Check the required hardware and software. [Prerequisites](prerequisites.md) -2. Install TIC2WebSocket by following the installation instructions. [Installation](installation.md) -3. Start the TIC2WebSocket server. [Launch](launch.md) -4. Use the WebSocket tester to send requests and receive TIC data. [WebSocket Tester](ws-tester.md) +1. Check the [required hardware and software](prerequisites.md) +2. Install TIC2WebSocket by following the [installation instructions](installation.md) +3. [Launch](launch.md) the TIC2WebSocket server. +4. Use the [WebSocket tester](ws-tester.md) to send requests and receive TIC data. diff --git a/docs/docs/fr/getting-started/index.md b/docs/docs/fr/getting-started/index.md index a00b150..d182b1e 100644 --- a/docs/docs/fr/getting-started/index.md +++ b/docs/docs/fr/getting-started/index.md @@ -3,7 +3,7 @@ Cette section explique comment démarrer rapidement avec TIC2WebSocket. ## Étapes rapides -1. Vérifiez les prérequis matériels et logiciels. [Prérequis](prerequisites.md) -2. Installez TIC2WebSocket en suivant les instructions d'installation. [Installation](installation.md) -3. Lancez le serveur TIC2WebSocket. [Lancement](launch.md) -4. Utilisez le testeur WebSocket pour envoyer des requêtes et recevoir des données TIC. [Testeur WebSocket](ws-tester.md) +1. Vérifiez les [prérequis matériels et logiciels](prerequisites.md) +2. Installez TIC2WebSocket en suivant les [instructions d'installation](installation.md) +3. [Lancez](launch.md) le serveur TIC2WebSocket. +4. Utilisez le [testeur WebSocket](ws-tester.md) pour envoyer des requêtes et recevoir des données TIC. From 87509b0806d114af12f7c4d75335b83273b93d49 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:53:03 +0000 Subject: [PATCH 54/59] Refactor code structure for improved readability and maintainability --- .../assets/sceenshots_ws-tester/connect.png | Bin 0 -> 231627 bytes .../sceenshots_ws-tester/disconnect.png | Bin 0 -> 361843 bytes .../assets/sceenshots_ws-tester/payload.png | Bin 0 -> 232037 bytes .../assets/sceenshots_ws-tester/preset.png | Bin 0 -> 232313 bytes .../docs/assets/sceenshots_ws-tester/send.png | Bin 0 -> 360006 bytes .../assets/sceenshots_ws-tester/url_port.png | Bin 0 -> 231701 bytes docs/docs/en/getting-started/ws-tester.md | 29 ++++++++++++++---- docs/docs/fr/getting-started/ws-tester.md | 29 ++++++++++++++---- 8 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 docs/docs/assets/sceenshots_ws-tester/connect.png create mode 100644 docs/docs/assets/sceenshots_ws-tester/disconnect.png create mode 100644 docs/docs/assets/sceenshots_ws-tester/payload.png create mode 100644 docs/docs/assets/sceenshots_ws-tester/preset.png create mode 100644 docs/docs/assets/sceenshots_ws-tester/send.png create mode 100644 docs/docs/assets/sceenshots_ws-tester/url_port.png diff --git a/docs/docs/assets/sceenshots_ws-tester/connect.png b/docs/docs/assets/sceenshots_ws-tester/connect.png new file mode 100644 index 0000000000000000000000000000000000000000..6a1d16e30aa2a35ad83d328f3d60b6b52be9e259 GIT binary patch literal 231627 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 VH;Xl~3qTb>N?BR4DX86$N0bzSHNk~z zU-dnjwVE;L&a?ElFY~wEqP%SD#j6UI+$n_iy{X)*3qs~wjNJC?Qv(v8&uH;u=~Nde zlVe>qch#Ucll5m&s$?6o*l-~4I6Iv z_&xVHj92<>?NRo%H`!IP#&1M*9l)R_8Vv~+x4FGxd^L=p=7q{UHzXLLAHRkCiHD*)p;8RBq!lE)j(QJEq z+vpuD#eg7RZW@I8M<@Y90?m`Z((}Xi>5Hl_+(#0G>CBnr9Gbi@-G_nu@URxeIW&Y@%gy~18NuJG(g~2A zuFq^Sy26j8)Q_Rul_q7mP-HtU;eE21%F83tSbu!z3iG>ocI8IC>HS&_&iXxiNtpDY z4TE^3viLT!VS1mudF}~FF#ClSOoM6~j0k%+GyUBA6=C?!$_hpsEHsSva ziVW8ir)uv|%rv#et08{rlYz7d!SbDcLyE_0U0iW7H(25Jx9%}Mmdj1VZF-%be?5N- z6rjq>%N>)lV4j?5E)*W6k;rJ+)DEzDOl-Nezl@HdlQQuS5TzEtTAq;JBlfpFyD*|Z zzK8iWp#@>NA^KU8g7Gr-?wNg`*fEe=k|*0%AQ4uQ3k_+1Fq^2Yl#EU7~JN z&xc9iru-Z(P&z%KeOt$Vq_X2@PF4y?Y?k$6yFsT=yVMyEE+ym!a}P7_*+v#d`jrTN zMnBmnpbo^x>H|ui+d2rr7PRogggy!Tu56>hH3FUoxVFD$^N$?OT6`bYLVsg*4cZSAnT}TK@>n_5X1@wi(m``?9;mMjnD~UkESJ|KevUKyw&_aEIFxl;AjBR#(9dALi46;PP zwI@M~;Bv$=Zx>+_(2Liap1dY}_f-4I*TsyuzTosyrt@sT_EJ%gLC3oI zR$UCJx67g~MJFw!u8XsZh)>cn7JiZ0xE{iCHqHorlgpfp21rumk|S@?w%Abv;ryJm z!DH8WpQCx7IZ9B<#tp|MI$H>&jJok-j#Jo-mu?aok`emLixIYRHmu!zGUFz}LXlwl zvfY#$FY5X8^C=H%FzNJeP47$JlfFk!Oz+=HETFh3p*lFikkf5E8o0YO!mqBmMMvLu{rDYq@# z7xdlz_#Pwbl~rkOZgyA)VRVrRy~IxYECz?#lDsjf>Xp0eTi;-eiYx(ll(eBn(@4$e+c>=V2)pFGL_rVPf_CeM1vh92dzE6CF z&lFMZX`J_S&}p5q|0?d%AzCsg5^bq2{o#V|o8$iK*$H*KJlrt6&F>*S4#$duy(6Ptwfh%X$|{J`CNR#hu;vzIwUy6_ zyv}}9Erl%v*&gaERp3tdMVKf8jhWuUaq@Qmp6Z`~GCS@s^;@^}Pk>BVSSTplWZ{6m zp)jO>feGgcw#=Cxhik-Bc2?1{{yjP2cv0-D)f|9QN|+$;1PZE3!L>IzRjmHJovo%^+%SQ;dIChj@z8$Ho+ND z=hd0|Rj&viuZeN3Qk(0+nI#5e(tL_pYK3p|DB^U>lLU1=u2WM}g9JQSx+Mho7A%1} zx8%us1j2I$iW>0+88OL~ovz+6ZT(|t7{FAc*nUu~Uoo3cNrs^l<%4pY47I||seriQ%5|A< zq9O|`Kj_tnPi6xpXKHr6;(R;+KWnarboP z#Sw+@7cUrYMZC_&@i`1@n4L@&|4gJTAyuW8mQ{h(U;bDpaI9W$ZtL@8CZ#QXV*BeH zR$KS}L_p?3o8wLMQ)7pCRzD#@P$YHI1)MXG!_B{4^Kl6<^vlc%jZg+Na#f?F>KSp* zlGWbIhrGdgtdtSYDI{-$#$P{|GVyg~uk7DRubmtq>HgM%D8^hLZ%vtRSvCr544YZ~ zIkUPj*ie%=|6tX!K7YG@(9LcR2nE!NyN#0lq-B>{26%$sOt|TZ1Vq|{{EZ>;>m0iY zV#3J-8l-8ZTjB?dcaB7}_CBUzQ%ue^U{S9E(1}XIF(@f{vf6UIc?S^|wWdVc9Z(V|KpHJ}!hqhD)3EO^_ z?J*Hh^-T;65MD9>A%yK`u>BuC?cPyRF?d~(3i~-N2Q)xqWkN1T_ROxMOXkap!nYt@ zPXOT>!Pw8y(Yaao{0~e*!VdPrQ>0@C)dldGl9Q8xDTt z_=H6UU!*Tz*J_5As@CSuQqQd+@37R9tt@B=wYIe`EH1+!B(&oDV|ZMaXH!r20RYN_ za9aH@kZydvtGwPZs9AY(umt&C!(mN>5%9rv>{>|JwTgf+GF)Vbo_hFwz9=78S$vZZ zQe}qdU~9zO1&F9%{k3x!NU(k}{4mB_F@elr#vWs583p2?zJ7+y4oid3)moq>yWMtL z4GUbk*)0knZg748?znHRxw-zES$TiNqG(Qc)tz}c8Kz)CY2;SJl~0j06w+8kX&Uj2 z{1V}P?gNRY-p2quO0dDHZ)J!OYy2^g2SSL(o1KaK@b7*$%{@TxJUwnWb%AzN2VVL(J83y`{^X#kCHV_t2m{)g=^nLyMAbR;Q6m{w6k)8Z`6l&}87do=GefzF`W zjs}UH?EP^xUR|Gb3!7mOIs)D*?u8q?Yv`$TI3W14rz+-sS0E8oG2od&o~KI4dfiZ{ zu~r1v0)iUn{_^pWtQd1=W78~{v9|S~6dZLVs$VUIdPJcg#ApFBun#szG7zzmB(Ym` z72#B5rML$l2kw-*tm+|`|6u$;V{Efv_c0hm6~o^lc|Vop3J)hXI_~Ie9ge$&LyJFt zTJT+YhA1e1P8Cc=Jn6oJHVNEgK0K0_CZ6DEC!B_(RA1lRRF(`;&b8#Ei@41hJps6aFl|2k~Qj=9ncy& z?M$6ZL!4z{UF`PKkOKfP5OIG1Ugc@zPa!ri$TK{o>p~3=CQLADd#5Kp`huJVn3sLl zU9FBn47U{f_h81Y2}!b(F5p>h-8vrt4|f_Uu*mKNNHfS#0rcJoDE08k5y2&>TPwcX zU${fc8PH0me}zkx19VkE909=BxbNR5-T-71P!*g8eu=vvzau_hNL&C6pN;2cSS{F_ zOc&IwOf7?|2_n3LbY1uNVA24tVFA|P#E!T65yMZ!OQL4Xi8S?OChHY6pD6{bCP3l^ zbkIO5JmG4=5)|YV^jAlhk}Z7y)z3l31Uir2nru4tQAx-RJ3hPA(lB)~T}Y=r{<13B zgsTz-rcjD+&cZ@)0wx`#>PVlyJ6tK2Lsvdc1(pDM$gw8HU^*nRTh+5(0G4V!UMmj2 zoN!=T+?(wTv~)BWtAaoSIW0MpnOXdhwYBR^Cu_T>Pn11>PQ%U7J=(E7VWv6V z2U!t&;%5X8c)tu(9+rmhggbs_8FJ!w^rZ`Jn>*v$UuHW6?RKWr&U8zWZFjGZV`f04 z^PP<}RVDrc#yS8AzeW-~oY`VsQ6I*6IvM9WpS)@S(_K&;njEA)Y4bT;Jz6344g|;^ z`VsBviR7B{=~~3mky*rFWQGfv0Kot@8?g+5?9Jwe9YGTDi?TYTcLgiA=N^n2+ud01J&LkNo{o zdIGjN*YJH#6;GP7B)Sv@PtK;Egek*WOJn%Z5f={w#kI&%R_yrYVM(mw3>JI5D1Il` zNSP6{Lv=Oc)KRoE!hdwoCeaNrO&{O1MF6U$qp`{#-4B^sDmTv<9ddKR!t?jMdPgEE zqaiCUrNG8f#$;8}Qwu1kH0WkEHCuIl1)N>f>0!Q>CNLV8P+?19{)nOl-` zUaQA0TaGl0NuPYwYyw;yp3CC4LMTJ*g zq-^X&mPCdSIul02k3=jOv~q`*;sc%+JVLs|BX7T6`tVheo*qPhq<~Th!U3KVJy7uO zA=Za5jcY?Nru7E{g&SaPhYkYxvL`TXs1~Wo*2se~aIwM1v(Tsj-5EpzWN@&Rv|&%? zm-VLPVX5SFp8heb&!(VHQ=sILA&*8t^#e0UPd+bna|^1A{1+OZv~L)T=;y23=>&!* z1CHf~l2^w@KhPkdf?d0YeHH?DLMB+I^?*Hts0)}nY`tF+X|sdotN{ozL8k|-FtC(#FE$15)Btn1F@Wo4@Q8b}j6V*o680h;fsrTQEuQGEGy>i9Poc1{tB$fr@Aa^Pq4JmAVu` z;EUilZ$~}a=Y4ko8)7-_6cHsMAzPpUzE8cYxD zT`;}_kQc!@m|7x3;%(dNl%;~Ya7#_yPJp>!zd=L2H|H#a)YXvqlI)f6rMJNKrf@-N zd?FQcJ|e5x{n=_q#oiyh`vCOQ^TNa>s%EyQ4%f9*RgJz!LjC{)7eGa&spY?hj@*34 zWTc6?Epa5-n?TYFf@QMxvRzz& zX@I(>t|Ir{zf`07vhTyO29?vpM~|>&RI$@vn5BAM?%9OyG6$d_(g1A${R*fOgzUGl zVX&R4!eXGkz7CcOFgNHQ9aV(EBt)%Fj+#&iK^I9#0Ch6IiSP@+01aed5Q#c{ea?)B zIv;n&YQ~#R_h?TM=g6mslO*u`bb7KhT(SqKLCm6w-d||u{CR-b2|LX7qEI2U2UH^V zL7=~$l0}CN$XK{mXlvEDtb*_(D8c3C@_-#OY&c24f7^}jmF2MDz|#X62CV(7l>VT~ z>gTcxakUb@bbk#&Z$akwO!^nqF8=nymdH!l8O=Ksr%_xjmX6mmb13Rb^?OY0UF+|B zB{LXx(gl7O+f8sq1|XHLJ`9U9)Se^#I0)vO;QmwOw4~9v*8JKLnbPtp!p?)~J+vx; zs|eyO&|61%p9neu!S-f$CGPDTNtzvYFC%oZF)(N_xnEfjlA9e5Eo2M+>h25B`;0WK z2iM6><SeTjn}RkBnmgbS~h%Q<}Oy zb5|KbpMWI8{ih%nCkVEMNar|0{Dce|inIIksS!05Y|0N&0`Pc3Xh0MIzm-|K&5f81 zLznsKJTD*LO4unrbGB6ozed-%HTxLnyQ=r`mL?75&By1W6+Ltp4T}Ue74Is~kemCMr3W8Tp zVa5mu*Ih5z+JHvV9UxzzGyq>p>9i--DCi&!c0)f)$bRN_dg4ba;8&qa%-PE92Glh{ zunWNfA?GMkNh1zMV9o~bMBuk>DJ_-uqDzXw@hrrCbAp&8qyM>a9{FswoepV6%WB#d znuzq<-7@v4v=8@97(5n6^((Y7SNT*!ewZ98U+axXvj|cr-cq0D=zGcW)ilb=^3p*lJM?^8o9UDbG6}Z}CL^vCnC~SmZPmnH5=q(Aj>|JbG-Ndywm;5bj^F1mT-Y zKtL97TyXrrQ}>2)y;7Et(A~(#jAwNJP6~Jd&T+;oKgek=NEUT7pO(EyEf!=>rsFf} z1m+<9BOtk@N$TOeYP!T$SY&kQ9Ds?%jput(FTp3ngmc4wp9-doOV9{_3Xxbn4U+#c zVR0}bm8S0);0Y+;mEh?p$+)9k`v^%>p_vf_z4B7f!h*2Y>M^!S^9}XwT5K=^&Vp>d z2owg9f&rAe*2LmaW8R_xX;Gas(1bAE1yroNIT7ST#=8Z{pOaC6=42pFRM5>4?0;#b zwFDjA0g^(){gTc{Y_YC3%OWZ68{qml_IW+M?UR{9?XI{>|JY!_>OPgQW4u|aTL6EL z*d1^nGn)RT|0e8E9p68y2qMKVdU(ycsjBB4iTuvMEW^>+5y2}NEr=KrzY&n`dszJG z`^lLS=gf7AaL|g-01ci1;{%%Uub%`KckO2jyT^tyReQ9SQvC;BMetFCOcc6)UNq%{r`Y%S{wo%E7~ zGv=!Lv*@!}aGM4bmF^H%)P7!gD7nb%uk?KGFlvdpCf~`+g6u7xE({Or>+9c7e%?LS z#Gvn&qoMt!%&M*qYB^bsKhs&>0CEC+$Ha&7ZrZ7{zObzB(>!8LxP-+ACbqJw z>hAMfuhUfXG?1J%PS=?nPzp;T5WxLpxuW1&EkF*Z4GP)dH@zbREzL9MjEIOnQ^$9% zeR{0p)<1;*A+0HJYB^}Cpx05?Z0x(6;sflC=D5 zxL&HFq7+jyGh+sHB2nw;?qYCvvC%D-nT_ZTjAvX(sDazW9jOq7=v9gOa!o+R2D3t# zs0LY&{QOquD-#mO7pwA$U6YK03WmY#sGI#jPu(2OY_CW6fUdyP`h6;K=Op)0ld)Q; zMJ1KeROsS5;bf$Pc-F+FLh(@rGLcNTDJNkevQ#6eMHIq7-1n9lWioX#NXEjHaJr*V zAskeU9boK5%BK0Qd3}mwl}~XR3Lpq-Sg0ZpITcUEk$10M+7QEAYzvwU9_H!g zX>|d+ps}FuJ{B0anegg!F0Fuf4WnB2LLn2HqD8t<$DqB4}7E6`AeeXX<%cpC?pGK z!Y_5a#cAci1jl7p-(-TaDvR4erTHDI?7;u~T&@AimrN48HUB2{&`i^oEj%9UK)pMUk(l~ zG;vW!m{;#AekJ2&cPnI?w;bFJB$!G9xr^UxFC>ZB6#RnIT)X{@y3G#2FA!wmY)#0l zoBOL|-+D#lB9M@ENikHP&>q|j^C_4H=bXR`3Pb?bf;qbG5bVkO8S-TQfW8F9iMafs zB^H+W0J(gPJGRO@>gofnLQU0j?=7Z+@_z9lEf7gvPn{_}JBo^9dbJ-$bX& zjmFkE_=d1cI@r&!$ML^_0;%Q~Cw{!7A5L-iyFNO31<`+Pwanb#o;t9h5E9nYDH~uH zzF2SC;4ojKX>%$#6Lvj!0b@tT5mQBJy`4p^-KG8nQGmo*s& zl11Qqm#(r-cyMH9rW4slSjR79pR12kQYC0EMDKi~7m>I2(up$6?iFz2^HLC9p3usK z0`v6yzN-Httr}MBXJ>29NlZ#oXj8D)jRRX8iBuS!RB(vYMZK?K3&XiYL2|>JKyxxX zthYTcHQQc3_r*(jdN^{7c`9vmn23%}M-Eqo+Pe?ycaOw+t+E;{*8 zG$Q+p=n5a2Q@Tl%T~m&y@2P7JZM{Cj7l4L2F1S{!8#a0qg9 z9iOqZkIP%=zRp>Sd7~5(rdc%_v(co&-J*T$Or5;nl6R^P1&RDEn|ARUi{wkZKb4a< zc|xmg-+1kXf=( z|78-dZugusL3Ye8zI~#zN4|xXid?jbj5dGF+?G4}va@jL7@Z?!ZKi#;=)AwhfVXDo)fLn^hVL@?`fY}@_XM;xPj?P2A4uJgy@WaYBGlKYtfWLOz}zV9 zZpwU9Pxfw#UZEiFR3Bf)0{(2)2f3TSzc^$#Jr;}w$kc4#x3i6JZ14bgNH+cByyT(q z*eSZ?r_&?J6J>S|j>gvX`54cJB=a=^+cjs8)#uak?)g-6cpuyXKdKZCq>X=(YBX-_i$c@WuuEQ++x-Zg4I~nwo1%4Hhbtlw0{)Wb)kP zJD2Hc?aRu(nl_@77?P(`aVs{aP_z@ry)INtKhL|gl9z5sUsbKsG$e4#~# zyur|%9>qQ5FFvPuvjl#=s0G>vsR8Qa+&n2QezLL0k53ou{@Z+2#PLe85)EUUL)lVX z>uO|pteA1Pe9OXCX!XPO%L}M3cahyF9We$>RCp(EP?%?kczKbgJ~V@zUG*Q`kfyve+{IS-d;h}kGXuBMqO9bIV>kPWWzLL6 z?YK{f8g|NTVdod9X(k(W~bQDw2tc^G5;i;hBG2Yd*p1DR(bqZueB>2VlJ*wcrWBF)IV ztJX)K|GR`ipYo!mvlO)~E56q5HVJgZpL~}sw6`Pfx2U-N(hu~uJ9(fL9 z|2{FHa4PbNXJbroO{T8ZKN%N^#@JqzNF4kgOM2hMuTan?X6>*P_?a(vxflIs;B4J}Jf2?caeVkLEjR!Xvp&A`-zTc_gFO@%OKwhD|Hoq9LTxi zbDNU!$f(|=Y9lk^YBk6|*=P7CV%c+N56M8dUqLsHYE^vfaFYW)NyzS@vEsk)n)A=Qc0Y=_n*i*giywacU>)yUOGn#$8zEM$KBW99F5dW{@Cl5di>K20VwRPte*TDimm@F?b&GaIcgI-SV)Ja z^h-v^8r|AmcuA^<%KyHc5F{UUa2&`8GoS zvqv1_9yDw}fS1wG)I8rr@Z=3ktKjb5zd>zC#q?u+6kV>bQ7TcO2Ig*Vd6 z0j0UmMiJtzCULs0i10)s*9@EfK2E+y2%=Shki?rLiy)qGNvJd4Jn#X>I_JsY9biwNB zqH#{2U&w+#?nvnu0_W+@_0GYVr!TkzOiSB=h_yCml%K&8Y<-mUZnk z473j?J4_4aI~4CWyvmn2F%|IeP82_B8S~~%Tt0nRGu-1-dTi@-*kx6I+NJZeZQSbg z!KgRygV9rsit*f#<30G3Dav;4;i`|h`C@IVwl5|ynOg<5%}OQ9KiS_R#sErr671vP zHy?IR4H$Yh=RHsUD_vrzlhQTvR0{_JeMsJ7EBt-WIAnastNZEAY;3}1)1^}8iR+I> zD@ScWjC4(A#D{Co%ll~e@adPO^f;`B@dz79i=%3Iz_UVr4$z2jtKsoKl2M%yj9)Tm zNOWSgnX3|6-n;nIhxdWkQJ2-uk$}fhNZqli0P3{Gxj6OI^l<#gPU48wX=2rIM8n8Q zMfI_1dG%?&O@lh=;d({&X;=C9=aw-aVws|klCoHFSiAb4OeM$vP19=O7INsquaY}g z94Xeu;+VZvCk}3GSUaoPl=ZLufAi)UYUFxj3$$+&kB|NiMG#l)*IA%Nd(E->69;gd z4Yt;u)PiAOM>?!LJALgb@$U!J5&QiCH9Z#Y5w~C{8>h7Czme3;LT#?wul~PU7Z;1u zX~~+_Lj9oMS!QI&EyQFP7#TYj_{jpL+F9RClYij;0{vT8ZpB+oKs?0iKuU~^RGxGvP@_Wz6Zl1#4BO(#IG)A~xrih2% zgqJY4n<_CovN)qpN2Zm-#~BOhE*#>AjlbPhxH%(gwPm&}Tk@Sy<_y>=nz_3tykRLC zd>DGA$KhSw+W58*`o!aC$-*#hV(oB;S>v4T`(AsRQVFB~oL>JoPqx4B$@gC0S=mjg z2tC?5vAtt&$3xZWA(JJJ@4wIgzfaQ*v48LWud7FDqAiN~!0T;MAB(EKh#pc+42~YD zm3?&BSK`pGEQxk_Of?wZ<{#cZQ6IUDq9%MC%j%sl1v1LHk!CnUA*< zuY$S5W$DwH-5S!eb3;BSi5xws`VW)sx>9<3Wplw!*V+0m2o5#fonZNN?bG^OhU*N> z4C!>sv|-*0UzaMZbbAgrAsedN6h&cGkjg;BGuD=kH4&OmDXr@ZlN?|Fcel{mQg)39 zRC<*jd%RA>mn5H&@wq6Z|AeiFCa$c#Pf3#4x$=ZLXNmmyp7^SOyUiSp{qA>b&%<{25)BF`45CMPV@2+8xuN=;HLliH2F49!N^MGC$l z9VCtRR~>AJM56Pr3x3{MuWKmM?&-R-zDPORHf*H-m}c+9J#FOMjsTj`LGl_+61wdN zEo|I#Pg)E;!?z8BhTKmbUb}m5)fAof9#G1Tmqs$b|H9}p7MB|M(Orrlw6LW6!oFuc zRr$dY=gy8>3emZbZmTVO=lm(xKUnnKl~dTa>$rk*NLtG#sf9*X%PILN$M>rgK`bq0 zT%2gh6JCSk0~4yP!rk=b&DdkQN2ALOsG6Tu6|^fpKkeFvP>;^f?i)UStBs{BONBv( zn}UYD?l*gWU>^6_Hh;rw)1L{O{@_+oZN86m#F&8lI1u^Ah%hJjSU1jAp6olMofp$z zmTj;Vd^sD=f+OUq>2@9Lo1@7#a@U9O$AlO6Uf1Mb#ret;52 zFWZN2BoCvACf3Ptqb8&`-s8qAppD{oC;3(hx5@WMWyH#x_(s|LCURsSbyq$L2*8JXuk;l}aZ zp2Q7?B|Kv7`+9BDwX_9+oNHR4_#Ly4Jt}Mtg>VB<_`8xNpG!+RKDqh|S{`zy1~xr> zOuYBzrw)E7Q^C!zmg+ChWiofPu_mEM5uKYgiuvi5cGrz#uBo@3pzw}$GH1^P%j=lD zwJV1)Ptv%yum2PHErVhzNO>FPjReWL-=o_g>!78!Uj$r8sy|*6L;*maQnjT zspuyh6D&z*1kb?kFj%2V5}Ovbvv{7BBGD9S1R%drBfFeC6)2kVN2(UH|-fv#ZN`da-*H@7RkQZ&>UW5Q=C2@I~;2XeBjHV`$8_|48fi*fGg zvyUs?>}1b>FXw^5QL|=7Nf+9&JASyG8f91)$!WPh_-Q#y=>Gv6MpX?8i)R+DDaCBS8Y;a<8tlolb z>62c28WBw`XW&wB6V<}@68WXth)<;@O^4kz8hhV#@i&HvvV`n%yE-gxeoKBmz!Ajy zai6+x-d_NHNi^d+ZUdj*JNr}8;ad%*hewA)1-6$(VxB%*bvfMDzvCl*M``drBNvAM zt}B(4@vDS_BFj}=R5|l^XOz?Toz@$*`(34cKM2SlyT#Fc>fqkHeK1X0P^GfK=c?}( z4*A?yc}E9|$I>_vo818Q{&3 zi)|`Iw55u4FY8)U;EJMS`_k?4YTjwz%3ttR)&0C4ke? zk+FzxAe`MpwXjp|D3a_HV?}w=Bpsvl;L`B6dK|cOF`k!G3s5a|o*vJe{x|8Yb%hL` z1Vmks%q-FAeuPNF-Myo~1IWL45@hEkw-x#8=VH%-!D=wz2EU~vY8}W@Er4bCza9is70nB-HS_Oxah2f zb=?O)N~<9=OzQ1v>W%uJD?2n%CX7#+g4oAM-xdUJQd>n6wTPRVU9V`E6&X7{so01W zROaSCbLQfs-9W-TiNapdEsz7Z5rfj6Zqudd%b9wI2UQlK1zo ze_Iw%CC)z#{N(D;bGoE0Fx9!{LGT1iN(D1G23Ypi`-h9j)++qnE}m(jJQ~bLtmt|d z={;lumEI+fj?KK~VD4dPV1b7|a;yP|vVXJ4y_Vp17qzB@=8%?-qZyh*L}w1q1GST9IUz(+>-4?hFS@SYs;h zFI=J|V!9&mct(Pu6J=cReAl|Ms3m2TyJFl9kM?N7x^~*S=9Sfxv|E!R&E@O0(pivx zP9?E5xZaYuOX*}`vz6g~C2{oB9`*B%PwBi*aRsdxC+1AuYIUfF8$`{SaW4?5c>_L4511!X5bad&m(*<^W3Kc99WrDbpkrN zr^A*wZw=x1NP2`)j*Txl-YQ6yH0%GCq-IR1a7O6^D6Q7GKRk;+ojeIeS} zW4M)Rwn^+T(YRSXznfJP@A~qfnInN(IidC3M}Z`VdoPI&+N?|McK2DFF%p zW6aeHwbwt)xJLP2`>>VGNZ#3bYot_k;+DjbfcFOTc)Qb3>|K$JeVl=!r{vDpV^i21 z<@+7?UX={m3}37M5|^tZ=1XfYqlznE8)+|JdCu!J26;QL{9#mCz z(Oi(ehk=Vf3BMgDg#U`F+BM%VPFK`U-6hX$bWhfD8jDBq`POo_eMz4kwUxS67yTP2(hlmu4gZ_-iM4ZJ*C<4!5!j)mY+KmLwXf~mWAUz z^>tV5c!ZQN%6*g)CLg=b^0;6assxm4r+l#zSlo7R+BM*6ctM+UiAo16sBzNwQ;5Og zjrfbKoLFs61aA}+6~k;da!W>bv;M7aNp062R7hvRcnP`GKMZ9_C!7<1$$zwgM~Nv|e*3C&c}CjHr#fPS3Vq`1y>e zS-Qh}wVZcLt~B?2qbr0pKU^fT?drvcRdQzHT*yJc-|SK4eyKY>3G03OP*?a z&5qb%?k9QeE~A_@!MU5NH)F$m1j9y*m9n}{zN~*ASB{fFov!RKq>a5k(`=^z5Gyj@ z)l;2~`Xi$!noTZ4rmKbjmz&e$?*U`V4`+m57&Vo2JTyN~XTBp7mXv*YKUC19NDz zWZHLv_BEV&R|kDELQ^judvJc`wifHYEOd9mDqvMp*lqe?^ar8TFx_&?`vgK}<@c5I zDQ2!~XOua`ITKJ{_u?|@3H0|{Me~Om%q}Oj>;!%-#T2~L{i?j2YP7y#{0oY6&-431+u~_c zV zT=|!6b*+Z63^~;Z-J7+?KeaxJ?@D}JCt~|Qh&l_XD7&a_8*~e(lwcq&Ee!o40wN`X zlr#d;9YZROq##|=A~A$CL-!B@(lPXabob1E=Ka3+``2=@T<8qU^E~IAz3;vEbyJsq zcBe9{!X-RI&&uTAn8A%p7*pZyI-}Tp5fNR+-F9|k^L9k|1>(Ekr4f8sGx!BhZQYIK z7fP6Y<^TR5Ru>M8;1$DulI?5V@6Sbx>8&NXy2;z)+lf;QrfkS91k%`x>Ph%??tQ(_ zSETH*fNq~XuDaa#C5H?e5Yf`qCruG`G<+aeBssYJXix22$B9B)TRZTHyh>%`Z2KgR ze4xP1!9eP+H$g! zs^?@m`uQX#C}Z?{{9VS$n1^2o+c@U_IG=10bw{V?jWSATo7Ix|*3R5CC4v760VRa! z@rPS6yN$!U?5Odz z2rdVXIMe$?;)1gVJ(4Q9HRvqOERpB7bE2J-*8z)z4-j4-KXfp=QJL^)%6HR{i3DG0 zJ(%&m{lKfFLw~yKtX;f;2rgwz6WVFc^NDTQV7*WUrqxvMM>N`}O;f(;ks+^&;_oxtcLMpkZkrx}o+6qA8tV?we}qV!C|5@JLjMfJocDNjzzhM4&kguE?Zn)8bZjc6oH z*!TZbJ3Tr%&;He_Zc4o8BzU_I5ysG}&bARs%X!cSa)WGi_d~D~V{QvB4mWoQ?#y@3 z#l~vmcY*EnhJ${8m|{1BcrM?`{ce`5KYD@~KF0F_ZFE&L9}QB`&icaSgLsF3zKL;$ ztkY{4@_EBob4b>bHr(Eh7^tXusAP%~w-EP6$Z@9Q-NiZRJ54*BI&*sa*mT@Ko(s`K z935$LP;pftep_CO^Li*j*c!Dutp)X)B6kf&EN;nzEs&Cq2E%KxF5iB(ZTvBbG=R8ow zhC=)QuOMl-6U>aUk@K=Cpls`E=Y-!==QRaCzlNU9J2g$ABh(uw!Y)}e0@Km*gcAE} zV^o)Y_lifVZAo%#1>4MCHG{oVqq01={V(6o!mC%1`ENtWR0a!YZ@+m4q#x-#HQ1Q( zVV$)VmA;Ux1zii#Z}F&N&qfYz4a(3&Hqj21u=R;#zlm=t>XVCCiEyhG zi>Cb`$XGwe4O)wYT|B28wCnb-x{{^83nnew*#77vu4xrD4-oRaJe)IZuG@!Bar0WF zZVr`<`!ST|>fFCoW=#9$+vl%Yqbj|J$K5uO@57@myWJczNbP`1c|Zemq4P%LXPyr4 zKJy9r?^@`k{NX&MLtVCe>awX@lVfj)Rf*9#;%d=B9?s&KKJ56P{HvkM`AYeFsa1WM~vj$!6Ci~ zyzYx2ZbRCx=w3y4J)s?KrbhdDFNZ0IdS3hQscSByX>BqoWRj@?9Q9Qj33zu^?9Q$b zd%wgD`6D)r*N+ovgMB|K@rmE(w)w@;SDcBoDa+L>4SIENSsxmeM6Cn^!+??ucn**& zbH9J28XqYVzpy3@FoIxIGf+n(+)qTTk?SJ*lHJl7!k&^O{y$JuqRGBbQ41oXrotar z>jM@^i5tep-#5=WUwh1*p+Sw|gkj=cm#vX4{1~?H9nj65y6-mkPi~ITh2+7!Jx#sS zXJ?uJk=HL7cg#htI{M>ToU7_r_r&DT%`=rAjmGeniNY?b5AZZ`#JX#} z%M5$GgIL#CZ{@d^d)hCQ)E@2X6i07VduhbcseOAx&6ub&JwW#R_OwQ967x@Ey%+z) z=XJ_f7|1Dp5% zz);JT4fl-L2utI&dGY5FZ^kEr$q_2#HQEG( za=9Db$_6{}Bjazsx6N{OKnEjeSaW2EwUCH~FqeJeDeF=%?4X9erq)dZ?FQp^)6Mm&GI;nnq!l4lK`fb+C&K+F7_}mvD=kEV3 zaCYk!6=euv(UukqUOjFcU_+E4rCN{pVF(iTpn7BU;yUc?CE%kN*H-92uT7^aK7}7v_hi5 z@Uu#Rz^v1gF0t(2rac5DS#c+2(y#PNhPPHAR3-rw^e-W|=OMb5^zR8bV#=qj)e5yj zb3(KCJ_NN-h6Z)swV`IwA}faLsl))!%+4C3ewaVRV)T89bIS;8=ci5OMJ`;C0FCf{CZjxNw7LUV@|) zn@^w?|0(hX*xA>zP!G6C9Oh%ZxO`xlKHTDKQM?%rRe za1CzLjY?9ZS1-OvL^1X^6y_Py>#T9tOMxg>n*rQ)J=R;m*8M_IYn>@ZrK8TA`Dpxbs(IC5&djl2v zIB8CEd2+a5{n&GBS|8Q2`Wz7Nh1CgPWO!a#w&nT@<1OD_Cx0Sz5hZ5IH*O@3WBi#X z;fIF0@85#?_CM5tYgb=B4`R-XOl$r@JNt#p&c0BI+eFrJR!|F_2k z$bMs1FA`P-XO#8S7W8x^Mz`Jen;72yZt&MEl7)k@C7Cw14l#My#Iwwd&${r|ktIrL z3NS<)x|}3F)LTJfN&<=#yAt4iRT^Yy0^1J@Clq@R(||kXjuZ*|mx{PK8W}*7S1_?L z3Tq4HLae*hgRQ8_?JRBSwNlY&D^xX4x3$4ZTZkUd#Z#ZrYefx9WcYd?Xq1akFbaHQ z&v%(4>gMh>ugk#`nWp%75wn;@<*AL`&&%D;Q4A8{e69?4f(dx9V|vLG?*#~F&zA|BzwIjs}KX)Fk2!rgI`(tzvnS+I_@O@8eKYt795dD!Rxd=R;!cheHCfY>!n; zJT0XY?oK}BLWUHRL=d6)0M%box?$9`Y6>)^e^Dt4YF!TZH<(_YK{h3-`dl=tK@eNj z+tilPY-9=?cQtjD!JmU{z{g)qH#N6PSIP#J7-z-_t8np3iS=HTa2 z8Y=-80%680an?^7e9yad$S@fW^~Rv8`0$WSUwFSfUSeZpoF?A=-xB4@7;G=ftwEWg zdzJV4TQj|djwX88s#(fm4^+EP*Y}YP@~~*6u;Z{Bxe^Sb_19$d)&~2m{lP%gXfXt_iC?Taee}oa z4`#(cP($4lBt}F#OnnaANM=~pI`eAYTfRD#@_Je`cg4c;H||OM?I!-`bfgtr%PYNc zSbqoLn22>o#-iL9eU8+y<|X^UzmH6YKfLhZEVKoZeogvA^=-V84!XCT6mh%S6$#FeG*hIH~vRYf#7jC zA&C%43!Rfz%o=UH?u)l2Mz@H^-xR(Ji%nt+D^&fXqpOoYx@jUK^Q{9St{u=`S=-n@ zazLc>b$M^4v}N(Kee!tSrNPCE-H}0H>f;foCQ&T2Ip~TT0;B?f!3f^iP>5xw4YmXI zT-nt_Kb`YW9V!&l3b8*_C|NXOKUtSbw4@>%BV{HuYOlV0O0^@cTRzb#ZSo`^`_g%r z{d3jJtfyrn#=moPk2PZbd6o7{6;il|^(17#SG$4oUoNrntLlQO78|bjdGp&lUj9?$ z6IHC8HTr&OrO+ZN)h|K6fqvFoF+M=cj>p<4qT56MlMZpRTrh#R$e7i003vaAcJ}_O zp1CyPyFGD+^*%%cN187~e(1x0{eYT}6q`Hjue$6VR@U#__>Xbo0B}y(Y}uNy(Sf&C zRe@9m_c!t-5)#CK92x&PhYv?>QYz#zM-@lBZV+#jj;+u}9=i~cmS~D3>Z!dc znE zJ!!p?C?=KvPG?Z`tiWaIo_QyluKqHfOuDvB9=+y%n8lQ0PkayNroOVx%}NnN^!j6%8OC_!tP8KgTCQmy=E*d_meX$ z7q1xk{#=Y`*_LxjJ$hr1=-$-)A)%BEwWh8RrRNxt?tj5do+Q(0VV1Z z+J_cNsSa{#RZiv3*)79S2M8Wil-Lef^H6duG!F_4LVJOS3)b-h;gwu1!=#o4CHR#54?2DZFm#Ngqw#7h7c_jp7O2)tM zMcE0P<<-3|8e>P*zW(X)eafb?_E!X7ur3_Tm^>Na*VIe_7ax$tbuua|D%$$~TG9F* zzimAICH6oxoe`==(akreE3%bIdMlglam3@FV}ZP1wDt9$@_^;?h@fxb2OH7!95-iC zQG2tEwvLXmvGM25PT9c>i4f;i2Y)r{lNtmML~Fo3%FM|`?B*TWA+<27ldb8l;|b*o zTl#(;?!6Lwp8zUW&7|a@RJ)(f3oT^X54%&oZ4?seREM4_|BR2|{rhGmJa_6=WWB{{ z2HkOejsZ_nihpu)X!1r3@7CLuaF3eTi<+TJj5F0ww`SD}45~W#nE7z1Jcwk#s@ttbs_J)7r?YaY|C)gx(;4iV(39Z1mD@l);7&G7PO5$$aNH(OR2+^u&)_>m zm_SmPFBk*)nUubsS8K3al1roUqI?m;%ZhN*-GAc8aeBD>FSM!p(7sJ2ne4F zHn`C?ZO$bg5;yOef`b%&L+Yq~cd5j_Ap@VOHaq~|{cs2)C58ALz$-x*QkwQu#m(^) zuDbZDZ}Ipd;9>l2miTT2l~GBjhbqn5_2RK7vV8G4rQ{yiWBZ4OoB@huKRDIS1Y>qN zRp-O1KB_L5s3qdQ!*CyLKo4>xDeBVo>V zb~mYl1^<$!eU4*QalpusJ8iA=sSFM5uRn=CDDm1c-`UdfhnSdq20Moni=V9LF!-E6 z!ikB+eU1poCC(`1GbL0C=JElKdF5rgO3MDxqJ_u8Vu;6jYY4=9g|iH!}*z>VukM6wr@T`s3tQ%C$ z427kieg;DO!I|mluoWzpc_uWQD4e*cpQnHLqXxvmh}C`8_qmnVO zg4?ia8R|bRE2|}t&wwUy4e)Wd;XP`)!K-cBdbwBzg^ZKJ?oUS0BOO8u3lCNk12m(^ zU>1h4;I0pt0n+IZdW#1%Z6FsOAdl^QPwaiZ&Db|b0wPobCk)bab;STC?TC@bgS#eX zuE{X(?V?=h;0*FahHCn`=M`-j;e3X zHr=MzPICu4GiYXzX7o5So<9KyNHDWTftR3Opn4Fdil%Ko-o;4x?R7Ju24P6q1hdBd z1mO`qeKWTiE>@L{m1gw(E->mn^jjh}yt4L`9lR!&?0{W&HRnxP0kEjn-b!5bVR^Jq z1QH$HJh1D#<$iu-A&L02MKV*fYlIqPXSf$#5c~2|!JH-45o)-ef)wVyc>m|(@&9UB zufg{7Z`Hryk4%AQd+e(Ifn9X-rTct4uj&Oi+rb34I znILL#pxnZ+1GAZlxy9sxA;Bm6C${BC5bz?&tl|~cP-|t??W*WMVs*#kft)Ay7Ts=$ zR!-n5WQ-c5D^MMDK3V7U-Md5rUq&Y9u}3m`gi~DFp?19kqkWGa==sT{|E4Py`B1mtWm!BokgRS2 zrV|>1!bJaUmU-N(Pfr^1+c#hs)3RBoW+e;{4^I0+zA!!4Z2xLSk}Cvbm>u zLQZ$|eu=$bsznr|G1M$b-9n|Zw)RztnuWPJ9(@qj*3J98$Z!$F_h4{g4TZ9-nPGNp z1LNRWGjCtt4CW{>Ra{is=mezjhb{nXa(!ORy=4-tas#SKp{;7<)$uilQp{~9Da+t9 zvmLL1fb#UF!%4qRl$2NO@(^R|wEw1E?U(m<>5E@(^YR%_RtkTZsa6mV=CZE9c9*$d z<7hFE6pr0mnk;i>-I~Lf=9T5q&x*yF-A{NA^5`VnneRe3rQaA^O)n7j2zVUqo8}hc zGnpuB3~sEDj?<*^l;!)CHY#PQ#|DBcmLP>*mNJ7@^&-y}nO3JaOD&*v3K{p6g#I?7 zkA?T5c0I+a>(P=dPLcrt&)3GPkmdwJLcVtU@l{5kM~0FE)Q|hoz#5rr&G6D8i&*E-ssg zDp^&3@3=y--Ukb<_*k0q<>_v>NOu8d#&1~)lZfe;iI!|juC%Z`uD=@2yk;jCKjN)v z+K8NWS>8O)0Sd zYU5saRNA8`)A{4M8pmtbH!FKgzMBTK3K!J^N@Sf&z}Vg$GBR7pM6Ft6tcr{HO1ufIU0jzYJ9ww2j!l#OSG#|EEuIF zBgLb@!4k7Tl0+IX4EWhq34+RwgLiOlVogavYrA!6T`-{N!5cUAcTKDb-?)mM`_&D!0$R<&u>5(4Clw$)ySss zl%py*CT4jNTVx&!SpwpFEu(3Yb9rw+OL*lcrICbXA5`fOTMU&t?7~m?)^iG8sZZ8X zkulD8z6&|2IIZ9h^f~AiY1%u79WLBPet;HUF_z&)94Gmfeb|~L$7r?G2_#*DakmOE z$ponTiaEpIhd=T@e-3)xv9xUPYX_(l$N-?~@We|z zALBUwZ#o-l3r2h1S5%C2OvhN|4USMU-<_n{3J% zd_JMy)dVWYv>+f=X}XAq?d=7S@Aog>eL?KyzMG|4@;36(W@}w#2F&{$;e8%%xY)lj znydfg<(n1#$8n~g1PT_5K!726d_Qv~!>d`ry2@KJk!f-uYCG=8(_w1f()aHZss2mk zDZJy}5)Ih{(UvUh(k@+y6Afb{hHvNM@7z@N?kxjIAvJ1HsjA>I1t9hIy2uJHdZ7tS_@!{;A}KhwcA75j z7@;e74#X|M(A%mw|KRXP!F9I=P*}iH8bAjtmX>|6CT54Kp0P9awfqX!ewCWVM5(^g zz+f4$^(hs1d5*PiSg-8X(bhJ~d*`-<@7S?3*D&RN?(4Qa-PwdVtpt1h)VVWgB?1m0 z3Y4X_e_vxE-DSH6*ITo7ak-k$irBL&oL0+OGZ(;d>b)55H&AZc4RJjoK_Cz><8#y0 z3z$r{!GDE&ogM(@@7J2<>usPBR{;yZ#Qz!B1TJLn?cwpdC30%i?Qcqlf9M}^bPb5; z4Y!{&!Us9FHN!hQ%`!y1zsWO+^c7M$T<8o9KA-|;r8Q7Q>{o&}$sNbC5J?#oE}{-& zV+~1jLLkl-5Wy95SLWqQrU*2{?K&W0@V!uD^j>F-V(^KzgNYYpPcA7ql8um%NnO(* zuhz7nYdvwb;@!}77d=150<_`xBk~E}AJ2y*1Q*?3G|Y90$XDs&8UZ=k047f@E0}7V zpU+}hrF-NFH}2Fe8fz1g&(mB6*f;V0l{nDK?{_}AySE?hm*#gmgUJBgiGM*Gf0?~b zx0kW@$TjmPEZDn;z8cM3I@@2iL6!$FD|}Q^GK$E0TlDXRI&D0?tXmAG2d?EBo6kY& zW-*v%oHxc*q;*&oZn#Et<*FX5pS&`g%{X$_LJvYGzAK+^~BGJ;XjBI2RtIG8J%ihNO0kqy3z`D1% zG>Xigt1!qUv@b_XslX;o!BdJ=EScH68Tt;XhrS<91FHd6EU_?J;)Eui+W$A8jr zl#~JfY4J}X6$m;38g|HGLf<(}(x<+8*XcTrRdr;;1!Q_Cu*YLT@L=GTnk7yAW`ld{ z-`C-~RnkRckCjIk_xZXgf*(m!H0EY?tfP4}Uew<~F@QPL*Lg28P#`DFW}_p!b^(`n z@j9RzNGAZ0RWND{)@0-AGyBCZdV6LC$_(flEmj-O0G|rP`t*`7Vzp^@*QwH!M@X#}KgEJ)^ux+WPKCyl2qtrEWb4!Sizk47K zHg}MO2amn4tgI|9EhRG?otr}h@IIWsw#7Py7g?$mEWfa%k?F6d_lm=%j5M(|Ss z%Ziy0{DP*yWmB_AxnT5>0wo~ZL7~YH*+LYy+!|<1lQIo+b)U(Eme5GS@G^EU>2YoX zB?+uY;P+|e6fDtj8rR{v)3AH9X)maG4PodR6K^&Nw@v}xO-F{rz&vaFp4rFA&Hb}w zXL0mdT>n6N-tdn*vN2|>=d`Skh{IQB4afbRMdDv8QM4?X&}!&fgZ@$Q87L>62zr29)}!Wu+;;#lHgS^-~}v&U!3I&H_HoEWmc(z&<|BXW`8YRGDWu z7DE|%1~3@Cxg!)xd$GIv-SfxW!kBz^kgf>Qk%D_W%9K4w+06&~aBo*aqOcRiT{Pg5 zVK+;qZ|`&ih)P44e3Py(PnW?AfK2J6A1=5jE~AX#43jo>^2hF9r;R9ytp;x?xcKri z69xj(VZXLr5Z!gkJ?he}C}wM)C?kRWHwX5z3m{aUm2q+XDr6Rz{N84&D&AX8RkrI? zd%X>_Ks6D>am6YLLN^->hWs#xyDQz22RBc)=6ZWaEy5@#t4T{mU1Ba?l3F-%R0Y}* zS*&R~eCU7n&(6cpTb??JcGHg|Zaa~q?!C7`){!!4-%B*EntJ+k9BeOY3UtMS?3{cf zjcLZcZNNXNmFM~Zc7VVDL(J3G5E1kiA=oAFsAPC2jJ=zzrn0Nuw>mM9M3hRzHD2r6 zFUdhGsr{QJ)f41lZhSVustT~;|0+? z@b(54j9YUJiQpk87wb3_XN@uia%#A96nLEewfw|}o7rp-*T2-*JEd6!YSYr6*g4~Q zK_J(=Jaho4&lE%IshKxe#n45o}*x zwS_V(ga%OTN_-_vvQeU-Q>2#t!#T!J^Xq0@T%6K4;v9zV!k6lZ6rS(d(xB}N?0Q?G z8D`T`DIYD}ybR{D!z?nO!RiGnix)D)`Ie$2-8ZbNhya=kIpKD>T6$D=^j5woc|_UR z$%;ONOdkQ%zt+`6dke5`oxuZ@s$7vxqS0Plc6gtLRh=KfJtDG8FOOz#5)$&>QveHiArd94Y5*VM@= zHaR(&-(f@RBH5SiB1h`L7`-8NU2x?tbvz2@Y;0$p5YK=F6Tow|YIuO<4S@Wj9eL^H z7tMs(i>?5hNkU=*Al>zb5iUXUH4Cvn*t{U0?MpUET=nkvb^MyFa!?Vu)pzc1o?if% zUR*rS_J7Ne-_b^ZJYB?hg$*kjAozbO1cA2zZXP{tvm(It2Qt(iEzy5nvJR>wXt)JKjbg4b;g0HCHziPi>N-|?^-bMcZ&mSydYX6RLZQSpj z>e>1b1_wAkpb41Hr%n%Zq#U0K&+C!FCpJWzPR2ljpfKrv z1HCrn8<>e{km@G)JCo7#x@ImIYzv5MiwOH@@zhtf>k zH&$|`B>2fC+>i!wCbKX4vuPIpr?TK)?IjvtM>|*;xP2fo0aAkv^M^N^cJYxzS(|o` ze~z=0h+lK%lFc8S(0lCS?=Hyu{`ES$Y(C4miezdCn9{#?U_fv>>rReDpVXWKPzAu^b|)n#8=vmX z;aq2(g6WAVu8;#=J-wAFi_BGEPXgc@{?4eu@`{QVYSe!?-{rH=+hnGjTRLXIe9yzZ z_hHQ~HO=R)86H<^Yp6k9F@esV^qKOm!zZpC)I@`lh{(C=!o3?2&>wZ@vn(GG*{;%(pjY zt|B?u?}Ifl19%zd(mNiT0DkSR6#EawKVN4Kx+8cJyOMnWNj>eJ^MZyrw%9r4X4n+|QnYOYiR-RRnANBBNy>JFa-Xrf-PC zI^g*2-}h@3Zr&$VVf4GVoTXL*>~ea6ciP!3&?j%7B2{T{G?oMH3T_AG%-IZER`1Qn znTgD)goIh)hG)U2L9s0kB?3(u-geQ}*U6&noOK!<*qWj`00@YMS>4jNfU&-v+gP&D$uu_Ct?`nQ;vj6+aS%lF}sqDRHE=G_bJ5Eq*RIO2j!w zRP=29=wabjdu6)k@xyc$u8e^AJR;=9$*A|vVL|y#?}dQfRej_oW4^^Ar=gGk<*rWx zD>=^|Yi4aY%*Q9$=Q7^|6EZ(EbhRzK{0!8XeZt%2RfetZdT*Dq$dcK6b(76soYYVR zt(Lh2{7vE|32D!UQ*xw(jvmXD5xW|!aD~OaoMSDbfXwi0rw+x4^~fU?o^9#xCxIJ~ zo05PN2V_swve1@57SZV7SY!UrnbvT4^ zt!Ff(oDnRz8)J~gFNjm;<5o^bXxG&+uKfqhTzuyf0l}KsS0*&QuGx_n751(AG6%D`K3tHwhyrNW< z*k^X6sIWCpfDl;z`P&F0%DT0`GUCU7)Wy$N;y@UMXG#KsPB3s%rE7+4@&P$17DER@ z(R39H2u!^yC5CzR%}1VswveRn{(OXpq6ZnC7i`gz)qPi#8k-J>%J`FYUib)3hm}{+ zteKDj)97^Y5ycp!@YLfR_7>u|=JdP8v>_8YV+S8BU!^1*7D5f5sG1&#=a2fU;EO&3 z<~quZz8=YAEL(H;owlnk5m{uZ4m%9R)!!38vvyD|ka$?{buF@$wIb*tH`^aQg27L8 zu@2%6*T)lw;1JpCRrQU<`V)yC{g%NVdRHr|=no>o`Zb7-4xB_oUlE{XTUGVp`|e#g zo-5Zl?n<2QH8dac?VO*?fgr}*Z0DnfB55G z-H>;J)Wumh^ysU6?RB;Ap(ztu%}OEY_9YK+#p)k&>RiP?3&31>U8NdgTyK#G!_jgo z5?=m*SNi!jzQ=@EQ~2$~Z4|KaTET|6?!L!GjmIZqXTL)vJ5Hu-SIhj>T>(`GWaaHy zH~uf~luK4g0K-5G`fUb1jrV;&P{#c)e_(v=!cr&|xYk){N2*cdBDC~`_1uk4&f->a z$*CEOvo^)K1wrqHL}&9^o+?$caIA#@p5_u$lqHaWgZjtJFjbixKIDKrLW(9%H3ciR@1nz%aZ>h-@>Z6l-qG- zeN_D5xA9DDw`Y&?^)-Gb6fnMDm~UXpng8Lm&|(IsbUH%CLK2*)tHdZu%a=?1|3F71HT02}pP z7OR{1WV#@2KIY=CE>u-1pSC#&le(sct8~09>3e1`-jixqAJk&I9M-LRSQ3)4u+_!^ zPsq`<1)1WuL28>>hr$ctXQ&^1K~EG!sW*Uj#dWx$dacOa%s_d2S>TjwOG?m<*rf?A z>$W?uoM84FW<=MYQ+Wr8v`o{0xL!~fsHA64Y~JY_-a9mjFdjOY>+kOeY)sJJ7Zil6 zTQXi|`T%R66jSLefOJpF@%zy>>P=?2Pa4RsHYQgJMWZ*NY)oykEKqEawsX7W?o zx@w@3HF{Lnc66uuN5{L;+JlFVNxaV`I_v73fdpa+H2Xv*$UkswpG1a)Z<>I`kke{w zfYMOE1E2@R6{t84WXS+)E0a&4WFrpJyRB(>>L#oq*u1EHcB(Q>(mD9o#FHM9g3b&EbTh=ck9WDm$DDR9AcUxAicU zZ@;}WpwZNl01K6wYyEhwyqI5%zS8{^Cr+Kv*yy9X*h+H2Opt{tSH-VcGZQELOYRw& zoP_0#x3E80!T8VurLAqS^gA8|0z;AxQ82&5nX|_4Z()vyk@hWtH@l-5fOs>yH7SW` z^6^SaDJ`GX`#xu9+#U)k{hFU(L6(_68o-51|5BT{PSuh7X#^MdX`oOWD&yfEH8h%c zFtlAr>S7t${TYFfiX?FSXV~u=&zm4$JgQ5VOv)z-2y!I1k8BnAP^fEJy!^K>P{u+9X9EqbH~EJ ze{{K|6nUSQ!W>u5_?&hD`}MtjQxI5l!4K3&XWfT0#tp% z`@ypHDtWYjP-#)9mo#;NMIrVG#US88KA-6938#z57^Uz1|6r^QV>VkQk^XgOA9UJH z2S{J$1jy8j8B(hL2a9EaJ{uzVpTOj+V(UW9&aa;2)YP2ek1;5bp#LHIu8JSYHXGI> zcLmlb81$HE;F6xuz02$MlbkLXV*Fo1)bZ~^eT_1f{0!}4!^hiuw2ZBkx8Wwzup3h*x$lb$Ah z6&q>?>s`@^S8=~fB>&Vh#_dULZrfYuC}xFqHz%t|(YYN;xJwY|l`}~F{&g#N?C+uv z*gY%7&yV78s{3qA$%~4dwY^d}$#zU!pXMd5X|ViFlN1=LPQK>iPRr7@U5P5~aXA45%{H zvLdjzG{Nm6U>dCes3NFy2g$_VUMI$Uw*m!*1ZUZsxz~#|ctf06A%+pqyY;5GPk2dy z`hqnUGOtuF8Grb1m$Mo*ctg-&1U4+-9{N8}JW!!vBLa!)WRs5=Kp3WRMnh}f4~|jG z0)4EwD%bKgLpS(Bfy1oezdHoK+x_T}Sn&@ltG2dohu6^77MCN%Z^cR{bRlmC3KxXj zN=HSyji=zXZM?Y?nlU?F%#k+Z|8>!i#NZHB7^^9LaM0xRn0`(~3?U#Pz2v%N- zU?ZDWD~m7)8R2qh3(GGtM(dx{+z*FZa1tK!=9bE+9sTIIBYScXtYW+lBzKy)H|a^{ zmQ{*1hGrk0y~JCBJR~3$s_0PYC|74)U&Bq@oeQxn_`~U{scSo(&Mo~GFCiZCIP6x1 zN{DXB3nN6VG|ZJ)`qx%vT47SG#Jmj)qP zcM&yutP5w1*A=mtdP#Yw`%wXEkn-iDu`oQ)f~6$b_p`OLa@8TJ`J|}1uR!vc-L7f% z9zTN@_e}Tw=7V>!VeG!!$SYATgQiTYr;m?<&8>qxt6J;MWm#AYdWeU19td#3TpsfL zV16{l1`-T_TRdEN0cxdZK^*@}*8Msm3g9fI#HXR5HeI`&he7@s zpOdvV3tsEP*(n2%^KDc&P{339+P*2cY_;W?%%&C0uJ?&u;d(i;51?rw<74h2O^RPC{Vp|GX z*X&xAzt$^Czn__?@D#tvOvxKb3j8i-YC^8o7&mGmKDR>xpC?nv{3N%{;W98P@LaQK%=J*K(@}{6=hyq950>`>+7}48US{RZSgOI^t-H?T!pWfH zIX&uNbSLW8h(RwBk+oEUH>-gWg6$W4Sjrf#= zSY9oK+BL|0U41G2in4R%+W36L4wlBsK>%Jajx)Xl2g{R$6Rx}M1Zit6CvK1Ngh%lO zYZg8Cwukd@P8w+DKP(I&tUo%qXZdx}I}kxN@=NZka$JErjxRpkAW~)K?O76}Fn|v< z>ZK_ydB=-KD{xc_(uon~u#X5b^O#KAMw8O5HmX!hS8izAiZ;S1gF#Isd(-&Mu7Uc?f^@=IndSHwp`MuHfX`4o%gn|3^=K=(2*|jw$aFdadi`dh%C** zGUslxJ;qIAE2}Rp+M*=~-o+XmX0>VgxpVx~rDIlD(Te#?FskKmAJKSX<^<%+k9S4w z6QGw1;W}Y}0Nr#xjXSO|tUIgsCOvXRAoD`dk>L>7_I|^fzyj^OI0Q?cSNYLJm}pp? z2^<)`7uQevzR!q+AeU%em2e$k+ZF9+6j^SNsHoIoZP2zAoRDq5zVQ9`A^RQh+@&~i01?i$J1#Xm`alb@G zMg_cn2)o6a6DlHTol(=p)%8i=iSnhX2Vyk}%8l<*{rRC`5xQjQ|9$Y!AMT%Hv^Eq} zfrM3EBx4uMRb7HzFS$xw9ADxrGJ}`1s;1|RJ}Z+BavM@BQU0yyJoxMxCG)$b3!wxp zB2k@}JDhTZie|cL}AUz^c*AA_3N zZ?g#6!J0S?lUU}@!)4vy{DR)t{{i|&KD(^JV9~o0O~PA>4RgReVdSGApn$GAvgy$> z-pv5zlCS21R<11cRN8%3Wq`W+^h<9&{D6j~msf-2;?PaUI&9fv>tpjD8=cjafQmc* zHBVg0qt^Ov!G9@G`~&83NK6>`#>c}r1afr(BU!?h6|z70vz6ZIyep17XZJe6t3Ex# z?r*ES;EcpzM!H4Sn3BaNb#LzGDpkgD$^Ez=I4@G!#Wfh&cFsrb{p;oJT)52SNToGjR zFX3{rFIQ0m(CH#&(Gu>sV+C^-79AZuY-Z5ShwY7)e$_CQH(_}v#Eeay+AL46NL}-2 zpkL|hDto$0xbD-eF)KjkK)igl4X73Lq4+{+HMUmb|BYRy>w%ntAHkFUq|)bMY~g$ilTpbJUU5BL8_>+~ag zp7*DS3*DwZdX!>$Y0pSu@_wc9G(-DwC}2xqzv`^%OZgQo_t4UGjdq3e0>O_ zafCjEUeB3v6Y;sZ@zjVYV&5!8z=jtS*_}kq7QFe*UH5zO<9!!I_sQ@pxoN|Rl~q%Z zy;8r-{p)UaSYmW0InzwTcfU+dvDyH%^RZf;p_E7Dy8{($mDezCmsUOOR6lT}-Lyg8EN6FaR--3{B_gk@7I|F* zeu7~uQ3ku+IuCWXS#r?>dD2^DCA8kR4Au|gjXAqJMQV&umkZU|Y#>(t!Gs=0<} zn_73?DD&lIn7$ZNEZZrYS`;NgMn^^$3AhDZ0&M5B^M{qgvagHQv}qwvy8Y4BtD>9H z-3xpZjON?>!lLAW_#2iTy0Njb_&c?Wrfsh-WYsH=28(hdg|YrGdlHNH9N+Uy%mCp+ zkF~#W%>9_P7?cs8Hj&=^_Qbuw1sQR?=uY}*Vchv|5Bj^jZtW6!s|EA3aEi4ltC3|r zA9=P>hH@=Vcra33oJJ>TgR$;Te6S-)d-;Q`I*gB-mM|B@4=>B&2c!3^z0GK(Sa?=y_0~g1%%%GAPRW1f0oRXI zUZPhVcLpKeevDQ8D`oUuY;dG6zAcUM4i7<{U5t~5xzn$;@PeXCv^RBiQe2r1+JN<#65_bZ+oPTFe5?a^#9IhXL352994N}3Auy2 zwHleJj+ud`M@F5>VQ&2AFY#4zhO_8Yt7+N)M)6G~kIlXxv+h?()r>8@>9qD}XA*bUxAsKZ`|#Se+r`9X z+;p^EcExo+kQbXLV(SQ%3oQJE$##F*?7p$yZ+q)#OK(hljhSoQ_R)KtjTNO~@_B}M zDp+m>yvS$_6_=>Ow&{iTn?G7rGLUscuwt^Y@7JU@n#~CfL$?8C)t{UlPPWVf+PO5 zRLu4r*+<5LDfV^zYcx2$Ijs|~%nJB|9}@6(rD?4`y`Q;Dt`7NlB-2711p9C3z-a?Z zAiu9o@Jz!b^u@IjkWYWg7tU%>wy_kd&w<-o#PDCgl6`}64+Qk{8l`rM*=p{FCoFz!qns4{=K7-P45GuRtN z${`~3SIMu`!Qlw*e0}&^B=nXljr9K`>`kDd?%V(Iv9H;;kfpL`i9&YLVo6BIK1oH^ zWZ(Bx(p@B#B|?%d*|#whib_(J24l#Un2~Mlzw6U|-_P@W&-tJ8?>ISXFrW8wEwAf! zy{_zPrAvd6QSc{PalV==Q|s z1;VT3#e{YN6P|bh=XWb5i<0Le23USdFqM@DRlO$ z@N9=X4V{xUZ<=SXO6pusd#WHQ{q7c9Nblu=1up^nPsFA01oTb+$*pss3VuwLIlY4> zbuoZ`Erc}dHYtqOH#dWs>JJ-(XtYHktVu^b$^yf=MLs{L^10KOM^yN>g2Xx+LHcJ`ZC$1k> ziX5ISP>Z~Cil!*VplDiq`Psk3m?jUI8=)yCVOh%3i<4w0{AD4Bz#+47F=$DVO~R1NY4YjGGTo%xb3N0R3de;xIqtl&p+9r?cZ1ZSYq@uD>;|v+E8?$@N?*Ay zM(Pp&=KQ$rOUj4ClSz2i;yHA=nDKwp;r!Nu*set9w>9Ij}YPR~ilO-7^Tl3*G zEe0I@&vLyhFac2$KR2moxJkNxOw6{p+HghAJf7Rv#J2D4W41@HTJ>qm=)Ji}QocIa zbAi$2&WSkRF79$i(F+=`(ywq&#!noDfw3~jr3cDKeQTm}X4&bk-rCuoY`8WziF-!w zf2q5%S#@IQ4ym@ywamGkM=vT;&|&C~Nf(-_xxn|B-%JeK=gWkh3*Id&b{;EB2TG=u zb(*r9ls$RWQ z5|1!Q38uUHZZSOTn|W$Rv%&W745j)T#Wo8^KXRR5ayk5img?6C%Yg9rj!#+H>UR4p zOl%ts^carcUEy=!y+zNnM@w8_AjkN9s(kl7`mSe9{rz$oaT-}mNi(4>AIB>vTfp-S z*A_+Tj(wzrPR?&DkXoQEJxXFmakDnHbo$I*lr6uj!(Ocm8EO9C>QJ{2url9&dEi#V z+w`>Nu%f!DhV3IBZs{FM+Ekr#PXHg2>nS9q`Wfn5CAKJZqRw$EshlgiADnyps7%Lm z9C782sFlWv?U2k4xxccb?1jpF%#FU4Hz>Py@7%j@zMO%p^6+Ye5n&5bqWH7x!Stz% z$NJN8jN^^*G5Mo6hbmp~dnn(`*dU0y(iasUE)*S2?+oX-Dt#k#-BJ3+$nx;Na$JAe z$ggxYcU%yPsWr6Fd&vK)25814;wB-8#o|a(Wf4;x-FU?b&)2W}H23&r$%d#YOwTdy zle{rFh(3^-vQK}Ui7T~gwqjjOR255YkXk9;agg7$-&$diS|~21%}S|;TD4$-|2?OHdvL-j209QBCDn^+bVI;J^e z$~lGvAI9$w;0ZAk2p*TleODbVhq=V(!YMcsagWbcm*_Wm!6;3IcVRtm-@dKju9dzO zz)Ke=sVF}^>|fKn_Dq`j%Mpq0u9q0~yGHkaG!ffl#l#q?UxBKkSkN&e+p-Dh=$JS2 z4#DlQay{F7O@FlAGTI+nC?E0#i*nF2g)B(FW>ItYZJQDnxT3Vp6SU-($x)tT=QSTW zx2MCN`VgOqUi2d0oY(Fu%0%y~L02{FiO~Y<>VuV?yk0e4CD%9S)kb$u>!VON%Z*^C zRaAH_)w?d$l(RnCjI+P{Lm&?09vH|+(w+|#+URVMzO^)3r7vVP-gN(uK>M-pV<8BK zD?^tXxT`?$L#Sp1dLb@ohY&BLK1?^}L(zkYm) z_Zk`oJ}zXwGZm|(r>@+%{z;2MopdEW2POO$ii>deaS_8XE>%>wCC6Df=w02?)(g>q zmW($?mGi7Q(Mdn5mU%C6{3;QpW$wx_*N?_zn7^+O`ziOXf{OkUO0O%cCCxl0*6l)t zsnA__(y~0m-C4}*E(_fa{P0Na8HO+H9B;d2c|Xh$V`OJA^FDba7drZsVzqt9OYd5( zPeYT)*k@tRVbNWCok{(ubLV9D74vc->0?|5@uk_q_Rp+T#SI0frrxd{`N=-XukBPc(m_k!zG=@biB7U|!P;wsq>M2`v6E`lH2e=Gj|Zm^HJh@xu8D+dfPXWBfcHh{gxjrJkBQ*qkF~7xZhD zP!LzvLdqwQaBC5RH7QW>`6c^rkd?@8Rfsa>TwpX3Wlm(IIh|;(bmC>|4c+Kug+uua=WY-m zX{VWp$bLh8dz=(Ha(u{rVrW`wacyVp358*ItWiBm)l+^K`m3>V+UDThl%br=Uh{w@81aWi8 z+umg<63+c``bE?B*}k^aI9)E~g+}I>BC%#vOo^B_Zwdo7yCxy*RgMVzX*JVEyO;Un zUoHG(rH9bqv5Ujs@7!99&~-mEwW*`k6KBbMcf#fRb>+=~fA>Ly z%5cDc-L&j5bF7%=Rq6ClExWB__?}4BA7%+2KyI%0pU%zWu1KE@a4sQTPuXfL?JabV)wDZPbX+cSsS!5uW>=O z@vln*^TH)3Tr~+cuhm={dNX@9F9oKvzjo6(6s74VlSUom7qI>#zPcePh>osBfyRQL3ib zp>E>qs3zFb=OI6GDy=r(v{o^dYfxCKXMWz}=w$(VPyESxheR*@P`RjP!|%0lCcLWO z+QRhWXtWuAl#rth*XM-snxAm94|7kuqe#u0bdJOUMAL z$IqpZcEshD5u?Od1G+t^i2aZbN~d*$W1y_`qFZ~yKyB}A9s0BT>4F<|TeDqr%TtnQ zLFIs(LkGmg(@bTj0=p#GIt)us-zSl>Q)zRpHV!)ug&WRl)p&|OI~ZOIbIZ+T)6L7$ z4eE}Hx7tLR)(+aY}FH)%FzjgP~P^qkt3iX-iK zn^4WYZ$5tBnK9GwGbvOW<7`opE@aV=L1nW%QgwOZYk#Lzp@4yk8>_^@m^(zTTC4TS z`$pyHOkVv+65c;1l5+P5XF+X-4oCCcA(3oa8=RNsuT%27U;2t#820M?_9h1>d(=}q z)8C6k2{21^>Q`Ii44dy-4q@8;&%H>C_SjdkPwbw^me_ESrwX%1d+F2hkH@Nw$8#T$ zHaRTD4mkd3&VKU7@OE6aAI934she5UE7)qbrWH5ZDK|;BNJN1F?3%o;{Aq3bfOCsR z!*93$?8NT>-H8wj*C!R)`f{ET&rKQ6Go(e|va^?_s^hY~z##asB6O%G+DWnXTsm-F& zjyoagpO}eqT`nM!8;inT3V)Hyj^695EsCuWPvN3wTy_&cEv%vlbcIiSmxh@5QbtcT zMHz11=P8bSQ!QjMNpYRXZ@Cwj)VYmW;uMR%$83qZ>)s}NK^hze zj;H3ZJFh7!uEHA?`{ctz*^ZPyHx4gy3cn&gpuWH(xu`qhi z?pTruE!jm~)wJ)l9b<^d*dXrOTdO0oK{RroKfk{z`*DJKSxGN#LpVkBWb)P3s9d(a50hIYh1MwJ>Hq|u&U zPh2PntexRR^VZ%<2%m_3poqD2C-uA*aT&_zx<~7*W_$_o~A3{iRXZ(Bz54Yq%k|2 zLY>I8&$MMzfKE?F?}Vbs?NDQBX>lrA4l=HdIa(RzPVF_?52L6vrCvF%Tc0O33EUa_Yc#SWNoPbHs&?h30NF`1iPDnzt?mDLOpv1&E zv&$1W7_~$#6BSKL!9`wpaE0^lCk_>a9s1Wp?rR|(Fef(q74_+Z>t)71{!y2Gp7F06 z?z63ZITM?k%ggRVt;Kc3qx+YaxqGAb|4L|+q+=AN7NK=u3`T84lTj2DwcZ_()BiJg zDepmyei8Sl2NpDvRCV6$WdrnSEHP~1KHSX<%v+=MR;^T%-uf$QRONf56~BkZzNk(c z9ufbg)pL95AaOl{_IePeYi;{$P_$VQ&2zh(P%^vZU|owp(Kte%cjL>(w$8@(P8r(o zd-k*h=fVw>H-94U2Y!AnT0Quq!YGG<6Rm`#=@1xtXTS(_2~>0%m!&auCWzTVy*S-8 zV|yh$bgy<#TpD^==Tp>EXTsVkS^j^+!TtO5aG2aara~00`8`zpJ*5vQgTQC8l4EKv z#q#P`v2P{gJ@j5k#`n@23UTbyTgeyen@AP2w4gD9*h=h{*N{>QIVO~uxojHy`VhqY zk*(|l0l*E3k=A?D+?;Z~p4E`*nORYs7~_2x^8!o#Qfrz#7toF|GVkycPBWIEL{jSZ zql~^A%bb?!g@odk{5(ZhpzJ5qG=`d$cr-d}n8slT5Og;Bb5@H8Jo1uEA{_SuNHRH~ClE#64%rGtVB z`QMZjKA&QaGn0CH+NtM988&nW4`JwpmDZO2t&k?9)QPd8X*Z|cLR5jf@d6Y;l zwKQ$?y=V?J105X`4y7@|*U((SYc8g}yn!aatIRrh8ssyvixy+2jUnw}f?mOc<`e2{ zrhV~Vc-Wbbm$}R4s4Xy^N6tJdWGb-YQh$Aj<;jyLplV=l+!o-7ydJ~D;0LT5U>w5| zOsE@0!>PFRSY4?w&7j`FjN0CBOtz+>;nw{8{G|}B9uqsm3lG!7eP97ByAEbq7W3#7 z9qB5vWUjW84^bTW`jy8M-!U}>%Xyai`u{$4%uMLQ#7COn=IXvn1YrA18L2nlCP1J_{TDAVy)#OOLV#BE$B^Y3~PLPBJdI|3lZQ1C6|24NGH63p>6BzE!xB%2GaUoX->{ zc|UZMD82Uu*5`Q|O{e(dQ(J?YG7>O8qHL>uF2Z)bNVHq3z_8?7ELs3Mz-jW9Pb?cW zoy?CBV>E}x!4Q!JLTjo?hJr-Q;r;tHf@l5b#j+LAc*%uP$G0#kJXY+qUExLxJ5FGy zQA7r%XRu$(nMIr<7~MS#J@az_6r_1D^>kbgj*d!QH4DaW~zNCWohzV$ZAuZrC|Gwu9BGa? z+I#Rdi(|z7k*fnybmFwe1zfPiAYbuiT#9WK?$Ydyz~;4L0*MqFWtrc5dUSy2w%p=) z^to072v3@@?KhSILz?eY4vHTauU(UKKgt`W_$iLG38bU<6Wim<6VaB{b_sVzi9FNY zN6s7(OgZ=wt}(j~1(g%IAp=$kl9ysD7)$^qduZme;_|&^Z_^H`%V9M&hQ1?y)1US^ zG>5KK2HF0PU)iThpJk>*#YCfX>2o4$4OJA&OgS~4eN2snCA6gEM(|%H#Qi#i^%^~K zWllA8KJ4DY0hGM--kR0D%eTpO0r%Y>gVsHUw6j1OKNUqjur;UQRY(QX&O zTj}vU0zI#4yFCxx=0kh9^Ma1Ir)BWEi;GNRl zp5FcCe2L*p+g_eK$4>7zc4DL&2!KAgzqU&AI)CkvdS=B=##dZo*gr`9oqfz}GzQ;5 z)90Kdqh_JJp%kAgHj_UchGJEpRQ}M#`uuS`7q37eueOuo^E}LLG~*$8?RYQEN7>nl zR*?K-WYPhrsSFO{!rw)g66{!#&<+S~k!k|h6zfH}U%?K$U(p53jU%F0SAoRX5RZtDEJ*Vfjw z?x|XQJ6!9X?x=M|TPLT)G%lb%8&nOw<#!H`Sy>4fZjBO+TD=mL2vUlV3X_M1E}$@Q zqIK4PaEDWfNfa`U9S%vV|<23wf8`px;TR;o}KDdh7f2R-dJ@Cy0f-avBt$ z_3`vXalrPStQOSCILFBCuTn(*DOWxL&GhmB0>q+HMYOT2tEy0vuLO4h@}J1WU|V~8 z9H=MydwP54;pfMZ7ptnOl6N*q6W>5S1KD^GX`lyRSyEC0qB!iIy=xa!cNcX#;Qk;x zFkI(HV=Qw>QE>wF`qcfWSR;4WqC``zZG7iD0*0d~DEvHMziNN)Rc~}?Abol3I{kve zC~nG{ri}D#WZoseGC3RHxixz(cyrDf2P%{*aP?3aWCPwyOZ@kq^%5_Bo{jhKQjJD+%~3^C z2Gt9A(w4GAhGgc6B)ml&ik(0fXtdKjLw7lfal)R*PgAH%L~f=7OR2;@>u zkW7A~Gub46o|_58ik&I8YFiBy+8??G%=|dC0QADMsj8_heo*PbygmfdhlI8QlMF~k zy}^{p*_4en61EjaL+9k1)<(mCZ}=AW415=IeO!^{Xx-Jxj@k)nqKw~`&giZSS&{^c z3O5KOmz1=%Q*4|fcw2Y#MTu=+xa_jn;@$IlJ+aq?pB@WhBW&)3QQAm53OiA{JBL}j zu~YqgEtKCT4qzKu6r1DM`-&{etjTcEu}ZArE8lE<9r=;2J93IoIE`ZKmaRQ+{Ly02)v-D`!&$EUPqJQJ2ZWc1x^3j?~qxJOljHo0~ zxFcvgKGp-_m$FGF?@-a`g!hb0;(;P^2gk4M=o=U(SQgU`tn_-+ex9wXIK+VZeD%m- zU0wFRBOqG?J&{VsjwM6;H(ern?27It2t9(X6CBoH@)#8?UA5f_aBPq5H(FAGs?Gp2 zf;2!_z09#Ou{%T2H8N6UZf*{FT4+k$*wBC;dWg+9sI#Y^4Uo{+uX823VF6F>i$16D z%t|KTO6EA#qo>&h`Sl4Y9g-rYWn(3C17xC)4)*NM&mszGcf0iz)lvRy*RIt?ZfiJ& zO!9&^g-8`zWD#u-EKm;2Cz2|-H3R? zj%wsGoaH}PB68EKY59u@2oCD~2_OI>eSfZMh&4x>ITwO{2L-99&eX}q;Klu;QM(aM zyE~gOaIGaV!zq*rcyjP<(O!6?$Z@yiq$g8j%KxObh;`OrW!1>?#a86T)19W_U^9P- zEABB!D;z79rM5WUqnJN8G(+dcSjEoE8N)zfg+ey-_2DoPamI&uf^wr=c@-?@Tw@6Yu8u| z7(vPaDou27(rlfbJEd~MV__2TGXiM>+Rrhi_O-|f0B715M1q_7R5OSz5uFRwk|iz3 zgc#%XW&4gmoC>f#2XzbSxB+%aTe=``&7283K;U>mlQ$PkKZWpyAx{tD7%Y(20vuW+ zwcncnZl5k^Q_<4Gh5}1t2v%feZlDtKFYvJ-?}(URP!wqcY#&_f13rfg1xIX02fg=b zQ*wGWFXW2w$Y8Ib-Ou!^RA8ye|7M4mcKV`rdZ4>WY@cvJ$7X*P#VF8#v99cPaE73?cI@68V)=!BhqwO-O1vfePq zPiuE;>7M09KR?bj65&*SFXCOWK}yZPnWv_vK;#Mu@Ilq>VnoC~ICnsi2Y&L2YP)vG zf*xjM`0egQyechyW?AflMpNCL80$z8LhJ#k6R;?fiJ?dXJR@C%3BPE0DN1zFGs#|wzr_=x-E4C2Fh4&`R}xBJ{odro zSAMal=k8tP!ahz!r;D>{z7*uPplMK=xg@9x&%33E*co$b$jukN%D_cuQEE4HiW9?B z_2}GIx$@~CZ}5mWKNyAiT0#dL$oriifoTTj347#>LreHE_XMj_Ye;XP+awt*DCE*e z&RBFdCvifZ93k4HyQ>9P@p~4VDK6T|!$vN@xL5}HZe@9a!Wi$`+qHy9p}X@KnPU(s z6m-LJui_^$tUgYyn2T_7=r-zgYTMth&X0{(-87OmuQS@25FQ~i>W z#JL6}jVL}>pPdtbmKt>N#bT0Tneva&HC`8#&Z4@b@%$ytsxWJ#U<6-xVr(yzDe#%2UOu%2K&x!C zC1*3A9m&m4lGu0*#K_^NW#AWBl=R;h0-?mx3%NU&axp#{cD_=H_BGh1xvH#m0L?6Y#%*L{h5B)3u7KVi@Ln8q6p>6 zrN_(6i+Q0pIBELX=!Cu$fc_PSSY%)7%&lps8LNi#(x?5LT^Ud$0w0SmX>KR6$Fap*UPWOM!XA-Oy6 zPIXVJn_U-UiA5#4V{M4x03XdYK$$Rgl7TUbTQuW%z17Kbynd-A9Vh2LE&8m#*Xd?Q zl73MU|36C+xjd8>{Y)eJj>Zkta?B?_Fk67`F|92)yTvnqqhru#Ju7Uw*wiuR`T=jX~JWnyWZ)n0;M_bEI(;%Ide z*O2#mcE0+5Kw1r10K*;ISY%L)a^Y#&7JCSUOZ`;&Yy#Cv(!4&Z%*EDbtTF#`jv$!L zx&*<5i(lP44rZ*mIhKJHLI>%-_#=n(2BWW|SfkXe1cI^Af$AYKFK|AS{{yfYGQhfW zpP|Oma0D7qI1Cccc5|cRnPga{Igff+&Pq)bSIEW3{7=}DS44*Zk$Na9wV&bA6!Uu) zD$;c5UrjYKkpT~QAbI3z%8EaES9f1Z(6RqU*_^#?NQQIH*iAK#Top~lsV#DrN$wafObRV4`hj01zfrD(J3)N@;`$icOt9PUxQyote8`H~WjZtP* zR~j?amvWWL=80l-rlv#NawMj|iS7#2)RzUUo%In-BaUy{$F?Xxt!sYy{xt7Gm7MI8 zQK!b^e7aK>duCnra%J%UeXKm1w%oqtE2s)(M`3Nj7##%_%uV+`ZXSkvtKa|os{PEB zt2r}og+5z(J-_?Uxr9$w8Lr*^cLEsluP>DTb87#*=+DE+%9G}uRMg*O3>%(6=^`BKZ)lD1_b^#ASION__qC{|De^Tu5)uAhtt)-sDrJP;iS|0-7902bIEeunk zC^76zder1}L)XUS!15=o{xqW6Jqfg*2fg!1Vef+`*2A(HQ0B%GIS~+QLbW;+P#6@d zX9;47^l*OEhPSSfZuY6IdQ#Zc0L_`@uzRS`>_$1CCm-c0#*$IxugWF*+HnV?QL;t{ zb|MGnp-Cy(x`@WvSo-Gkxq5{rF+IZCXU|&DS$z<~U_P@cLM>hQr4yI#_R2GUX;qw9 zES6cD>Kj?M+`ELET(*k7i)Sff-#SbUMdxP>uckRzi-5y5jIMe7t9NURoikg(QK$d8cdW zf&Oc+w~h^WLbF+%-~VWw4d4!%cM2NVnh1DkRzwOLTzK=KbwVp(quDX!=0G~F@7m7( zGD1V9>BM`~$3T|WE%E^6M{N$VE5pWbbJX$O)eGZCzI3;Y4#o@3Pt0m{T_vOr{FSDB zp$P1z+rIL0aKmDZxoO|euiktp-q_QQ8;_944%#}V6Zs!2(A#_QzrpWQb#-Nh|AW<~ z#iD<7@=h%Joml+-T@XS{P-R3yRV3vGlV3K6UD9F{hrkp(1o}Il$2Y--Z4n-xLw%4i z#d-|^gNR+Ch!!1Bh3ZpAU$9oz#VMf$>7S=5ol)7k>n^#j%!)wXeE^jqWfXnozSl09 zeBpa@N8E8M4ghOH4XkI)#a`7DKYFy;YiMRAtm2hYdE%gqXwi>@OT5L?9ru?70(NQOw_svoQxx;j}nSpZw zP0bF~mg5*6w4DVtu?cPHcGW;H7e8kp12w)w-3G2DF?QAT#Rmb#hw!Pu!RX;fC}eC# zq?t@!+6%y{_?=7Ir)6-HN@pPD0qhu}IX!v~tloP+tk#K1PM@HgU6B0b!#}$TdTBx? zWF<>wL4LQSMQKSDV}xb%#GCbN=V2aN$~rg~&sP&jQtlAU{HY8;bR%8>acvCufT#^+?k%Z)Dyjd7*C zP)aF%zgmCHi##1R2MD)O(QbAejB&1u>V{PDb*~%n^LgJqBqpAxK}7%YvWmMnXYHq3 z$7E4WWrH&Ygn`WpmWqH8;fT%^2pqE%`4dY|D zSnnyb-Ukss842L1aQ)KfxdTn)MkY1t9Df%hAdh!>e3|Qx!J9pAW8`FTsK;mlSaXoz zyk{k_G$5fr& zWR+?MPWZ!+`3Ea@`)<~p!@tJ$>&N8$I;WzdqO^y*MHFTr)p-e`*S);Fh*KY*7)5*g z`*VXbVlW7_FOG(hh^4iohC@^9Nkrjj7)}!=RXzAVgepS#8fa8OGxZfD<>Q4iDa5zxd6SPO)K0FYB z*dMUM>Sh80Tox-krTE&h1_cE11-*4wz)o672(SSdY8#FLymHK31H7$0G)jdrA3vVi zTCL5QfZerqaEOP4`0d)@=x7I0z6ov4ej|M=&d<^~d}n|S2>j*XN*bJ-4Q@wVE|;s z<72K3loM-ncKMtmS0rFPq5KB!EDBNbGdTqpZv`UBi|8(z+*opKL7y>mNpxP{`t9(EwWjrUR(naErf?Xv8w7&i1c! zjK^Vxq+%*Rc1Xf6Tr6?kW>)haKHbr|X)(+!GsU0-M=}XE0Wq|&Gi_O#K@}Ah(N}Up zR6YNi83d^UTRS_0W~WY=cLctCuHJ~{W#P{lJu(~$yBXtfRhC-s>c1OVJy<@AT zcrbT>l~U$?qF4Y!pS|I`y|Hph)85Vw3#ZU;q9f(`*vRHD+x>_LO@kZ5D(yL> zpm5RMohfR2ASwm627xvqdFNh9KB&h_(%q70-p5z-Z;mH>S01R2>zquXKhMje=Mw5p zSd{bEU?IjWjk z8!apX)brj9YYU3wJ2-}D@m$oavT`&4@&bv;?2#)$b=Ox|q&XFOzLmGUJtewJA3-<) z^(XlD%7zwte4xvas)&LjvHO~{xH*?e>O;TnsXec}Ug1`>8CAlcKB68o5-s%>N_5Zr z=ABno4VP%YV?j zmOM1w+J4fNJ7wO>q0w)jJ^l=a1J(k0?5wb$XQ(6g_&PY*4I|!PuD9P5dByb+fVcc-0UPGq7D<#*f(ddgGKsTdlxT z#3ix&l6Lkx5RQB0u`wms2YKfYjV4mNe1{6c$!e!Kcl&$hFZ!=ln6SAphKFp+ArF=1 z#P2lb3fqKKOsxBD#Gl>2q55PGu$5CQYAWw0bZ407JjEzR!K+(CXtc{yg=%qLrytRq z;;|c3^ksymn91~0jAF=k!E!u$^w_Z2vLmh_DY&jF-mF!~yQf!`H%N(=wL4ysW?7rR zjMVl<9&xsI_WDHx0F!aERaeLhYc027&9R!013_UJXCiHuMdv zGd=k5y3iFwy7m6UNPUj6+lnn1P!;a=u7%JQU}>lakQX~elVTc*We&;96LIXirm^d$`u4ISt^;H4=vcWbSJjA>_?LDG0*S{XI_b*&7w*&;` zfB59@U7Mf=b91zv2KCK@SATh@=zX#gK%Pi(*s^#Go4^9<$!cUQiz0Hj;jUba^cI0y zJ@!%rSK(lY^MhDyQBEWcK%R#Sdy?fKwud$lUl@@71v# z+$NYa#DBUaq$=RSOF}&b0+5#Vk3Qq+Hco9|qoG~|!6rO~Q~2VEt!BceHjK>RDV$g9u0D^@q!E|EN~R=B2TLxwWF10iMTu{*&}GL=ngrF zGP>51gjCEQx}jmEZ8AUg^h{_H*4beCg%4Dlz{?*Fd7I__7%Ag}8aeWSfIANOF9t=e z1tH=5TI+5EQcXcHZb)6Be33?LMTN;gD|OcVz30g8LERK|-7dPzguijxx`_j|x~8q) z-N-dZ%Ec333QT+tF|@)1kA;YgLRD{T0cz4tk?U?qZ47)FgqTVeEcPO+q1xbEkOE`< zbT$t3vlG+^;oR`fp)TzlvBC>yr)6iQd;%%60f`A=oFLWI07r+$z&#MC0C7QbG-P@o zbRB^mg4jm-*PE+py}AcNr5;YGOKZ9zB`&DxK-rDb-MVG6HJh^|v-7KGmsK=s>oCyZ zhCc|S#LV?6EL>3DUMWXNCP<06b!mq*22T;ZJi`eNxe^LG-fH1VgGi+USzhEu0Gyp> z3A7koNqGyij0D7LK%DS{ngDV$f@cdd5FQ1fL;5laa9yCL01PuC@|fUPVdnwn&5l^T z;85p_$Wbd2R0k-)NAo@R)IWbFD9nW>m2VY&cB6$ZMWfdm>Msnt_VT$A(XyF=Iw+X9K{w_RP#0P;g~ zM5*=h_X@tDqQr;@@WIZ^c}x~2x0U4mIz_9X!>maue1k2y}-+YaQm?&)afCMRMI|{ z0#{UqDJ{4+EG3=;a{0EcUs_-S;A#O^>r<9y(tSs4y~;F@EP%K4n3k0ULgTaq*gKeJ z1BEuw)CE@2(7`&&^zOVOd3QZ(_tX|eG3r+OscXKQIyxI<*R9{A$P}1hvb~!ewNk$8 zOgXzt&~5L5aba!zHZ5=SPk|@XLV|Xu@(G|vK%K%!i2(&xq5^Iwav>bp;zW=j(kXOS z0wPa1kWJgG`1cdLFaa1P7Q%xri7J_^#8QlD2Z~iIDdQa5d-L0Hm zl^qaIrPbY8Ohw{?gLUg|45DM@FsYU7vpe6~4l>BQFjHgptl!eq#~+9Njb)@?IFdAP z+sjd7485q4RT>Rn`gkz70isxlYmxBy>JcGw0YVX0$RKVgh-d{zBf|Zd)}4V}!QIW) zEyAMc4lWAAAQ~PG)e^vlfE_Ab02Yj?`!xrxJ|(zGdh9%8{iHSG$<(`AKr)ADe({Tm zA`tw{w5j2)Y?o*whE|1+diZttce%IPlwDjq9y!=U;U0% zQldiv;>qNcnV9*;@Fy@R*Y@D_8b?lY#PhCzoqK=a8@T3D>$vF<06d^JjVw(hd32MK z6S=HAzPsGI<3pKPk<&N*^xcShY%yf`bf#V#-!p5i_o>XtDc_Nxkl+Bwk|WK5pRYpYat1I zjO!54<7?dYH>ga}dVoUb^?4jeRqT;ts>m#4>O1l**F{hgJQ(=sm=|=9Ih)y;P;l!Y zTBJCM^kMt0X{a%o=ZbpgZlIZIRL=F?i;Vubvgb{${mh&1#2_^xbmTl&p;Qq$DT~Pwh=l-cZj0nzcND@#g#NQ z8|B^sy!0UV5(h1;6!0-8uOD*<$WOlGSVmAkciYb48-a7eeL4)R8{2+cA{kGIVpx`n5QZ7 zdM!OzVQO0KDhXc0!+6foFyKRqZXe#Ei0&L{TIqPABxWvWV^L!wjh9e=xaV}jYe$1> zs~B_iJ!`0&&dURIBwN4?3kP+A&;DUA9+oJb5U>hgZ0BD*6}5bdGNHTUiiA2)9Yo@o z#~&|$q5g>!zhUN<_8(=<9ir}565?Zkiib=W+4@A3amQh6kF~DwJgdvt#1fXK4}s2)e?g7fqpSO+p zOJFEN4*$xhET6PO3|%M#F7Uon#IS!0kTq&!O|Ij}Q5YrNz41Rj|_Bb$iy_yyr9%jo$kji$Gxk)YO$Li&!AN?E>7<>!Ip5 zJAOj_VM4Oqo(z7ziq}o75;Ov+?|A%7fQBtN`)Xr42nRg9yz`uj^DJeb0aX@%#NxHr zGAwV;C#>ojJtIA1({EB@xhM$>zpj}#wY{W(+~@2x%vqmQ`v2-6ii*H$1Mb3#g0qZZFn&<%7;X;Z_i0|? zL!vfF5uy13Kn0*%0U9s(US|)>X*_V91LcQjqL+eImIr zqM(qin~GU-5t59z8nuv>aa^>Se$Ii);ek59|0#-|+Z+ja%ir;{;D=C5&L4owT) zlgg0Be4h_|-O3^k2r&@5?tc4r9^wh`KL|w^_}hR>L4;*#Y}${o3q!rx@pwECoP#H- z)yN~HaDILT2Mn-&as4qhargJt+&dsxLWhp3I7-J#ZZY|kf-8*Zj^Py+^baesJ{se{EnhLGR1`uD8v$rXs@ z1*x}_3PX^f6nJHC5c~?tY8lQUbS1!y^j}l>^z;~ai{?yQ#Bf7^Tu9FzT7;w4U1NUT z_y4ybWG!|e2X`WgB%n#9 z9ei*_fPEgruqK+@w=jkUHv1b*$`6=MIwj=e0ug5R+_OG!*OJPFgxmZUCDxo2Drik) zr4}&AtsKHMH?9G@Ya%_v`DCeG9P7dFrF?dH`6IOu;=PG4NMb*%q8)deU%}=2MXp-! zQ3HRum!3AvrSo2|O3R1|)6N)pyYo}3d~ms3HO+e`zNT3z#p{(cr!cUK9+YovX;?i^ z(aAkYgS=zl^7;aY5aF(^ZTWK;U@C>7C=sm9kGIDU+B$bOHwN|21^NN6Ov`_L?;Crs z;U+(t5i|bs<_%dG8oNktqXD#Z{0o*EqRY?29)PKqLFpr}@*4)yHdB>whKFvM2q;*K zZ~O9_3*eSSNGc)LcY*1ra&oB^l{JY2P9eM}m(`Erl&d9|WK#0$?H=DaPo#IDP^qYzf+wJDsR^K*(BPJ|U?A+F)i=>ns33Ux!3sliL0AR@tCj6JPti9-QE#;#h}YlUQsc>m$5qppvs{9?J1x;pM=Z^GHx*Se!FcEfDkaR|saz;o6FkhzHXLI> zb{OWc1F}pdBk?>9XVG)dnjbsxo0Q z>aGRUG!dUfBKCG2fL8*uxM}gb&{c1>-i0^l$5Un|ig{9|$>bo~L?8vqg)^YTcX$l+ zjO*oWkDA}0PA#(N)tvUkC%S6BuPHV9mknrp4pn21f1ldJi~7Sg6_$E+Hl=`6T@~{} zf8q;$gwN+cM8Hmm-rNUwWb2{te#0URIMUEk05fJz6k8bwVysK(pucAa8?x-k-<|q! z@mEz3sG1{mVFN~ltEbpXQ5LjMd5&$eX0q1`2u1St`k`Z#Y}olE2f&)Q*z?Zx6+-ePLZYDnxUKP1Y>K zk1Dx5Uc5B$QsrT%SN`vMQVrkl0VB!ff%L6)bzlB$a`icCOVib5OVRTIlqwKZbwyka z0rjlr0i-k#E@{jpf@;!zii;?fGCZU?>cO`cr#ZqJkPNTsyQ;hxXcU7~IpTLFhYP}! z*wgaY)Ik$8pqPpV*#&`5M(zQL*s)WkhOQvz--t@^F>Q?hXEe_hup@yd3&d15R#^r` zfCvb*C~i#OVU)+7B)bw8k$^HVC=-d{kwi-?P;q?gn!| z0PL1uANizj9DFSxB7j@?oym`8cAkojP@QyN2lBVcSn3C5nQ@mL1>f?;i!H8R_uBp8 zFyNgIFO!#(ZH&wwKj{Nt2}ncSp5%hiT1J(7=9L%SNq3z6|@0d@+`K|Dytj99AzfapWnolzO5H!tt!*EdT*ur7n50F`%d&)@Aa0RV#m zL!vr5JCH0{l^0;%1By67!X}L3 z4l9I+GVD!OEmbF0W00gbq;L&2C%omQpp8yQ>U9oaabu}2B z460lK-UEOi5HiWI++PV`8H%@QY}{J4o)xgKNp7v53dpd^@KJzBfP)MC9bPm~F}Qzp ztK#Ah9UmXP(2{2s=LVVHKD`d?hszU#!Rnl`+g^ael5Q4JIEK`PUL~ zcKrO@7H5M|byss&g8D*W^xC*@2RR`11W+q*7(s0-2ioKo`J$hgq)uRY3XOQkmLx`X!*U z02z@Ly{xe{F@M6GeGc3lKxKg^rhNsY+||qEN-bVgzqkh6Ah63o*um*fH+z5L^$e}d z&^Nvb4+3B?1?YexpA-OC2b4GT(2N7Zb(%Q93qwVO963pC29e<9I`~g9^pcDcwyC zaDIx&&5+=hnGctlMpR{`ts$W9kJBJ3m{wWH&pZXXQuHh;C-e#Yx>E73Gq+E|ZPkwL=f={d9-AbtZ-B3sX?BuT74!D=wSrud}OzjImPoYSI9 zebYty4Lld)J4^QghvydzCY!>>`Ro^I$a6HoW)u!{s-h&a4ofEC9h(Y~_*ShSF16b%ZIQ>|m_ z75TAs`1&HC)F1hqPn$deHW};~5E2lm1&sH^1T5`7rTCZ0cx^?lta`Jz*ZX##^`wC! z2&X~XzH)*sH(rqAwvZGhDE+5V!O)>LS^bu4le)Zr=hahi#bED6_`kR3nTSH1NC*6W z@y3glmRzq6DWZh~vX`>xVWxW#A>B zUkcZe_trd?nI3A|@v=2*9v+1l{>%^?2DWBgxxqo%s}~G>aVj|qN@ak1;#IEfAA$Ip zGr8(@c}mlXt2ValISE$m9slZKZ$-fRt`M!&fNAt-onjjtA)SN`3$7UKA=BxCLEh~= z=cD{Uh=W#h)G~kn%M%J{ojuR--exU2?!NZ#w5DG0 zlfO(pe|2q5199_|x*2!?^w(y~1Kg-!;i?W8iX$r#MhkYW*Ag+C=dUDgehFaFUaiev?a&kZ+ zCbx`omls~zC=A3yDEhP(Cmdygx}y!O1lWc;*P z&GVcp)mzM8eq$L+fPY>x=w`g*|I&L0_Im+L%XZ_L}Wg%{`3pz|Q)m;F39jrRB zW^-;XE2i#}7l3cGLyA_u3cliX=yKYj5$F`feR*Ec;_X|T&sBwcw8+?&c)>3(p3O{DE=}~o7SF;T6qVznnxlMvB&(UqIMg!`{{~YaBdH>*DUKE<$_>P9T0i|5?Y+= za>1-&I{;>L5WBi`j+t|-v0e=B3Vy~!~RoU?z+_A_l-%z?_M!TI+dJOGF% zrmcFiwSE0FKqAf3AJE2mz@(h|lk6F+x7*X&Wq}Wz9w3UZ9I)?QYceeBf3_*r`{8GNS<(8VEhP>W^i;?^-0VT&XAh8CybOB22WMPJx-em(xY^*uy&#Jt_HWcg z#Yg;LU$2+l$W;bpxv!*%mB%vuLe~4{)havi73)wJ%mNWniT1rDQ@u^kz$$VRA1Z9r zvYt&rd^r0%F-HF@W6ih(5yv!GFRa+PO{->uu&~2c{C}>h}z$AvS^j_lIF9z(n0p;6>qfkHh z;~te;XZPVS_(BRXAJfez#F8e-Wl_@z7-QIud$`YZS6pFtNGHuT?U@d3kE{R?>;Pgb zIFT-t*EX?j0Pb-0O;OU4j}AS3x(Tv%)AZDz(hykVwNKE;tZP2@C&Ck!SD#O>2z-8k zFBJbw;fw7)^gb~eC6(F7c(H@nodb})p~Mc!!FQb2Q4fucJhVP|sI`Q17H6Q|f6ts> zSZo!sbN?Tnt=IM5oLbgAXtiX#zRlmDYpmuBb|nF~T1zJmk}RYTT4+--o|2 zz=i#ccx}hCoJ0(a4V-mG^^ZDdmP++!6bv|3Y^4A7-XfDfwGi%$A2%hjkBK%O99{=O zf-X3MJ)8A}jtDMp^Vi1TMA7hV&PK<9d*Rv1;I zV8c#N);!v1s^^~xsc0>rZY!D{0AKt_>B@;d=!;qI*{4Bf_;}BQzrWPz`n0f(F>{h5 z-md4N4AYA~6N>!yG2Z{(_=SoTQ0Hs8UCDC&Cufwo{?$VxaaWZPt;M-ExZe%Ay03Hk zvCt4cV&#OLwA|fu=fFVbQ+tNo9)S{IWXqavjF5nj1XM%WQPYg^ z#@xlhRn1e0Tx{t{Hy=)kpB>fmWoqnoWK$!O;Z82|bD{g1L0zH;+_i?U$F!84gn;vOTTL6N^PgeU?bpiaYK~+rd^$)hOH|&X^1sZW%(n@>cd?Wxtk9ssh zBrEH@9>10~)O{s*NB{L#f2?+aug8}(P}I8R_;S+J*tbp4oSBd&xFALan^xk-9L@xp&H@$tZcL z$Fb{U2|;hIpB8TiPYsGZjsz_`>IMi zZ;_Os{G+tUOyhEk=m1C~biLv8^mrZcjn`Mcm%FUo`3S@vy!vkq*Q?L(Q;x?S_GI&$ zR<}D{44NWE@aHef-FNc-qcxnJND*InHd{(g=s5MXFMli?nctxoDI;w&BG|!Y&-c0- zFVhe&_!@&6oZHCYjjvv|z)|&w*Nv!^rn`2YRo7n%nk-rO;OWK6kSE$p7ZvA%S|f*N z*`Tb103)lpy3M&6ua6@j6!0DlK?{3b_bHSLXywBsBDQ#hr+)t|AGiR4r z-HSk=;3W})80z>9`2Tw}ZstS?dtRJ}3l=$x;^EB}fJd*s^nK%&ZAlvCgJ`NfF-W_a z%}XFHDG+yOfNKc;Sez!sIb2c3bch+n^`ZW#(C$~WueLcLPdX?V@ORg~JZI=FiW~Bn zmSaz!Cc3G)VOL$8qqL`|e3a;6@-S&X98RGZWqArF^d%naFfAzUxTn8nj z4#TRupB5enyW*7)4hV^Qq0JwoIX@?MM|ClyT$vjyC~J2;`2Fo+y4Psws%!P7cq?<^ zy=;hGVC08NEV0dQc5}lLl|&nN-&kJuh;{-zu7OGS0$>}j+@3Uqq97D5-Rx*^~xt05J0-?p(VPY<}}p9|?KP%58b zPCVz=qDnp$a0a3VI=%MqSJt=Mf^UFjSb`htNi)$$wFjb3L(fo|753UMIXg<`+_X_W z4P{4^mK$Cp2*LgbQ-5z?A;U#G*VtrQ;#)?;`mRG^Y+2L%P+(C(t!v9xzwn%Ay0G^J zhhzPB-JGyCmNR%-<%;ce_jzky3MyMQU*5uaKHkAv9MiQsp=<6^vkL6yMTvQ$#0>VQ zwn0yYRB*sEbt<3Q{DC!lvCEo9->bOvzK}P;KMw4D!dA{H_po93U*e>$e>W+MpK!73 zpUD1%=&rbzqgAb+LYQ|R8vPEFx^r?HAsxdw)jA(NOB-=qtgapeT*ixJoi?aJgNJXWdyjwm6Hq7iSOv1h$IohOu`s>EP zDe;toQ|{ab$|00kE2h3i(oC$AIu46pt=F$}?x?HK&vmb#R9S14$&kd`8p;#;oY;`n z@^~k@-Q1YVGJH=nwYg6+392b4jmM3+lx9xAA5931r$=MDYA50<&_2S;21T?+&o<&vPwktJOU;~ zB@an5ShcX>R(kW(dYB@kT)KN9drm;zf5c|`&PpRVE3H;}_8(EpqN>#lO5^NYC+Y`t+i?}Qo+rS&y`Ao z?zj{!eHh!qHl36&$pH^|4ax{ql{3ONiU<+Pu*s^}FGPl}h07KBWt0MS1pHF=oz%ft z?qZi_^D_!5RtMuGLzEYhKLwaj0R%xtEH7dbvI|oOF{%nRhz+pv>^aVyg~>BjufLS) zh|L^3%xYRv_FRv)kLA~+mM6SrOW)In@gxc9RrYgzD@WvW<2>+i^Dh!^V-7Q15vJvd zlG3n@fYD{PDhj&WzBR1k{H%`(TCQolZ29`$@`Mn0`{61PVKe6uYtsy)bJ<(04!m5v z&rt2OUe^npoz>}dCSe~hq%&POIa*c8E>&6h)wF2x#Ov#TyBOgf_?+1@DW?i(eD8wvWxLffypTI`I-*jCC^GJ86tbrPq3`u1=^_I~J7 zYukGPo&)^s!_Fv6Ww@2<0#G19mCR-u{;@4aH6Lk{QV%I1=Szm;qY#^c$H-vT_DQ=q zu{|{i!`Sw(77HAvc+2)lm+ce7y*eY3#Qv%QHf!FRV}@@@`KlA=Mno0IUT`a=F)9g* zHqLqar1rQfKBjS^iNv8w#PZwEoUdD)!}Kg%^<7bHlJb(RkCQaw#03u~#Msz%sLT0! zILT8uS;aN9rn_1?#H;x}ga|AeBZS%fTB>*Boz2B4TqwVH<&O(=m(4{5twFZmyAO4n z#V=C0gV}eLZNo-uD0re<@e)aF`jjN0Yu+A1L@2oK%^YFGWJkLZQ~M=1u284bUqp9D za~DRIwKAHqNr)kKteP4|t3jBw2liESx-5zHBrGvjIK6U7``qJcXP*F9hrv;oiB^3& z*7?U1Vu2_l`%-<@Cvjl!kDJwEEKFJ-#n|R6k@;8Ii)V_VcUb#96*Mi3_bNH4DJF4K z66DNMR86E&Fb@);qcw2BD=kDFw_aE@6Z94mJHeD3)%!Xdrz_;l#tmhZS7@dwo4!`y zFT3%EdqOvK#-(~@pc~{J4g0+wX{2N z>5g%j9kFNXd5{v3TGpdj^kVpp2w|@oa+t4l3F=+#%Go2l)l5~8bE{c$c4>E{vqDKl zzBGrmP93bwmre>ouD^7d!{BY*uG_AHH?K!r+@2Vmdz_=EKj)Tbdttte>er-WZ8N1% zZY6s$zOec2>k%h!^niF{6872I<%S(6$KxIhw%QjH`dSr*6;t!U-*gEUO?=qE%nG@} z?kQ#|EUeJTrKXGIx-&pxdk@JjEvgIaI`V_fMOJw^_@GBd)sXTuK8gEj_Yc`lOra=< zhz+WVs>@wj3#5XK^63PDJtTn45-khn&!KfZwP`lWif6PY$FH0r6;zqC%5!l6WdSH5 zvU{>7Y=Oif@mrsTNl?^rR!^TX45JsYuz`7xs$lO>7x@si#%}hXua2QXoKMs1jY)Qy zfk-y1TrDQE9FZc>^g{=LO|QTaFaf}rd|NxmafuTJb-Cp1u_y8Bbf+g?Wd=etOIrNp zNHZAq$~!NLW-sI0r-M*~#0UYm6u0=6qUp0%M;zki*AFG9d7nBUOU^o~mPckjzh(&c z&zVd!mzI8)ceiSonlH|&NeM~=PN%IMo2xyY&Y7#E`<$6m+wqbSbkhn0I<%YJZ_JL6 zej4lSO``$dO4%3vqUuZg!I&-wkJ}NCVi++3+_L;K_IG6NR6%h*Iih}Z!X)2yM?-dm znDxEluw@F3e^rm?TDReCd8o;XG_rWW52<+S>rY0H8ua=#DxMz~u~wdltg0}jUzkfs zjvul*c=qehVrVdGjrFnaf`)AK6q@7_xbQ-hX<{<=tbh$2=L5v1{R5Zde!f3yVn^@TZShgywj9A>Qo zep8Dd+L8Ua;(+Z#(4d5Cg-)uMqiqZjf3klspW`&y$~6y~5k`J|d!Kzo6SX#U`Pf)z z2KZu8!stGqKnd?Vgi?-6onXGQTyr%-_-WEMVH~otxZ(++i$)ml>HCBP8{d795U55H zj!(oJHlZaezf+$<^k}mImSUTt2{K8v=6xq2VnMu>P&@bh9^uD&s##S8@x#VNI!7h$ zM21j+P4AiPC5guK$gKE|Z7zru^z18?f+5ZRFcBOv_KH;k9;8OBYpHtskXO>^$HSIV zk~n&Z>#k#cY0UI*mMBJu?QwV((@NMYShd|16R(eAkLP(qNHL zkoYedRL~N0GF^QEUck5e*&ljjtC~V3CY+0K-#7+st;I?mc|N&l2^i&Mv3EhCGG(m2 z6T{(OXN8x*?QX71b!XhEL94;PrfK}Zw45IrLGl=3Rbq=DjBLcWw zHMz^BlD%=5XKqLz^3%G;PbHDZM;MFJyGtsH{LqiC(GO>RV9dzh>{~MiXIM@N#(O&R zka@>1B+^6*8a0q}Ok(1D)QW(rp4U*Fo!yh(q%5TXNerdpiFfb8inD=uM8gZ7ds#}) z9YTZvm04Pm-)LR(%z6#+*D5`NkDt}-AV)}bjZUS7x-zFR47y!bUWK`obRKDiNm&r2 z{e{2;2!3QkNQ@*;61RKIqb8Zek702o>Ubxl~Ey*H!ICz-SIN?Jr0c=FXWhJ`t_8U|4 z@F{Lz2>}r+LiI5jt3TRd^t2wCN(;R;ZEVUx)Bfr*&(BipIXRjr#z#tG+a47EH6=mf z8YAkb<{#COyY&b_{7^ir?Y#HskLJ?AX|l6;xb{Yp1Z?LRE@q1ve#(aR*nawpwMhyM zzAtmOWMy|yXTJ}kJ}fzTKIcbz>w{J>kU%6d%(wr+iy+;MFgk6uB_)M!@$_=DbAVj{ z`PU(vyLv#IxvGYc+VbW)1X?5AZapN@4tafGb;{4#M}u}|rir0FcQwtB0f(S2nD?CH zbM9MYhR{=ZO#?9+X#wnx`2vcnT~b#@)g#9Us9ZRPk2-$`m{1 zAKq?^c$`ZdM8^#JbaSDu1adNoYy5ZE!YRDn!0aqpHJXaJ#J~okhqEFe4t#zOUfMEg zt$@!}q(Ccrka5XNVm9p9 zZ)Mx!Z>JL^UEw|N{@5N%pw2vM5OzW7wb(|IK)a26Wbs@{rB?Ge<%0Q*Ucs%ET2rjm{}!l*kOZM1oXymm8O}rkT0O% z%;>45rZSu#2bX>;kCcvEScx^-Ygk-1x10je!26RJhdoJrY*Lr8;*}v+42=^IC-%&( z7QI~Z@OiTx0{^Wb2TVvBXz%wlJ%8sr-k!w1{M;+{0k^v1=TvyS1eM}|yOoes8zF8T zLzpM<`o{F`vkm3KzeQpasMrmWV!gCN0kv%fY!Q4K)wu%p_}p{jwQ6Y$-seYCqQfXb z%(%0abi<~m%J)+^cH`{)sxeGgJdN)Pcp37lo%wc3Fcqt*ti^gXr$>H|N$i-;Z@t~; zQ6w&P4CxG{Oz?H8D8-3CK)2O#^8Lfq8Q5 zyLWZ!0;gsit?5}sAg8eUu;s}yHn}pX9@P1<^=QUUtSwf(OM;L z(k)Bzm3Q_gv6Jritzu~4=(W0{!)$pn z5t{m2{(*1TnnDGOjTO3HQ@ij1{CVgM&pRXM@VFopBMZzoxCh~T+V`)7!fghoR5l81GNwdN;TV-K(zlaBT5Gy~f&GqyrioqM zn-H<2v2|8p7YA8O5SaR@LQm#%igS$!crTy2&B#F~zMdkFHeLeaVs9IGps&w%_-PpV z>yI|u6Z%VOvlV`1SWp*$wkKb*W`8>@8}CeCnlu?`v=n;WMbBP5NkmK)oa^Qvq`uE< zdAQWx;^)c8etOao)WXCW-6?QOL-og!-=r zjy!Fs^!ZF&|7^4;Ve{D3+_lMt2f8r+;D|*!@+dq3QuTnuUBBT=MPaLUK$mfnF6Cnd zi{o?s`RMSWX5k`lZq2PTa~C?qp4~!blxg)WzY>tBs2`CN-bzF=oJ9&#KfiG5=qA?E zuU+)V-ru-ZcBpTYL8O_Dg{{#gew+4}6<>r^7-~E1M>IB1{70juY)q#Uz5xz1^WH`>+SF9VZi6rjTPv|N_c%(<+v{oG7kqxvc7u& zVJyttj5(wr4MHh=Dje%9I4t3R;dTtfU67MW*5Ah#wv);z$ZIwO3@RFmzGi&iDN(Ha zA^)@(IDEu)kac%!G^(LA$rD1OPQXHuxfHE;wSf9O#>a9p^$=f_QzzXNLLjSr-!0m% z#FO+_I-oS^KzpXiekm`WxGt#?>7^Ub_=v^8&|9N>d$iQ<1mkk>QYP}NG^>h z(qOyyr)H%;Qg(3?&dS4dr=lf>%=tb-d2)++DE*BhrNR$GVrQA;p*5?_ zFO1~dVcsSf=Mia{r8`-p;Xe28&r}pq2ocHWj!961xCp796>X-u7BKYHQJE@!>FF*$ z64K<^F0LVEHg_qW`#HZnIU{5C35yfbTd-<7Z!IdI$|q?#Z2itoduDD>W?pA4Xd{u{ zAo?P3wkO(;&syum4C*<>iIZ~mP+iNHRHshNZw*+0{az6@SmV^#{83Amx24pPo#){k zCoHYQbgx03M_P6!Z3{#6&m4aV#E>6TN$-eg9^f2z9EZ%kM6daMVveFD%CIrh`%)1b zi|z_XDBNji>m)}Avh?}Dt%3f{vPVd7M~eiIxCP(cLRF{tga~Q(L8SK!zM45plesaU zF!4tU+rA%OFMY&!WFmg>+0!M6P@c3_G#NL960D06%#D{V#b0|yZa&HvQCi2f1{*Un zt{EpLuK_8cX9wD|=~&NHcXq;Q#P~-K=LRow{}(Eyyu#=p9Mi#=$%fIYpG5oTMc+zQ zhN<|?kr-L#DS90v9#2>zM}B<6vDueX^Q+71d-}k06r#p`WNTnbZuo$ZOa+!2n*<_K zr_3yxxUwj)4Q=Hfd5Dy%`3%$hjQ+3OfK_n)j);C)Rwyuat)brZlW?7lo<7LrHGh=O zGYLL+3*h9j##!Hy-hWO9k4(sM_mxPt?Uh8&>fNwN84+lEZxz?>-HlnpEyiKU+I9(< zr8`)o#$rLZ8tf)aD(*R=q(vjgWV?1P-#z}*LaU>-V9c#2+0_@QzDh^F5Tem7(X?&6 zu@<%WbYNSli0Sa zZfB*X=p%t~3{%DUf-MLW%w0@A4V|~EQ&k;w&uKSqX zY2qubB`hqG$1C-i(QCEv2w4z87AeEn=E4|-SA`1V!puC6U(X!M5wSH7A}~^&_2LI9 z3L6g~<0Z?q+@xQxxJTJ#jxnQ1VjrVAbdO-w-aRImzWJ>*(MxvdO<(%&PCdsV;nGE55yq{t5n`<0a5Y?s#?=c^Sc6(yHAQ8lx1li= zmg$IRh4#%8+>jbE5EprT<-S;I2^lBkS(^piS1^7kEl65%J!5yx7E>g}`$jWTP_O381J+mvA_$9K^!Oo(y+n_|`v+1m)2*T=n2ttu$^L+~85d?NGls0?txArVriobQQ^ZBWTz?&;knUisn|2`4tOOj1k zX^X~><1n-ZNR4MT&4x4v|8*vM;|Sj*&vuK#weXm3hH0w%!1|=3I?TE%^UUuF&{2U} z9eE20pi45{F!e__fF-!q;9(E?i7G3uzX-=q%&C#!uGzlmo}L7FyP&wCZAvX^GSii~ zZGuO=MsadLCF{=q!?yVD@-CzTCm+-N?Pd|N&&7Y*DC*vI)qNv*?v5K@k@nZvr5s$m zc7czzn&gK2!q`jgCGef713RP~v?G=7v1!-&dZUdgZbo?EPM(K=TVlkp$I)j?{VtEl>}FXw?qi9KkNk4|K+nMFQtmY#789V}&6#YcP%R zHHBkDc>0S4e&HLCq1BHx2HlNuof z?J&s=Ca@IW>T?F8$nDIEA{&J}4~UpkqC*oz4v~c&evO{Kuy?JU zC|E+czSYGKltQ?a)$Rp=odqXno!PFKCzL_YTQ!r}g{!AYb3Wd?8h-x*pGH@MB?zqE2>Vz$82$V2zu%A%Bh2iO zy!w=j!4-YkzOqj>#KH!qLuIGrCEk5jm^7*C1#3uk&(FwKunY9amvBl08n0mcs5>E_ z*Yt|a&kwO7ECer3xBmwMnM^X%mtzF`VoQt;w=wIs!Hj4^%lEv8BdnF9R z%u+-INi>FKGG@{|&n4;w>^_}O4n+VXf{CdVP`)1o=9T>tXv7}qLVUy1b9OO(YK%s^ zt~;GMxn}wJoKW-{+vUx(0Dmf?2t7?O8(_ZtR@_NG8#^b;v6ANejJFL4CsErI>nvc%A5S$!~OuCB9!9w~!9z$k9T`06%l_aK@oMQ&maf8{Q-5NFuh$ zd4LSriA4aUjG-Wpy-5LQfL#hTL8O2#z_SH8=4Jtc5FXJDYFgf_vnyqGx}ispjH;v% zIznTUk9Lb>DfWiv72#q;@4kQ#Bnyd)>Pyys|*l$D3H3X}zJD@<#r zH3RHue8(|jXF*QT>GX-CmMLKWph_USEsta)3lYaxn%8u%F7v; zwg)mFvP`5hh~cxD%#@*VTt+rj%n2FF#6wWryhx}$(HdXR&)yJFBH;MmZ`BF_iQ2Jq zqK#YpCHL7pZk#S?UOk&bD?jO&y2}w)v0uQD11NKH>z}O~^QjW40mw`se;%O>3#TH6 zt0+DeEL3ku9I2rv54nE(2jt~U9#BSiuweK2B#t9g7-M9smm_RtW4H~6=6)YG^K!IZ9@j9q>WaBvriXEb*0%Nx_nMk8Lj4Fj`QG|{p14zt| zoWwacAS-+^x+|IA9`UXm67%b^jEZ~O-FRhffe@S1AD9XO$Jrk}fb)5~y?-Yb*aPuk z5HDE5_!NU0t2m4jvr0QD^e81*(Cg}}OBBCo1~hIM!!CJGjsO`3c8maEpA~f})PsmA z5%$-^e!|+?rKzT89(|WH(Mn+7DsIPgEvzZ=XuZD@ZiPvjL@Yn~n&L>+(<|j~GYHg9 znxuN>(dd=3R9K~+EJrTy9o)TbbFX^`Cs62dd17WN5WZHAk`-n%dcUB7ul7X#_+XEV zwoB@ZXoEGHZJ+hRP;J^J9nv7TD+*5hCmjtlcUA(p<#Qjkg3JM!UrK-^JAEQ?fW7Y8 z&I|FC63+>oUHUWDf~oaCvkU(K=zf&1P!X`lfgIefik%xSB!jZDA@pc+FwROims1MN zu(0o7#VQYi`0D<1UC`Yx$LlAi2Z9` zmEH_Sj8}W;(YQcqfR2b}b?0<~z>E~(S_Pmv)%>a1=ph00 zJ~}yWqjxQIqC~n$nIuh2Rwz%eZPqgyU^JDb zTr&I3T5&J5kGo5-EpC4g5`X(9i%~$6;S8?HkYicXMY4 zl=pvCV*TcfTm>fLpGvc{m3lXQs_S3=#|64Q-~2m5^Y<0CTK=Ecn}{Wy=ta|;9KWlvP`*Glc*jofO)p;$m61(3BY0Hk^_ zrSH!d?R6CBpjE9Wl7BD}Hq+LZc-k7a4aFZ2Xw4Eh=B6-?oWR45M-)$~=Wm3HQ!QAc zdv->e0T@Py`0Myxdy3_Ueu@=lc0uq&v|u{PU#2w>8znbKBu#q6i67o*bT_iuMy(AB$TzwsTJ zMgRB5*6hFf$N%CC7dHJbuh$k$F#ZVe@6UBpulzfI;WyLR1n~Y+3kwT}+z^rjRnW(i zp0Xn7&=_P2F{a#S0WVpsZsj$wI&*v~?N%e3IBTmb!8SJPcQeRX2r+`wj0* z%gYIl{Yr+~>&>dwB|B6}7|FrBRakmO5Icmbw+qt@YLNTqpPpMFO>5r?$QHLn_!P}u zwNOxn`~wZ%wL{_WFhOx2GM2XZ(w17Z470#BuIKS8^6mU_&F{vn$|mH+;^OO9;>ZJH zc-?~mj>4o%9=&`rP*HBXZ{rBXdP<~q5V>AX-h8n877bHLh^eH861CXy&PBx^=wt?u zVX-$OK)7f2Wkg~FO+YaZ?G{7*0v(lo(@7!~m^2u{>@Go^fh}3Cbr6xft}OyH&{>pC zhl;r>ox&}dkz@yyKl>xrk&VSLY$YtF+7^wYC#H#zZ~k!oG*2$_ZTw+q#1I8~*wBKd zTTVmd-qWk3^z$m}Y^;O7aN%;PvlLIEW3<)O%oC6Z{*e)h$n}U8!kfg+@i&tsrKbfk zb5yfb+C=P3@xqC$-;DplQ=7_b2hT#l>%!(d8q|66RNO&T zjqZNV-F?5e?7!vr|Lg1Pepz~V^lG)Y!%tw-)Z0Q!nx4KD7a4p+!aVA@#HaYqQw643 zD1l00z8&1oJ+kZKv~xE*MtLJ50yZVkF7d-Kj-Hcr4?{Ecm(5ljlVmy57?Y-WY+-^c zwIt-dXwr{BJ~{4QBfS}LdJxWH_;`|LhyYqyXoyc&&kR0ku=g7l`on+%lPPuK0W8E4BrwIE)HVCd!WKq^haU0O zE}w>%Ds8rVH&H~=g z0?YI@NVe{csB%Q{=SLq4V%DkZOwT@96`2ImO0!4@nQZG8v{vJc4p>P&YcR0;%DPiZ zy?A6x#|L`-D^-Ss+1B}g3G_plGOYVWDVPuNA7ajGJMr&8?%(e!fl=w%ug+0Q|5H^v zc*y_PoatXX?5*=ZUvJaWdKb9B358h>w6a6sg~WR+gS0WjZiGKy_Yl3|i0$gWVA-65 zcOi36JQrGHAevUwaY~;)Xg}*i@L%s)X}w+Ued~hjby+rQY;W4mdiK~5WqT`C?Io^i z_S6utvZoa$uX_RZ_S>Rc5uoV)p1Yo|paxe|%h z6|mFK#UN#JB%izuQa376^J^^3oWgju4DJa7%$z=RSK~>6r?aN5T)1xV@$vT4h@LTR z;wzlx3NxhFY;(;by@0^E+n)^_iJeoJ0e(vZpcHWoLtcz8p>d*m6VrlvT3=55vm1_9 zPmx^9!qjh$KJB?FMfrFm>b@fmia8h6%>8B8(-+YxJ!VLdQqju%O#cR$&k2K5IfwZ3 z7u9nWq08o|kT?(?3xORgmR_o1jR9n8S46t46lFV9AU3VQYWyI^v})7%jGy0_@!xbF zpX@(N$YA|^N6c&B^F02w#eB=_rqAcTD4gAUT}k5yM|`GN67}wsyq?Luj`(>xUJyK{bbRo{QilgP-H2Uolpoa#UD zpYQG+{VsNo==eu{gWy4VFkMi;1C7W$I@Bsy_n?L*fb2g$u)a>SgAkDzAmdyFQ;1=S z3Q)w9amfeO_VKKL>m+I3L^}ck1XHMqD?|&2=|wAWePaQ*ts#g5b{+2V!H!h&gzA z!Fm4Aw;{-(5buAO?G@&lyW z?0mrC=o<-I7Z;d0g^=Yl>6q0;z46${C`q~=J%)+1iTWJ!aRaLdl8fV8k{BSA!=F41 zX)L55bFZwKRd`y*Fo+yXS$l&we`i8EpyAOK6d8HSF8M4Q@C7)Bt;DP9QwFsIBx?DO zjmd>GeIzkR=n6UCn13o?ersnBz5ls^|Dh88_jw-w@gM%zc_07z@6V-(XNY4RO^Xgb Qmjgd8j_wZSdrw^XKci|JGXMYp literal 0 HcmV?d00001 diff --git a/docs/docs/assets/sceenshots_ws-tester/disconnect.png b/docs/docs/assets/sceenshots_ws-tester/disconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..baa76e6c9361a7123ebfad91de3dbe0d578cea7a GIT binary patch literal 361843 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 @yp$X$!Sz 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

    o34j>`j+5RH1?~Y1Tb*eUpvq0( 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 z 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 jI1vBO)%frF(}ql5hSiIunfEf*=T!=p~FAEljj% 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%0A`>oGS;-Hd?S#=55sR66HEyyt_ za)ri-tpcHtD0?oQ4SEQke!_n?Zo<9&Li6tkNl&2Z@uz-O8&ptUSA&!zw7`EL#^M{I z8D~tnUuhtTPAj)@tVUH{khY}dtxQE(g|TNhm^3|Gl-MC-IT_i65V@M(_@uAK@yGn* zhzDnYu%X2-p%Y?y48rpATBSzLtQ}$|_n9LcN(W@-M6-!p3oKsXKW8PRW)mHu2lmi+ z7~=QU;$eHjQE&F__ksHp&_zGMhI#Z;78c>xvdR@OTw)2N_hkL|e@9CDt}z1%#=w?% z+@V=8zN2}!1~I<~BP|Btg0S7U;bv`D?K57^-YdN_bC{;YNzyMJ_*hUyDFFWGYLnrt z0~uxWQai<8h?!NE1;n1 zR3ojb8V$@nECn++W}g5WV8w_Pbr6%SO**xH2iEf64>Je5))g=^jdw>R}deiPq(XZY>i zlncX7sh$nrM|y3N_*CQ&QHzp1@E9OEG^?2voB|%8$=*YpZk za9&(H>%|sHQ2`eh2bF;g7>N)Y7dPD~7_|4NK)+a6RP=t;*tc3&Acv;M07T9d8NQYQ zv33p)l#e<1T3iJB!C2ui^#<`V!h=xV{p=FF4AJegyl*sKFr?A6y+iq&vO0*rkgl<4 z-yjH9y8xjBzU~NbRahUyASB^^Zs1FnWg4hDGkCmPjk!!qwT30~mtdyF-(R1xdfWPH zq~bW0wN3mm=>*^T*^tWs6P!6KMbmccbWX>hM$`2dM%{V(9_7lWzQ7bXLZt^fbLF0P{Sb9Kpq|2vaaiS(lk_rCjhb99RaDcM||W zgCcE9gEw?YFr-DwW9Hk17>zo6$D+p0zDGe_2g7WTgj|P#{l=kdMuP_yG=g;l51ILw zSvwvCmG-dXt1Qt@W5aRC4BN(%-{?R$6N6_8k@H1Uc={y1ic9B8 zfph(~C1+=!9{fi%dmc?%L)RY$VQqZvB0zzr_W7h{i0|nFh8%?B2rIBUQ2dKu3}7$^ zGBzFiOUzcx*5ml|!lqAv{E=>Y>h1IQ)8bCox*B-XoKk#&ahVCY)`~MIHsqGVzj#{3 z$eQ|tO171d^28f#_O@lU0vV*n?a7DQP5deEv^^g(9Oor%+#*4swUEO#kz+vCapd(c z4xc3M;THz5>T)sHPQwx`ORMQqC8~}0d4rBlazcykD>dMcvAOmm+?Z2 zy`a9Xm#eqBX~m%SlH{c{f6?0agqNTHT-8w@$U0Y39t?Cfj{!zb2lh(G zp*xhYu@*Pl`Pb7tfJ77h_qUdQu0s2vo-AXF#B?hCQ)~_YkzRUU|4Dahx)Btot4;8g z4UU;H;JE(l5rCTp0nyHqc1Tei=sS<_HN3dbS!BdyKq}5;2hi{kP%5pM*{Vq=HD0GrooMO_?TPlY#ML;fVX5I zU^~u7OX*YJG%U8Qp}2OAh0mgWbL-?8R~Kw8quLXo@f;nmQ8J)>hO<;PvDZ}MQuuMI z@hEtDjF`+>p3#do$(L^?^D48@^jm$)0>^WAIxOj}dN||=L5?da3%3zwvnP6w( zzeD3cP=oOpAS`aCt-{VSIx1|qkuM-fImY<1>;Ntu()2ltgBiH{*MLO>51Nbyd)mc} z7PwPGn-1yQcmSG=g9GVkyYZCL(H}~Yc=(hgY01=oYAXw68M0>6d~mQz_>E~zNq^oL z#V6R5fkFO>yjpDJ!5$e^;h`RW!=ftuqIj8Y71%|gDQa_P4AliyFr7-50*}AM&3;S# zLTO`)KWN-)8pX+ZLzW*t(j$j2S2vJ|1jUUFX@9sxl#IGVrtH71ueur@! zU9X$;Wu@$ES-(Hud}g0$gRuV;MM=PuCzJhOZvrY@=XMXL$g9X?g?hF@QTstb*Ynch z1+kw$+iYGm<6(a}=)nG4N+Q`4&>lL>gk%^lIxpvIHkNirdltm%Pai4(9o8#ms%oDy z{HCZV=g5m_c)b_<^ZMU$&9hvvXiV#FO-6-1U)gNA^WrIV(4XP8(Z;y3G5?7EZcEZ# zC}|NIzluO+d463;JsIi0K{{lg)R_IA7YgD@m(X&bY7NQoSx8-z-(zEmlL5~@vxp^nfw^7i z`-->J!|LI1u-!n8?EGFvx_81XE=H&|$*-Bn_O0Iv8W)Vh3HsMjLJU+@M40`wf1r zF(ui4RLPz%n*hUqA{SS~er`Er0@|I?B2LWd8g%Nhti7A`({@f`4mdUqy3|v z08t88R=xluzpUA<_}#2Hy?i|f7Y(3!BzvWmqu-&%-;)egMWJ}EXt-c2GArKlhKIIO zEoP#;vV$0z*2YmPOy=K4O2<=6he6wx6|!sNHorKvBbeny=%`+~<|QiXGTxYMOzx+} z+dy`O{IQ47C>o*0*mN=>CU^my%()tfX%&nD+zSoKHmd3ZO5f$KktLwc=N7V>;6)-Z z=?Y3aT=-FWHkXcF#qh^P8M#M`Gk-nbKhEkod*ioL{X5#Mhj@Z+WVIaaFI>mlc;oHz zUx(LC*HamJcgnKvNT>J+uI`n;OO5O+?m%(hZTC~FIR7*83`QL@?=yF@zIIC;)vWmV+FR~=^5N3=l1hreZ=Te3VS%W^}t}QEd3WRGj-t4A4$VL~C2;j8;0H0I`y4{>fmh z8}4|4V@(=3%&)#kO_eq&y#hLrA|hXVdd?>%8e{;)$7ycRt<}zw>jLtFS4)%1Uwj{u zYk@(6&~h<$RS$jg+gw@{a}4M9qaLP}zNzZ_^>X~$ds+P2trV)8H*Lhyu6gy|DNyH+ zaphC*IH3dCk)pTBa~W^VG~qijo@3+Um=-*cz}>F3`v)KnYavcA!UQZ|Tmhu@zg!Mfm`5+JsIWG~pVZC$Hr=qP5Q znI-qNEVBvUe9TmO)M#S8{PW|fz?;gE3N|*kkFHeK<@}qYoxUD`7+ycV@^PJu9F&9V znQ)Q&W5PYHcD(w6u8H&7uC?nO8$Hm6*7j?G6`@NCG}E5U=5IB<==;z%W5ba8o9QZC z7@%<#P}$|9A0F+!FgP%ybc8rvo<51@jy8Jc9evirEQUL_6!l9VsC5GRtcb;c*tkdd zlfB#9=kxONlplZGjhL=I!FLK1btbW}DE;vjL*Cmitih0!;k^(s&JOEKs3_KR=&iq- zqV%Ok4{mONS~2~JERMlXlH5&HpXN`*jom3wx@5~&ZuPwEWhKA*O_k~hBbsK_uqdth z$G2P0{Av@F!7E(;vgUGf+$c8gTj%F~w@Z$_QOhzQH|wOe&U;d0qO$zB)v4xQ$t7Ju z*;B{GR`I7-XJ>)T#Z>cTMb4A0jnHGZriN-74pdq~xxZ_eIe7A3jHRAd!Gu3B5NxDV zESb+SD;oMj$=00{4YGd!y33N;Rvv`xFc>ISc;UpF3F22EE9C?!3-bAMIgg$?_xiUA z#?oJG$#3sI9oRU@V(}oLx~&S`_f$Ii-RxZjqqfs__Jzy>DSBlZEmI#Je;Pcm)TV1t zD&yZvhZvi~DR|JsHqIc?b8`&izJGG>-tE(D`5KjI(EK}2a)S?fVA0vB%8WHEcq*$b zlfTx;7UCP-n=X;_y3XB@n(d4nM_YdpC>dJaa+doGGDk6p&wkan!K+N{!<{(&%-bY+ zey39SN*d=;ASxenQ}0zJg9$3^QL^V%R2(@WFCLDKVVcl@ho30msTWkF1G?=Uq4(+9 z9@o6GZPL{06*dvbDIa*N>deh3;LX6A8BHYxb$JJxy#+J6+)|o4=-ir>HSqx#2@jf! z9l0^MNdGJyy8YOMgaMa*srZxk^5*pr;vbWKfY8XBA*WiUVs1uU?gZp|wAlRH8#N{; z%Q-7&gqp*SVLb3GT5?F0mZ|G!@0Yu<#9c}6?0)>iYIBVKvAO{-V`^rW&DKKi&lRnH zyaIhKXyDWNAFmSi0&Pgpl1I*q%9!LDdKP0^9t+*HwJUN0ik|B8CwfzuRy%4PB}`+D zx_>$gq+Lkxtawl`*Y9%RFTgQgc@D5C*Z%S=Q;)a~|4k#}*up;y9KwS-nie8KttMW+ zK#%1H`|rbRR?HxFpo4HRnOY!ZjUM=TA0Ic8ijSG?@%xeNl)dIjt$XFM1l|j!?{5*K zRB@^{ZdGQqHfHviiHL~B*S(V`AYb&O?W(TgiXXquHt*2CRLb}gr^nltHp@j{`J}P!`=tI&469~rFUGJ7WQ8RlE1aBHfn!z_*&ED)3|EtG~Cd2 z!+|3qtQvF7=}_I&L!mPGUJd=j?ko}{GctgYr4T@T#kPGT5Nkgwb?pBtWpj7!WH9X0@_|3p| z;VpE+_Ev2ZG1^|e19{n6dgvB2rhz&lC%2(f;*yr0~K5%!hyGKJfp9yUK;_dNAwqc{IwtD zciZ(3$lJJW|KK!lvu>3-EOKZTo$28~;Xr$u(TJiy`*xypt3!sceY~Sq1xjsKr|OObhUc0>$+ZRFsl0IqpiDwtnyd~{_IB&D4s1!i)`IUniCe9dfB8eT5 zYg_Cu9mK4ZmpHh-&x8OI^h1ePQ(|~X!?zytExvyF*TLC#aBK3?`c6=b^|)^#1)KTL zwG<904m0NXmY?$>oP-X&Oip)fJ|^UJ*S08m{tf39C0eO%g8^^#C)S4*zHwU8<3jgI zHSx$!lb(4ld$!Yr-Z@Q6x=rDw16gplH#UoC`4beuxZ>Q1rG@7Tr0z%>;N=>&vf-Q< zFcaxVyy{N)3qYO?w}Qi_=Z3R|UA_bSCp=ZO!%Zgp4FDugRX1lbb_Pibm|3824E`!_ zp5lF5H?aQMwNYua@NbCmf8Eo->KF~3<1l7^hpmJt^6=d=6Cj#uEYr$Bw6qYi_YXT# z-7FM38^G*MWCu|1ON1e{6x0VW9=N$8Bowd%@X5_!<(moU?($W)yu>=v`NktK6#xL5 z-R0eor!(^5izs{|W@1jhVLqs`+2V9%bC&zBho?U`y461`O#Ewg2@m{KC8@h&3p+CY z2~)tKsw4yu&2P}&l{W)>y!DmSRP1C*HxU;d8tLo%e5<2`>|z=e*8CF3}Y##Ios zE7+$k=ZBu{HgaIsFRKTsM3jE9*VUJ}4$N&u*&vxGu1%GFX7HcY_F<0bGn^c`WS5a9 z=tvN0v-0mjI5>0_^%GgK=D)B3QljEvCx&iDK*_%afOFIqbg6r$m%wE3c8RLcq)#Gdi6KIv<6SRsA`CGSRt!&{=n8g9&NmMfN45pBT?Xs* zDo!&&ZRdm>#NwJ8=&`8gDHiKffvysWr@VGlgMZ&pEeaj_?C-0s{7o}P(9YY5&GBLXgn3%v7i^`Ob1lUKm&?+VSu!{7yP!_o z;2l0OUKjVf4;#7ky}T%KFjE~HQ{ED>U_I(Z z1{Je5$X4vy*>mzCF}L$gFP)t@(KSg2_&=X&?zcN>W^1D!#@xxxjl*O8V-c+b?Z2pvUA}mo4 z7g*vGJ#Y@ZELH#`I-WQv**L%h#wvV9vzEoQbNU<31A$pC#qlspc7{|-n)g497X@C# z{`G!NQ{E5j&xGf2oT^F<=9PKuwK|jNno`IX(Y|N8(e6JC5`b%R68c`(Xri;HZ^Tg| zUHM~QmNv+2izaz2zAerDqAsw8GiS1)avt-8MhB0S$BPcYpD>QdAV5^I!r%WyoNrlZ zp0`gCLXadR2smV%wl7gU(Hz!0pBOTw>3aa~;$Rg*n8W!UI@?Nhh4m+9QbS;3}JvRWf z^+vDJ8rOpQhKgKqXjyG}la?ljMjhNFRy(8s4h38Y?8Hw48h(?vP=kwr)aaQGSLyv9 zjBr;|Nj=^UNyH8EX5d2H#-Lh?#?;x$`Bexp8H^V5`TrEe#zouO3USb>6iqqPfN2Y2 zM=-h6m2lYmA7Y8kRW8})V%RK!6SM*l7=V}3MLC`Za+WGdB{%Wm;KlOa;O|buO{;Q( z#sRQwbGVbVMV4EsAi^H1P%v-)I%nS#p8cdXj(F6gK{^D-17Ilt%oA{(gYTYQ?26iz zTlRMK^O6j>%YPvEPpJZSv@2djG#||Oh}ik2<@Ow!?&T$eBj-$TycA-I2_%dgXSxe9 z{i-(sOwE8e0PaFK3?b9X)<&anS43roS>oacx_l#J?Qs?Q0f(RWs0@ElUH>rVL&up{@%FgXYt75<3*D|_C6b0L z3B3o{QCfjvsDps&0aPZrc^+fg1LnQ42mftSq%=@6sX7P1@)Ook))lW^$GHnWN}B$) zNV#8RJC>e3xdL#-je-V=TptbzrGx8_4#@NGgvbFqSe#JJ|M&L(zqP0`7~z0pVP9)r zXb$wpuWl2;5DF!7tels`W`o&$E7XngrRtEyc!OAA>HzI5B1bx-EkyiCJi{X6gq3Lt zHZATW=Zs{|s;`dXE-gD*>jzHx-MJ4Owx2@aJP+lJx+`;AG&A>mo}pm5?Uq!*JFWbi zNz}4hGr{xKzfL}N?YIgka^f~lw_Nh@&BMYD_>fP?(rzy}xz=3L23faMKLvCnkng*( zNywf5vaTlLh0yE3!_^vm7- z2RaIb!%1k`)oN0u2nGhG$mfPF74na^-bs?j-mhA=k|L#{1_TS=++I^(%nY#TwR6Z7Z;?#}Ym~q|>*`ZE*t<5^^~q z{+AGTeajKTj;$n?9W+_RYU@CT`W@xH55c2-QR-do($Cz=QRE?ZKIWFc+rp-aSIZpF zmn_`qS_TJ_wXB;moxkBH6<-&uX2tu7W|k%*--uGv%QfvA*@2Xx!pi&|^qP=z&dcll znmRf|4r02#N6Hu5g)PEl&b7WCh!S4uiKf2Ekz4O2FCn2+Fym|CIQW(Fi+^8MA0JF5 z45=2?;v(GvI3cYya#pHBpsi9jMT?ngeO~3}7bo;V=3e3~U5&A_i9fsYC}0dUpF!~* z@5$CVoWz&)SdP!tWN)|ScAM0iW#2ksbuZs-K&%U&l>IYr7@+*T15>(6hWbpM|l{GU73`jE6DZE9aJ zt3(Eg$iIihKOgRE)V+Nes}%;0(tpqVe?FXNU%sj}8Nnr@aN)Fw3()&8&K{3TgzdF4J1g zD&`nqG`@2(b-DWVlHf!K^xrDYzc>Hc7SI5x(w+R@i_snnCWJK+sanL0Uw8if`F}q2 z(}LeSy(DpW=dRNKT8!(TeZY4k-ux5A{?|&K=L0x4VaJn0*2w?&gMfy}UBHlW^Ygba z`C|8y6!DBTwXBrN~ua<@keEG6}P6KQx}J)+?ob|*8zR+gO=cEwA%arT|f zl~1#;Z@j+Y&Fl;HlQre}?5pS_+mzXA`-GjT`)8lAOy1R>t6uk%TNKo)p5TtM>#uKU zi+sNK3Rl}boUQE~d=fIb)2Xvj?TifNlNQr{WdkobPVZa2 zh|qqoS~B+V=2y3X%wbqD%^y_g2$|Tlk6V}xR%PW^ul!~58L3`&cRY&30MSSwv0i`2h#sz0Hbsc7bYcM_E`osOT}Vy1iEiT-^k?#gB~v9 zs?IG>`CH%`38?Iu#;J})RKE`PFhHhdzR^lq-s?ldjEHNg^~oXCEN7d$83H?Cuy0v7 zCUeNd29*M)Pd}LPl-nGh3RgOmtNo_fgO# z?Cc#LY}F(FS&<_wC=Z}B27n73*u92LY4q(_C713yu}W#ZcZSuIAE>>u`i`GPfl9r7 z{q~?nu~CUbDL#Y)o@l_8Ix6qsdPQZXS4*1xUVv*<+WY z%*gk7sQ5~+!;5JNVLoH8SuXU}0k)ank z6g25hS5$nmXa`m9g3CLqKxd8WOBGt7JB(K0LB->_xvoT@Iv5>(15`Q**VK7h@x5*v z+e12D11e1TN_b1``goleu4Qj?$1OM$07F>fPRFlL6Ry<+#-%W!T5Jw40)P(aPAC<> zX4?N?o%d!j$WTq~qxcG&TrhW5O+JW`^vjIC=!>Omxpa3{u$x%K%%ZQIxtDV`?~Q8J z*ZZ{c-zNRAy}xfpre>zANU4C zkT>ef#&Hikh)Jj|6r5zf0LSewG)N9diCnpEjfS;bZ3hMx1vZV42q1d zwn#y(>dSY|9R=;RG#O)KlkTzt@rzSU$lO3u{r&QHa5_90o)*1_nno|wwYMn&ifB^ zo8COoYi(&E080BV9NlE|bObK3Tf^~@O8WooF7)z{r}yEZ3SoXbbu5x48%D^clhav6 zPo{XR{QNS0#03K&QFcP^TWlrTp}?V2%O8ZL6J4!}BK2Iu@thl!Y_u*L^~ZReHh`(H z6YYFm&;@XDD*Sjad~a>lJ5I-@!jFd}$kBI7Xg$)i|JsatymZ>L3=T-t`S|$gh3$RS z3mUiD`SnkJ4eqz3disuo0sX0{|La`8!iCV;wSshdj~$`$!-2BB5}(cdeO1O|hKpW@ zyx-nj`&LR&k~#1}gCn-5&Kg?J9@HKq-8fIJ0s=0&2ZkTbpj=OC-6a{1UWK2#O%5L{ zTH}B}esa9KzbSd*AaVGUbkoIW*9A^SGpe~Dx@k(%HA%f7Fqh-UPNBC55uq5V&TzSn zmE(0G+)}HA$9;3u3hX8peBX7P1VU{l7jp7V?CsAXO`b%s{%}9qmb#|i*?;|iyHV=l zBJ@YSOpj(JI8{7Qtj!7kGEuB57_0PH9Y z4QTd(M%*_FehL{c1|a1DSQIJE0kslfYW2K{%9R%#Gl*IObCt;}&77dc%Arzaiqog1 z>UNgV*_zjqubkpMBM{uZDZJH=U2}4@>aaf(n_jZAl=x;T{Yu;7{~zTw_(Dh@~2!ZWrGu;C5}gLN7v>==i=0(2TtU2CHUo z!X-l+pj6TD633CtSj{Fu!)qo2gyCwJ)cGmr+Z=&WKd&^u0(+;#Ev^2@OBgXYq6*l*PR5Cbi?R)>R7Ee}&qHhuPbV{01uy2Ki`w)nx^fU)-+&lYKx&Mu2DeNu&tC=8OuC6S>B2FDM2 zA>gw^SE3cG)mB>+2ak0MPp1N#bfP-}mrwt^3T9L1--wEAKukeuH_in^de4LIT<%oR z>6i=Q71uJlwOyK}Ha`zN^e7y;zq{{U9(ueP0X1L+BehXeczJ{SWYVa3Xson`$Degog9`rX8m4UIU%eA=_Wa>5< z#0usKA6XxWtjuz!T(hz!tHbJFx3_%=zzv7Oz^mBm zoO`Z2bG#Ql4td4$TVYp@*RFcoRBvlKl6Bp(=S=iE*s*KMh_zdrkb*NOdRnax`1D_q z0DB=j>nTGlr%oxb`a58=KuBO;(GGh`5;`Q}l9MwgC?R&}PN6CFS`Tx4E z0<6&aC{{=ck_#;$gz(CDKjZvg-Tdz;qW&fe1InZI|cM<8(USGG{SsHGDjok$IdinBYdZDM!L|4jRLYUeqx+>Nw zwy6Y<4$Kv#7@OTPq{z}C5pk}jk)h!WzD}W+5G#?L21H_GZz|k{MBBr8b+%WL+t|}8 zq#|AmIeLAn%b2yF4Qyu+#-!2Z^mZjx4bBHc9s_y;q2Zel((s6iH$WHD*Ty{|Fb@+g z5tqgGr=jn2ZwgG;-}zDo>1u^DKi%BaFNJrlBg) zM4ICI0!}@0PZ_a#{)^?1b!DogJQ-XWwHhf7Dv01kV%}BY?$Oa1p*k7BYh83-yOiR& zArE)eeqL4VYkIg?ufSZBtR%`#w3VdM2*OwMGOl5slQuos0YVfOt$d-sr|(4*@j2o@AD5H+2eQHTd-Kc9$5@M5z83WXXjG$sarH)scMa{Y_T za^^!0X^(H_6u0@I0*$QzW=yC$ut)wsa~31Da8olxWsz#T{rPOLwQMVvLbOBy$-u!m zgUrJ#m@oN?Cn_i7M`%sjK-014>$uKVsZ0ejX3~=eIRyP$@&(I_`*h;LlUK$eb_64u zL6NKZ3d+m_1yVTxDrMipg4292r~e`41)Xwc27~OeoSXh?Ex@!ppLi9^%#AbHZ|K?N zzMj~ZkDMWldms%`@X$(-jm#H2Y?+6Tj{9d;&-(G*WhaS}0SfxqyK=4_lr{y=jE0Ln z!$Emw;KO;`OEv|6W#7Gpqek7kr461@Z2VQImY<;&lS0X}ZQ1Ig_AxN4#hcS*3DIp$ z593>Zy(`6Ob$=U!`w;33Gv)A!*$?YUJ)^D5@UL1gbcjGPe1yw@n#|w8G^j!S3(sw7!Q z{ux^*^W*cDeQ}+{ZE~_40z$2yXhu9lt_W%wwnHT=C4NdMnMPH+i_Qy+-~DTi45Wn2gYLhFjFsi3a05mK}$f2nnzSnv4{qBKUWj`F55e{ znt>9Ads!L5oMS5JhZlZHc~*z*zQ0y-=25Q6d30%ZjL~K7b5zSmxe2*fQy5Rit!{#k zWut%)@eZm|VaMhafp)GCX((Q(ySPeUVc>}W2d2Q8xs*3g*mI2TDRM`}E?Le9osB8H zs{mQ3=z4kHD`PFgrwE+-zh46sH!P~Z%A4Hi5peD{W+8mxI&jANrMzL%d?}enO@K7- zjT^l|4o{x=!sfzwBwzXYE#pSYYLA%!8)a~`qOt5pyEyx7rL?XeYeDQZDt2zjx3 zwqsrqCtcU>nrHrbZyOsfJ?XnB3~`vEwR2cE=~HWb{sMA^vW_|0cT6lqlb$yd&L}jU zOTV1nEl~O<_zFEL$n?pDS9{OL;~(9GjwmubGyc5A8=`zKH8|8iYOxDgW=SgJPyw z1qkobTzy2IY@^>ISE!_O!-HLKqodwwRKsMVJWsLU;d>AXtAw;T$LNCvKW%XpWw9wS zI|Z(w082>~B|LhHAEzL8U?LG29H3ioXUe_V%4l8DEQ=B;dVZEc;`FkX6mA(AU4})8 zD~$xJUT>a&V1O|iY@rqEt92L0HHufKYr58rP~eYq9+on+4c`w)%)JVHlS>cz zC6gI@0-?6`1q{Vuu`;v_47d~{r12>U&4tz+P$BfQ$(I}!H)7|V!*jX>14bKk0eemWIO_t+}lVwmgI8Jq2Z}(TKAZ`UNrsZ6+kdR zjzO$=Lp<56E3D_5z+buTWt74-{E}vL;*^(H6GA{CTb!|SzD9t-rof@q-uYGXbs@@{ zanR`F$Ht@-CEt0O!Sr1suW;%nci*gta(*bY$|ZSQUh(&v1?z!}sTMhoDH-v%ooq|E zc>Ix!q>VK`J5j8*(sZ6{M`^Kn%d_oER#fiD!3n#cdIB3kfmIVEMVI^4G3Akz^U8~P=*1%5lUB-Q0kr^ZN zmz&kyAFEl_e%+#SyAH!w%0?qN9M8oyXuNeZjzuUdAIN!C&)59%if5`%czv6 z3yHC}>6Xpn=ygNx+qb0NunUG}{Flbno3s4;b7!i1g7Sz)i2L3~5Ngcms2u)%1x_$h zLlXHMfYVd^%>VDFf)bEShe9!oAi>G}R+hBahnJp_q+=hA5woc9@dTpaBXWM^970P_>vQU)U~@d`Q7 zr6^>mi$LB6J2y!-y@&V4Fi0E1u;EZd(E%B zzh*sZ3R%I=F(5w$zPuB4ZpOaCp>x*@)HF(~-!{a1k7c%6I;v|Mq1TJ}No3yfyuHiD zBO;=Zp$kKSkSqTE^;v=p93>`-% z9P>G%fp%iNT^Xv`vc4v7$>wf!B}?$uu%=_1~}h4Qh+@sD)o_ z!mTZz_xB6b3$}l$f06sR%v-`nB%<#EOeC6vWu7-Qx)PD+{Ev`M~@~CW6lvm`6 zz8C9($Z+YXUD#CP`fb|UVco}GgkrT(F$1)cBxd^;y{X9-0trI#>Is;|8Tt1|rDVMF z@NV%hI4pbKCc|y-P|?Wge{0R77-E3s;NSpTb0tn>@QNY<*M;0m1k7UGOazA2Dym)0 z8>hB4`67acN9bwSPNgyhLFmj^?e=?_G15#JdM!YJ+w4}otcU0J2pA_ zrAr-e38CflXPKB|>>wnJH`?+8{WUr(RtHbC$_ll3a{b7;E_sVpFB9OTGpD%%n$-9erb_**f%I_Pp9@WZA z++U8BSyVD*e>2~^o;>M3*Z#itet`!`LSyF*D~TL+tVg^b^!T1shA2Op&%37W5^|3a z2uEny(-{;H8dTb3F*o-M3()u02u@lWNs`Ejb>X~K&xMYpNhzr_tYH^%h(GqAl zso!U3*-G&rw!I;OHrfY?4V&WEW{8@kc?CLgB_I+NP_=?=9pVqC%#vvI zi^G3splG3e8IL5!joAbHqOj;>*R#kI2jFXYkD zWfI|1q3oX0=XgEh&<7O&_7MlKNi9jg-ouLzXY6T4oNEGIGo#dr+6oFD;g{VF+->g_ zIc6TS|7=Bt;+h!34bC8%`9|=VTz37Y<^gSJKkxOCyqAo7zG{J*JQ4+j_dfN-44%Sm zvM0=%F#K{>r_?$_MC23^(uDMVi=xz+Pn}_&djDBifz5jnX)?T_m=0LRKE6I*wU!-g zv}k3iwV$=l@~|;y@NFuHTS@o4hkA0YQNKbPhK5StY3CZEcTVAG82+^+K=S{WR4f_6 zp;r);r06fc-p^JjwSJiq80qy6S|wyh`d&g|yq8?+&eDs(70!2NU9R#Z3Xox9<@eWf zQM?s5BEW?b@t(IBU2CZ|Br?z^4#gCg$%T}aSn9-CQ_d^?S)DUjLSg+WxEId(ueWFN zgfzq`;F@){SQOJj->d z&!5@K{leO7R?~YPeyb*9>L)r(5M^KVA;GCx-uc{{viRsz`lT>Hfh9L6&1X9PV-LhBqQ zCROh*TH}NmYtf5125RQ?#}nR!Vnj~AHV^vRFptC~f_Gs(@|GfPi&qpeTAEd>0oK=} zgU_fDLS+bQ@!y(mX!L1(Pjn*6UBBnwjBcn$otCO1 zjkHO%EluD*a-A0*@wm<)?q1CIg@}UuF2@@h8ipp~Q?$8PbL-@WDYs3c#7&c=0yF1J zK3m~O*K<;`ziDe9u)Ot*(ZAe}dB)<6Y|ia$^-HJD(o5Vr&0j?sGj*3EHoGiAsjK6i zRboUBua&4Cv#KQ=>pkx~1#{K<;6)%)AUritVw$ZvxH!RmvcbI%id8K^ zQLN?jnTdOSl1CbVmlC`H!98TMW~{Te9SC{1;xg9`iU38)B%godrl`E1E0`F!0!+zUSu%J=TbLGY9P?f{8K zC|_n627$ySz;egFRJxm|^tRr`uKgtSiDOOm47MowF(yb1r^!#6V=7DX$Xw!hJ=|fs zB?7l77XI{P9M%tdOa}Gxc*aodtvb6(H~)CTe_W!$rTKVeb3p=#>ZfJpARUH+4^(HE z2(ZA-Ls5v1dBmT*YxjfT0{;=Ewp1T+_DwO`4oU-x)y?Sk5P0&wBJb{AJLh{zih5(> zCF#48uu!#JN~n$t;l@7laNe9g&Jwb-k5sQvD$srR5LzJ#xYx|xIjiberHx8rk2f2h zF@66|$7zCD;7`yq%_JuhlYD=VOZ^&E0Ru6*fQ{mjfO3ci1I^Etbt8IV=V;~giP<;l z5HU3t1OHmf`SA7lF0YtypGn&uqN}D^P)Ncm9aOVD^Z?>T|PI03-wEF!G2 zJMJS1t>Cp#jYP0jL}CLG@%SwqdN{>s3L;xbngyxl_eI4scd$mc%wY zg7hSGn4s#iZX!QrSTMwUs}1it_!D5uBc*Fl<>>@IO3QBT$&o8%-1)9kIfIJ`*CBw> zyZmwy)!u1?I;cjfN*?h80ui*^!fXgAgeH28@|_)LEIy-090ElB1i3!?yv28%O_@E) zO8q9~2PTxR1YeOpTJw|e%ih+Ot00N#LbV39=@)m)i z0p16C!x3Z#XcmZUp3veaKG(0rdUXJ;Auy*D8Lz0QV!_-76!jf2$u9Qb+F;a*ZrHBf zDQG&i1KFreM5I(byAdwBcy}FYL1wvBy8lCC{QXovc`fN5l#K7?Zbj$xCwadK<6b@| z^VP7EMF#kMe5=G!3?~+^TY5tW);q;MR%dk?zMoOWbg2tS%}rGb7)%#9wNWP!_(`8P zAm<(~W!k=|rQ$f_1{5A5zk2o?yVB`WQq^i0L~{xtXCutYBY5qiMGe4h^&=O?G&0`!rqs!Q!Gk(%1G?jkdf!)>TwuG z&*=i__;AI#&DQaG{_0DeUAFZR?a+Mr5+t!Uruh##B^ldFEB0UMPlo{IqqmI<)P6QW z>!ObJC`@#3Ww!?P{3X7fZft=vv~gTnIXT7wz}7pM`-dl=%eVvS?wAW>k%Pn1 zNHtyA!--UhP4k+kn;o9J9^+Ff9;-8SW!_Qi%87A9g}7+Dy%E8h&MUF{-)KxzTU}Nw zU&%3gQDjYQtS8z`K0jE&go&;^ey?r3I#%7@*|EOG*Y&gyCE1BEDQu1g%F5w-{`y+F zj0OCk&ATQXl31>6|9x7Q2#Bfl3%;WR_48^LoedUo~?*Q?njw`xNw^#(+xV0B93Jz44>c4+KmLtj7({Y`m8*f<|6 zc?04%v4QWPS)@CAvB}U*-U+%cE^!-B11be^ajsJpY1#mBjPY{lDO{0LU7H{xZOpAJ z?c{w@9jB49EfO3XY%S?i5(Y}lNbEh5ELT#D1o{7%r%I;n7tsD;NdbW1XK~pN963gO zcVp_+uho1!>P)^UsJnEswZ2uLhdGcMyH{zAU9GFvyPh$sz!UnBKpF&nAln7v%cNVE z$Hg+wnYM>9w{!X@Cw?{vg*6->9TadN6gTnR9}p!|BW$1wBuuEom#FhO3_pqZt?T15 z=GNZPf&H{;<^gmwZ(N7QV7?nSq^bRoP|cxJIIHM1y2l6wq$fmzsV71WP$&=&0ZvWs zc;y>Jisk41eD@2C`z><6)uSN8N&WRb3n3Z`+iwx*A5K!a{hH6(pTL)FSv~ArU4tkE zF;8)zFP6mZz5WWC>h)(hZQY1#cRgxRwFfRIVGpV%FZ^)qQ-;=I2s{9Yo#~Nd1eq7z z!5PqJNb*>z0BOqOtB8DS1#n6#AWDf#iJzVL`tmn>0Ydh}0ox-=AJCrr%6-(!z1|b< z9i3=f6A#-X5MfVQ=2iHQpB#;27*<{JZBJTVo&s&|S_YBi$);o%LiGw29BZK)Tf+m^ z9ygdUklz?61Q>Ml_Mkp#U2Kgf@%Ub|U^ynm zqkLz?a=6(1d4}0PVx^}o!j~vMr*r82kc#E1Qd!yxY_-D&Ot~raL ze>3>Mzw!NPG9jB)Jj1cn>HQo$wo6gNkE3uo`r$e>)9Oc4>MpgYR}2gxUxF z7nE)~Vhv=C9ew==ClkxOUXgWZVU}hR7V~DRHapYjn>?;8qAES45hei zZ%%o|7SF;{0jVq4jG-nk!3RLP5;~B8@KXf@nn7cd2$m6~*JcnULC~UI_Ut$=2=@xn z|COOX8N%23_94ecWNel1V&1FRL`YDG5mspha1e za7Z;3R0u&RK=xx2?9!x)1ErlM;V9{f#;qk17(-w`*L?}eF7Lz1&?&P;6*4%d+Tq!j z|LpMGUL1e&#Ol0JqJYCG4ONHrrr5PZOzpE&ca$oxuxP{H-f2UUswHn(^r>|K&lU|M%~X{`r*T`OiOtD?H9xHr5ML3Z zK-svrXot!942pc$rt7YoiR110hu`woG6k!HQV%LpjmB%d4oC8jXu-j&X5XF6&PoA! zalq-jHo*170&&B6CdZ=YH)F9B)q%_duANL$Eda`uak`!az!_{mYltn#95C z6(`~qhYHNU5ZvGUz(4UhU9#`b%)4xVdDLbqa#g%93ef>B;&Z*j-q_Ee`V23G@`lJi z!6(l)aeMRTyZ7hpjgB8Z`pxEJJZ|urNE|=B`mws0@vmI9Md}h{u7UkD0iqQRk6W8l z!ew^YRD**OJ1#Ent$Oy|oPJk?%~9!OX=*Nkaz-diQe+TC@gi}R=Qyg61DG4^>U``+R<>RvnD4!f*zlJKa@KyY?n7F;Ry7cEQ6Zm6WS$CE#rZ ztVetq_mxysK}0Mwj2sFcYB~K7h&{jzg)NH?ff636_!b$WcvZb*d`qnn|9BLCe*)44 zBr$SWeK~FlNL!>1!Z`p713bjH)07_oLH)t3Mxp+cK03^H?#0v%QSpu+?Hp4eoFNs6 zYcbI5E9$D9{`EkFEo;;trclvj1 zyhdExJ)F)?A_EJB{w2{0R;3*1OCGdwO>6&u4w*#qWA3~7+>bUwOFFb&_p{Z5rQzxT zs@km#&lyYosk-Ml5Wj*_p zwcJP8r*6xjrYf5wpRIoNJ9(2rylhU8F_h+wLF7g8`33GP7@@O8+q964km9^xgJJ*_ zYzS};>ws}q2sQxfbwC=Om8o4{JlwH85y=;v0ATucer#Y6kGQ^ikNlxFRpxgrhLT-vT`y4p=m5a7T)alL)qm` z_wB9PN?8;&S9QL0r4;^J4UaDo^C&JcZPP((?$>O)^Yi6&By6;gY+wmwz@dU+hEDQ_F9m?ViIQ8tX7%A!H3BB@eh{K=#^!UeBe^ zazA(cPMhMQ7IIP$!$R-^3+JV{$IfGnF2oVVoyzHAsIDhn4#X9$me~;=A2_Oo z{+tSsQ3lMVA%HZ><uQUGBA*?W8Zd9&i7edJlxkm7Oq)6--xY9 z#XSQf8UU-t!^4A9$s+%-+Wn?XgUqml2gD@shY(-?j&@Ryx3b(YqT{V}<9o7s)jQgt z)hi-k^P>fx;!vqIonj0}nqGb?#f|H(=g9xOgk7qa(lkt8I+#Wj=>RHY$LC8}|4fm) zIvSQ2(#iUn2YOP}0Te`jCU>5ot%g2hKU+PTI5<2x-pr4zy7Q)}c1Vz()ar9&0PcgNB~|$>Yb*ct|RyBNYRu;(QLud@f%y z*c}`71XE|!;D3fgtMK%o({I6GImusYDdR-}K)poTDxjg^7TEZwPhSJ&2WR3TB}8!u z?-U5!*Xgd=&i7`xrl1-dPoIy9x@8w)eRCI8%!ybSi#I^oYYwH;Dz%~^l{h5(vb*w? z^fx^Whg7UCw`{C=^ULnWiT9Gv#`h@IBD;nw-)SZ6PS2!B=s9)d5H^4cpTx&)JShqd z4uP(q3r*x<%iSOP&A%_e{XAu$n)Ho0-#}sS7I>a$|5S!_MBS#_M9o3a3o@L&0G~a4 zBneL(+q(Me*DoNO|Gr+>y0B*cDF1!L7;u*TsPD-C5)TRFo2~8b_3)FuaQNMT8V@4l zSR|+s9CN*l#C5i=4U50e7(}z>%cbaYSi`yb`Jc^nw(o0eQz1z#wCKO4kRiPz@!#`} za2Rj9DDtQ!WZG|Wp4d(#*0kZ(0}cwp2A;*`BIj)jWPgC_DBYlK9j~=b?ax`ksxb7pCmpUJ%J!()*1l&#J;m!64Jh)K@!A{4 z$>5*uMWYpcRJr$cQ!uGMDfShy%CT1UrB)Ut*GjbCbd#daB?;q|TUt>4!N8huVx+o(g(0gi7@>xWk&r$C9EG*+R$XYU{g0AUgNq%*Na;(k7 zhH~tNOX?v@e)YU8T=z{#8@!)M?ya3P+`b;8c8$XA06kVe?loz<-><)KEF0jzKQLJP z6-i@0d!Yru73s4*j=TD%Ren;)WP721uO@ud-5LxV2uAtTRqu|qA-x7qfquUBV~4?W zi}WP$3{`&mV~p!6*oE3-%aeG?y(7}i+UhA#_zSP~Q;yaBc5;AXyb>mZN5whoa=eHi zbogwrcE{<*(^1R49~cP~N@9n1;FH!%bKu;u#26c*VT|ux=ry}{4U8x-0uwQl+SLi~ zmyfua?Nxehjc)z(%H%!EnA7HVf7$>C?H?zyIFQG5slJZsYGdZjE$xH0?)Av4@-Gai zhhIO0p|ENIB2N(O}g{Ot}02CW!S@r}!rk7NOeFKtcQHqJae z?aF-NE8w2SWyV{Ja>6{~$9HYNL#zwvx^<$bM7T8?lnho6hNtG1p2tcE?t7-V#$bKc z%NJbD**^3)h&$6|sznJ0GR~JxiQsZ7PaxstR5qo+sMljD-aL~g;+Bt^WkZH5trICT z%}p?}@3ytyW5Vp|S-&vcR&zd4qljQ3ROu9-cT){mRg86_576a`#eD_Cp^$dyyX{mw zBsgt_h0Dbr6^p5XE!mBBN@SnKN9ph4y8H8+Q3t}qJOYqPWJuS6)lsnvp;|2>~S2)5`WWCp=|v4S8=T4 zsN>Q53p*KC&ZLj%$2y|z#Eu${?{%$OQC1!Y1tuwo<#!XF3(=h8A0|ppNG+Q;wcYJ* zWs*<8oZ_bt`TJH|_OBhekN$d*IF@8N_oVM|-OEGt$rp{$m83D@+-goI0X}OvJ)XWz zhq1D~%uwO1YnWgM1ATOAUxh?%j4@=DVN*}A$f?~d-mWZ|k{ zIa|644LTJa6@7~GpzO%>qUrNAXc0!)sSrhvU@-Q=AbsJiv-&CiTlP+a8SY>60hz-C z=<8le=BpVym?kq^gWtu=LJZfaChfh9Hpd!gbF&Nn29$gMJSTWsEq*_%NRvCAmY>(p z5PM|OIf9}DHPRMR&StgA3znr`~J|@cgNMQN-_jpg2F@00{3>kvV z$N?VbSV-t&3zwk8P)2< zT|OA8=`x)^D%8X`o)IOPlxp>jRAY@Ycw# zOQq4D-Ig1M;xpsVSP!}}E-yOc=_OwoUEMK8XO$>ovVN+2gct6-&EZqBAdTonMMy|4 zU!vg?8ypS(6LAIxCP{W4bY~Yud?`v=7TMjrH`sN5-pGtpQ6!7=J-?VdP42|Jl6YB> zc%H_n9LB@3)Lx`_##DRUjCe{UEA^Wd`uv|@hdhF~%Z*VanY$B%cT~RYDP_(Sr6L;D zt0%E6?$v9)ylJvP7`=mKvC0{}vl6d-ApAPA6hsyJM7kCoKaR;d@WyWUK4m)-H*Ts5Bm5U6wlpOqhzD@m*R>| z+)fgkmvEGjASaI~x=-85Di1;Na5ynX8;~T;4Lr+gxhET$NQ@vK1gL^qb2E+x#an89 zJ_{fH{wDo=af5LPA&Y!8?yen$AsBRV@o2^UrF~i5oP2E#veXcoUwK^ZU zQy$-%oe7yjl67R*ShmTcn3LU)!w5CJZ6}tV;&$|*Ovlt~@1u|R0Re8&~ zv`)M)-zz9T2Ei%Js5aHm7J2z$t30-sl;h8tnUzcH6LZ?JTHwGW>=^iu&|yr7Qs6H; zO_EX5LSet_Co#Bce>S`Q?1z%uDnwDe6YSy`KWSESa^b=Dt*LOKFlcuZvPnrQ=5tIi zXZW+J;zj*bHoxy*U+A9uoKv8(|%GM-j};m&_s5kppe9GnPA|?1L$!8qk z@=S|Zl6qm%*XV^_YtajR*;T>l(8mqmNLfM^URdX;{;qCs#!5vfFxu2C}N_+Zw zz;axpU0vm?*ofk5mP&!6A8TYaLrs0EvLac4z(ekDFmU}^*wnt{30xL&9ltV3LrdrA zcfDErr3JCGs-|SDWd4Lv=QUtfpAyPnv+Ek-_{>IVlYN*I`902={(SJ?J}i~2`NzC< za?cB{P_KE0Ll7HDJ(SmHFr&zo(`kdWMET;5FQ{uTh+Lu8D!N~!w&Zxlx;hQQG4pNDgxyVU zX68byULICX^P%V%8Gb$Rta=)bbsSv_wfGZug-R5%?bh#=Q zsLbY`I0>*N8Jk#0s?ELcHt+0x%g&h;gwels>j}2wfsB2o`7-i;?2iA0;PIq>D$1if zGHHFUc=}Y5!)&+}9{m0kE;jQazQoR{HOGHHbXF!!Ljy)vpk=b+*&Nm9__>o0*qNE_ z1>#o6@rxM8|G3M*_5kUuOq&BcE`OCGR<1w$_V@1$y^MV-*B9pbJyrz9j}~=LJQwDp zkEL~P-I}&?_^5xQ<37gDRb>0W%iDIeSSV#Q^-#@<-|^XLy!~D|lBFZ~A_@XA`qY_K zS90=-#~G;X;`WQoe@<{q{9~2H9ex=tv*cZuO5+-c21*>q#yww|C&Xn28EZX^aq2zn z+)xjEH(%SVVw;twp5F6pz|uGY@CXB~7}~X4dd#3_x6KnawNx3Hlwge>E*RkNh+T>k zd9Zx2t$uOy&wem2Dv|Hv&o8PLx1L%_+OdCA^ayk8925T`s}GkenE3@$%c)ErlA=ZP z!COF}8eD{Go#%LJX3FB=a^9c9KYiIUl-%AjdE%q>+#Xi5iq{sPk2I>25BpF&!Qz9 zFsA?d6=AyMYfl!e5>u>Gs){K@9!>QA^omP6FoZT_Gs6AXVcU;ckcO)`Rd1HoXU4`#XAvh& z3BjW=PZ)mv$UMXGk;@2~9+W=~3*vB9%(>afwP0cc^buy7!|C=nAiE9C62{_ZMR8dD zRR1iRea$>c+(^F5Sjv25l_PSKuwI26^r+TrKLI>~D(CQx<p>+dwzIs{bpM4Ef1LbY_*yLM&USyGOm zm@Q~phT*oplW8=(<3Fv@(Gd8Rhd`PXT^cy`+KGz$GByd*RhH$f@17Lq%|haqN@b?h zc+l6lgf1g}R$@6=Xx0OTz*7?-5+DXB$S1w(1b}q#K$zgDr=#OA#UNr6svVv%mk9G5 z|6+@O9X&Pm1pJeqDDOjOvxD1mnzLFtvt^|%`vyBxWl~9hQP)jB@dFa#Jb!q(9X+sZ z4St9@m%c)W)x{V;@xcKnP45T)`8$f&QQ21e+mLk1&6llIu0ki!oygPRP$OqujN1wW zr>NWP1JCfBzoUf1_k0M8*r>U!b0%;yDzmIhGLaiQWH^CU?-ch{M&3SsRO&}Z2X!M; zxu7Z6fem}U_9p|qB+h!C@`0Iegb0J8UbCGIoSXmvI|qyKl0UhT7G5T<{Hfd!0vx)+ z-i!QA))~VelJOK_2P3dQv^pB+*BT>A**-p!sBlKs*fxWNs|NIHqzjFWAu3_#%8uP4 z0vHcY%fD96*}vJG)m&u$WoKn_Rq|-}yB7>t3X%_$CzuLkvRLUV-qp7cX0|`8IKG>8 zjgagLui80>;hDs}Zl9Cd{WZoz$*Nt`W0$HrjH3QN5&l^QW`%PkeWx0pyi>n~NBdlr zM5*7>S6*p!H}JVat^%3dX}iXB9f8e^d~d(N(~?9%*xV!^1}uk;gc>tBLu?qJ}M*R*s`JgXz686J=9DcPjQEZjc?&NHd- zO!~3}^G=?gJpE26ajfF7!qJ>%G&R+ai@W(nnU&+p6Vr%&Gva0KOB6{T-w1hSqj9q0 z*n)XTE{nQoAj#Zqc&x9exDn}ru1zT0G7-QDgzVEA{m^9dhYBhn;KP8$lBDCaga;@d zWsbgPevTbRk-CT}x%jvwtCbi79~C=6e$Kh8~c0Pp#&J@JSgb8(Vm=fMPql$+Dn-$zFkbr`xDlQ;GW@ z)B42Od@oY3_BB`6{ehEhQO4stn8v;yPU^VECe}*A%OOf2~2u9(6?RCBBo$D8epqXOqlh0{=+!juc{P0tm~cE>@<5IF@PMgR>}^>B@@(BTU;Gy`aQwXXpBVMhur z%foq$G$t%acP*iWIr!_QXFX4QSh{>rq6+U0mZ*FbOPY|KvZZZ*Vc_L=Rv;7+gizkyRgO3 zg&0bqG)iG%myY9kEWD0O3aLKFZs07Dl5HUe2$C4VG6}i9el?hD^iiYQ@-pfNNE$s1hzV8BZ7h3gM1pO{nLNX*QZ zwT;lQ7$EZduu9N$Gcxd99| z1}qMoIiv{u$}s#5_6h7bhmQVy!=n)=-IImEyIgNvy$88;QE9r@c!haEZeLXRvyWt^ z!QB?jI_(+HuJn5xg?0_HF=x4&oOCByK!>YnQdghZ6l&?f*F|UyxjJPVFim@{f${h? z%?gU6`OL1Q&^Hn-Svmd|{ERps&wpM<3ONnO|Hqf|5Pl z5_I#`toE(ZB5pm&I&b;Z>#iAg%3z8Ozs^Vh-XeK?|BC>XWbs6iS5;Lc>B62=xm>Tz z=a-j_mq$vrc6NFP2j#$;dU<(4oJDaPEFF9hj9k*?lJl{ZG@!pBAb|H*u3m%Fqu&Sy zH2&ID&R@+dpCp2PiSiEvHNuxKUvlUb$EDblmiE&taEByd>~AQaAG8*D@k;l@JE0ev z;qM^=ux~gc`1D}8Y%|QI>Lq&2lkR#y9WC!XNTVzqAnI2Q0NPx2_m93zVojkO>aqqs>|#(=sM+eVJ! zO&PRu-0_lJ9F5L{1!JEjhS3_yAao>)l0#57M6v*xLbNFc1``GRTPSlP0GL8X{n{E2 zayO4EqoI1g(O%T`bb5~b#pjA;4o@PPGexXaRYGCef(sDV0u8P%O#S>Zo<4&0VXz_w`Z!HMVlqTQbP=vW=hv13e;6h?x^KVq9rA#ygTM{5hlH4(sjh82NK|(69PTre?L}bE=qv6J!=MWR`&a0ctMK7b`jRs^UB)RMUGv zcoX)kkmDQ~Jn>IjMgCV5W5}Aw6B)fi0=)N_`oB?>Rjn)Q7TR6*)4d37+qJd;HXkv5 z-tR%NQ7Qg#oC>Ab#0AX2UBP&zVpjt8!1WmJta}{TWURb-Q!wcu##k9WPLs^QUDWdG zar>{X8=Mv;v|#3ei&!|*2I38DVJ(=Ay&Ac(jvfszzV7iZXcyQEXFPm1g$gUMnpd}} z`iZwuK`(;}*Rbp_GA#Iu!mm;Og)v%5NeSUUz;rw<%fOoGei60wk8yWcU$Q)BvpK7n zcrm}%XiGQj5g|8;bs|fyQH60t+ssq?>p3nTeg1X`_7cQXY@!{(uw8O@7K@jESe2+| zh@e|d69&DjtHo=obB}b{G0anQ>&;8qs8frSm5zPOL8+@dm>l#`+3u{lzH{bwuQ_-7 ztDvBucUXd^=Wml+YI0r|m2%CSEju!1ql=vIo#e0MC{BK>Qdg+;0@FBo1n+@1e0@qiTz`f2yu2FuG)m9Z@u3WoxM|Ga8?Xh+G*^7g`P9P-=LuoddMaOa@2G zZCYrEhH2y!M4&bh$&&V(8;bS900ZV|=6)Lhr$7>QKD`0&KA}> z_m#MA@i7Ew$X|EdkcmtB)NRK9dD%pD6zU|_9s}FhhVMSf8BpAAREs;~p~X@D@7n4q z*4`96m5S5}SUNPmuynu*=Z@S+7%$T>NB1jL{B;18jG~SxKE<@YQ;W_g+%9vcx%neA zLiO$F0fdE$ZvG%#Y<>2E>hD;JZN-+=3Mky;rQT5Xkq*e9Yj2dq5ZkPEYZ%I;ZZD? zNPtc~zg&C_2nZwIP#C&^lrRPJQz%TDwPK>fkzVD$5&@&13WhtmcAoj{;69cK$qUBqmKtb+`5HJpYT$vEK$vq!kD~Vf zde14?f1D}sRDC(kuUQ8-j-rF^$A9uZX69rh*?R%gCSN)tZFm6SG}<|Q_);7Tg8{&Z zBbZGzt{l=^3Z3160+)`m$ODYu$6vrB2;$EiiH9^#((z$o#G@jVQUaxdp zdfjQ7IxZ?_7k8HKX5R7O6Ap{QZ7z0K>n8GbsGx_k{?J)y>8?sd@po_pD%`A+b$*0I z_`hFIxR7fap)2T%Dx%5#xz;Od3Ap=={j`0B)tq<(Phs7+LJrWifeI+fu2gx@$()~7 zEAV#Cx@a&nNUIExspMdbSD*OKetyhMRmnpHKdQBQxy>p?iT9`9|D)?o;Gt~W|KX%P z;%+0fNm0qZuSF_Zixk;q8Ef{PR7i>#FTI2D9HRkye8%E^f@_K&(5loJm@AkXc!lw zj>&r(Rl1`$BxfL>(cF8g%ec&t(8dgSurutfSJ@+c&7h+c|wsSd)88Or50^2aB7)decMKs1_|H{xZ7QS!eh$L zTH9M>oT7L5<$;aW(wk&)E9$91sCEl8{8ybGOss-gY1C!nwbR;c-h#?^8e74hF~ope@4PdR6h79=aG?uN1KWrOsIq6-80k4Ad( zNwZVa1?0IUU0>Oyk>Ulhkp%b%nhRVA_Q zktsF_EK|;MTYFsclP*=e8fIuC6B;L`;c8%{Dk#fHE_-;h+Bj2p{QQu>!xyhLJw{*O z6iuLwGyd=1dgRA#AL6xWr7Z8<9*xqblMatG8%q=gFM`AWQLCKmI$4g98UuckI(Vs= z*~R8V>h94+Gf|h@RkJeE+-36c&t*=ry(ML*iYCl>y>3$d{m2>bmBDhNcMpR};>^2| zwqdfu+JH8uJenftha~4kA!9}PvJn=2QlH$r2-nUFuwzzea^D+yaIz!rorG+OBr!t~ zCJTdf5B89(*MTFm*5t!a=|=1!O^f}7@{h1NItRnA4F4Vvi8$@R5}32*48>2nB~li< z+;(C+IbV>-qDD?tD<7wWS+F+pA=Am4&Nqqt)j~i~95XZ=xcjt-uoKEqiGy`*!E4OX zD@#)}O95t*>dnYZH(xZiJ_^d34iRiHNox3Ea^#mmm;DkM@*a?DB9=t3xDAv1-(?RD zDF6E+HUgANpo(fFd9Qv9)xs2(Z+`lp`nb>?=;*4sJ^^0#io`ZF%p=DynXe7cj(AU$2E5mM% zQh3TMRX|7Jk-IrubMkHmj2>wt4g7w?ViFP|mikf=(afAyiB=^nbpL?!1WTHHm<)^8 zhGGNe9EV{%skJ-lWJEuI%Vo|+`hp+jR04^)7%ql$PN^e2aHA?|+QWu!i5;s7+JEn> z;-CB4NZThJen6DzbgOash+$>n%MZltFMe2C&a6a*J%!?5yLK`gk4%p=H1arzX>}4f zq5XlI2kkmgo`z|(3~jz%DNVgF%Hr}v_@sx_G3-gRFD|KN>w*o&dZsa#rfdDXqDt?S zi|irum0D@qN8%b9Iix1JS+;Rgj-|JrZbC!eMx|cJ#Y_LT;RutmjqEIew)Ge(kgIuN z?%waB`qgB~lSU|!Oiy14$aE*TVU~t4`zP*gKs45H)LW!{Y<*#$fiF%A)%`#hC z{7R3z9JR|+q}5euzOwkUj|#%ZpKwAWm}Jb!Euu(E)9Fm=bGvUAKU025IcxT)fXd5v zgf}N~7n7~VmH)d#P%Mqwb<^-4rfZ+M8d3U3_$lSHbGA%{K33H8qazlJ*9-4*Jh|f#2_8%0>It5fVopX%Fr+{BqT7 z(`T_pwtX_2&uzfbWs5^Kxm|o@7AI)t7V%=|`P8r4ni;NT39$}6zeWrn zCd00z`A%3C{uae-)D5okoP%|O+6vZxWntt;cS2`w{m3Qipy9n<%pv0v9Xl*@`>FS*>&ZE6VwJ7q@ zhIrjOt+uHW4}#uYyGnw;c4#j~Fsm$F^+8zF6Ta;S4saf4;4nVRew^p*vl_8w8CAOu zgLKVipO9$wyGOVu{5Xnbc)%QM#z)@n#$5HCO%odfJ5TRn{Ac^o?Pk0CH#gQP2utiz zrT);x4KDNXYq)cY_E^SzPgsZWK4J7q;PKUZbtXKb4gc)~q3TE}A_FQ$YuhxL%v7}IDD3FtsODz$Pl2i?5ml5pkUCgewPJ~06T zE%+_1J~({}$BcbX^UC&Uc0s0X(O#Xiy2M*Yr6cZH>UTIlE8At%v0D1@^P|CAyoUqH z_XcOlzZWOlj)nqIkV3EHFGv`4nXl}zy=mw4O=hL}>wzI1O~=&0lMNjL4y1TJ88)BE z=FAT}M?~+PQSpTszFor1H^km-yJJZBP{1VDQ!Vy&torED z5AH@A?rs=0eqeD^Dp##idqd)f>uK8cN=F+CCKwM_7sS8Pypnwljw*uF4EHwOtJy%t zNYuQ`-Ki=;g-d$)j88sBJ(Y>myvcBP%qcJetRIQOnRzJ z?i~9Y8`FQ_ud#2bReB1VZh^S!qA{x`YyemBLFInEfB9V;FGZIADL%~!x(|}~gM6Pq z^#1)LO!JUQ8%_}qJmW|PKP=OP{jCQ3dgcJ#HfXGKey(jZyHT|--T?pUmSvQm0?XNa zLvt86GgVvlzr|b;h{-n!eY=lKL-)k@#W6j|ZDx2+z{)~{T~uEGbKwwk9?(UB8Y#>i z4IZlPJHlab#{x6dniBgly*Hb*!F5aMv6YP7O$nI#SeME-d{ExHz4}RVlJbE4eqLd- zy}K_619i*GD;}nv4>wEhd;q-4rP=X*Q22n#N3zhBBES#+?8jgn6>H`^J%0sd*?Ww4 zH-esN;g@GsLt9pRE!5RDd%tVCDmI^e+v@8IL$Hg7 zeccQqcinTPuyOslSll9_RL?bXsA9A9qdth`4V zSMVKjmkLO-2RZ^gmM8i+f%=VR1+LJoxgDz!O4`B2i>mcsSao`F2mKh(s zG*cADjN^paUKivQ{{Eigt(h;^c$~kcu6|B6-GDFxM*;A%x>52N6OW0O$hGMIE>TH= zIaEGBC#hfq3_rkP0h0(;0~=mQfywqmKY>WvcfVh*sr|^15hh_~d(DZK>?g{4Gpemy zZN)+4i1ELmp{WUE&e!c2d52uG!TU>A|F$=h16X@y1TrxbP1P*)1wp?cAl3o{d(uBr ziC{?~Q0Boal@|gsK6m4nyr#mk_+rARr(?rZC+={GOYEZq%K^{5GZ&XeT!fkeOjDX#lsqn=Elqt;{R4(Do7xb-el@2RY};yhlG!n{wl4gk`%deDRnZrqY2N^ja&+{% zy~qnN89oM<5N2F1!^1~E^3;eVJ??HibY1)t?FZYT4&MD*l`vTuy-kT)strq|xT2P(>4DRWpvexo|6eSxdBsr0PC*{mPFimJEK=*2ENf zUAt>1PVGSu1x)D+yiA45lB=U?{b7PP>dpdb4UhlwNNa_ZwLLoiahwqngj|Zc4*-u`WZfa(N8sHH zs0lC;v;r4NKUT2sz&H!Pb#@ALfZ$_t4xj-}3H#ogJ&XHO=WBS`4{{EIOddol%UfZj z4UCMoJQLy^gcI<`8@%&40}$;ecOw{v*)nJFxRmU;d#5$$Iz6_uA{Yr-UmFxZgias- z4~&k%iWDv(lBB>O9Tpt$1MUts#36B=#J%rE@4P^SiG-H(<5g=t3wdiZYkdtS3o^(A zXrO-31r6+7%G8yVEMI+uY(F5Lyf%6l3)3_Z#mWXeLYEviFJx7rWcU38f&S-+2jsRg zwY2E3No-yn-d0}*hsvIy@DJFaK$!wMFIvTjhEt#XxHk*nYs9;0St*^LkT@4<`;gN# za^5I3tKNd}O_))X^+(7K)5iPp1tKeY85mGr}gR&z~POMb|EG+Ajiv0RuMjL-axBn*H%cq7>~V3QA`**F?h2p4jx1s*g51l{Q6 z89zpLYL+{^L!}}BQA#(?qXW444ZxuMcagJ53f1r zR;~Jg=ehLiLh4$=v5OI)_L~=68>|9~O4TH&?VXlCIo)nI@|;|qt4ckc`}S_oDuX&{ zV0CHS8gSGKsAX1-iR(5HJgP0}zegFc?B$xp-A1``e`N?@(g(uNV9Bzij3mOKEKrzu zHSAIq*ns1quSc)3J-hcJHk_YbaoU+HRH+g?WIAr^!+HfvXxPoo5!?#-BcMq^k0&_a zL%o9eI~a9LzOu_pD=lS%XO!};yH;?ySuh`d0m;|t=)LkioF8pp!=^5jr4FLt1!Qqt zGcR^&uJK=T9?aQN3mVnU4<4bChxcWo6}LcIgk$Yv~tSF$18RSygoyE)}Rj z*g;lP7jY2aasWt>&>G3H`1Yw8I@j()n48Y84|D$T)&SiCah55EQBGw2f#}(71^jk| zRA$JS@Dm^wWZVOy0jQMpa{jr{_*3vYV7eeYCQ=mFyus_@^4{Ze^S$@b7K82;c=jVV z`Co7QcRAt*^^nr2d$y$1d8YMxA=Ff~s=){Wq~pB6F`=-5(0ZW}By0jz%Pt`QI{e0W z&e4W>ats}FvO zKcm>XDlZ+;Z@;SEPOh@{446}BoueiEqT9E22i%s3P53Hz?N&roTa$KC9 zztu#ZeH|JxuNJrRe(HDm2fGVTE#v_Vw_m|f*ed*Q&JAWz6T1SpUC!oA^-4_ zFxn5Nma5j4D#rcCE#Ms@copp*b;4I#m^!%X;9Swi_t6dvmy`G&t{Wadb_Sz@wp^p| z@g+Fm(*oh)gH7FVZj?HYMD8kVMm0UljXe{eB=Jv?#QBSYUr1;ur0#$`#EhLw-lK?? zz$(22^V$uzGg|;y0jT!wkt3>6V_f#@`!5LX=HY?x?p_{}gjTl}1b7M{FhWxiLjl*p znCrWFh06>hfK*ZLPGvuI2zH4SGin4Ae&HXF!!FojsZVzkCMOniw;9>h5bsYMGy(+LRsgu6m zJ#{C4QW&kP)hWTXD5Pi{(1Pt4Zg>TR)(lQOt$uA%m6Coj64ZVS?;j4HE~q>1B2ma6 zURhcBx-=Riaj(}yMBu~;I1x5j0R|6y z3K(BIb0K9Wl!1iAM4)RxQx66lKfL-MBO;DfBL}3hfE@+Y!4oHV5xr%8QL^d-YF_}U z20f@+U|?WjzHD4SG&3(@hE^^>l%q2N-jH|#3V~Ea;gin=`~Wf=3V>o8ir%#xJqRue zhz5$pMkvw9IRi-^uN%4VV_acG%mX1NfcFPOs7}4@0kn45+2Jv0wS}H1-)-dTu0wL) z7vS*e-fZlHVI4zKjkBj;g9+}szX__-;Mr~e<84aUJ5M^4gEx*{G6KfF!hthkRehza zR3~9+an+2gY2Dk{IXDoz!O-RNaoG8%7UZ_>;(9Np9>rq@4INZV;D)&8`5r{+5#GH) zTi?!g`=Wk#0V&n>a0a!h%b!yxN=ori>>>{@o_9X4RTkQv(j9T}Z!gOBYPQ5<#zSW5 zq+67?uuVjczEcZI19CTl%@%1sp%e@O|74vuI9Yd?f*KRl-rBY2T7G+>f+~b%|EI_6 zStVe!A_(uW*Un_k6H4!O6Mu1}#$^edS4KyMU0gEt=}CwCLDg^W;u0mbuIkLCn(OcX zj|{PHcxkB*PV{iXK8H63p59~#Sl=`VynBSW0t3-nQ(%(6?$|IxZFe2Xh?|;^JVZg3 zB(>;|ATq$vnWXiMFxVnMIT|idj?!9?o*WctG1-?Xq@yl+s*IVS^n_v^(O;z0!cHzY zU?@_5JkIc=k!M$JHH+d`a9!f*0f`@SMnRj+gXnl_UR4SRZt1AkgJ1Ta>#DO>pdUc{ z6@*#%KlJr_yAg3% z{?np6wZm;;(3JhWsyfa%E2}h))5LcV?97H0R%en9`#uUi>=O?UgKPC;$*G9c`R#f* zL-00$+-TJG9rB;Pk`l0pfg6YnvIt+o^~~e9ph-%qf2>plG6l))vTtEa(2!Zah0{yd z=2KsKGz`Q=k)76^T7)>9Estmmb%@3nAVY~BIP?NV7H6EUmm@4A@kzFOau>{Yz$uR7 zMC<6@VjpJ6b=s%qkZQuc^Vq`|L&6!_HYvVM1)8tyL`4$UR8;R$yuk&wV~0lJjF!x@ zRv<6u&`$h63{gCUh^kyWDhL|azu4~cg+bglP>Z&d+?QkoIR!{at;=ZGtc*|(dz**`n7LzwAa zP!NMX>BN>#C+gmd!41jE%Zm(3Vc5lT5Q2}JO@7ry`dER^f?i*1?z`A1;?R+cf|X-2eDe5ek63tmx~@~?o87R3YXE&Cf)K%UwF zLp2N7{QVTrW|};-@cg_LP$NnlL}B}S|NgAU^D*PPUSL|HgA<=^NJG(Rm06L(VGyW{ zFb}9wmu<|rtnr~6fazNE{{05|jNMoE_8G8-C`8a)mgl$4l-EVos!b|K1_ddc$kBPG zrgFr}-h3m;1^iN8qC#NrnS%f+CMG604uAyE-JzsPad-Mc>R}Z$ z0zp)gp-C9g*pp94k4z}yQ#jhDtN!V0bAAJxo50}AD%*N-c}h1}#E~PFP@yd|JOSp> zJmAy{Lz;RDowaIW#jnH^Q{r7HjkPq{WfaKnyB`2htqVEMr5*8mQ{6i3wPd_~d=hxJ zsP5Kyhb3r54ZkE~8)|FLU%8W181~{hh)^8TbPCVqbda)X%swGXS@+|QFx?*GVkYvY zHZW@A+D^vom=B7ies38Shwf9Sl>>MW28~%a+!7LBUEya_y#ypypzQ&#bC^oqdW7eb zJ#DV0Wt0D)!tUn=sUMxWc6dbi;4hSE+e>^s-Yd>w%5ft?!<-$SmHxLpJnGj@P0vl6 z7jbYNY|r>-e*W>6!4%_5<{XtQ&gzxgUyaX1u|FD0sX6QZN=d8i;^QC@-kp17GExun zswwj_GAizHKEtW_F#k-n6a#64;)L_-P2{49iaUmxyAs-ZSUyDo{MSxAtm6 z*ykPo_(qv{9#c+jOzY-5pTF?H7y{?hV}B=4a&+%hqeJMG2M!%xl@|$(#R}NOqt6oj zWAC05)mF=L4~#u;oGiLWMgz1sLy_8laZxwP)Fw=(gm|!~HH(GKTjx=|7}q1yBbnq? z;+)3J$Jxhv*AfCclKrQq2@P&#gu#BP&yKvRe$7+){}ge2;eWwr8?0$sd|_5~N$Tq? zpMO<{&cilsW6&cD#1$U>0LHlS?l*t6SOo@a#$NECY0k`Bb$U<=c&j*E-|Zbafm2SQ z0J96)S*eEDGfDf8xTEw(&{vaoBqW;caUlQ7T=6G=j#plXl@c&X$w9P-Um+Y$KdFljwuy^ znMclHL^^Pa3Ta7`_8*$<>77~b1Zu?@y60%@1!)b8xc7F!$FEb*Qm1MYOln0aD-t4_ zg*QaiWn(~AXdr?0D39Y74AyBS>S&C&tX9(aU9+nS^Ar`~psPdPmk_Sgnx>k1}D0 zMs=wGn@WqyDEAiSy0=OHX)Y2tgEgZtPPcUKCDkX;&3&XYtae~!oWkqFU0<30eaFVh zxoL>3>}FlSU&Nzob%x%$;H6pWMk=dPjbg2vJkYh~CX4jb0>C?3ry8u|nCmk2^&^Y* zK&1-7{mNd;LERaVZ5!dR4u?~icz)w(wiCd|FjYT?(3>R%7{Zf1@^5?{{b{dqKCw?q zVdOi6l>kRcD&iisys1t0TM5m+EI8^HoEC4yLarDo;`8q&mhCQ57@oEZ)zsP?ZS3Sk zxhyD4{%(DGs#!E7gj3Lt`e~v2Z8L4sT`!(6n9lblx0I+BwdqlO%>$qqfA73q~bH;eJB&7xzn$+(PM4<7S)Xb@!HwCh`V3%_}h*clEsw0c_LX-V-HCzEFDPV zeEY+vpY80(yK4niVMihqx#bC{yugU#X`%P3skN`hTl;{CF6T*$*XWfqDWlxC|Yx`bJ1BPq9 z=_&QWx$zVU|%vV@;Q)@>3!9IOzu#r&i{)DVIt@|+|p?OSb=iaajEBkgu6gb_rHG4B0E`S zfa1p?Q|l~;%daTh?O~_8M>;jwEWS0h=;vE}1Q-oG(o6U7?XAItXsYJa>2|t)c23U4 zzaE}nW-V(fYA=O@+u%~pwC8Z}fH zVgKaV>HfkyRP&0R{a3=n_z+FnV=GLxq0HW=57gsAN-+2tIYG{=d7?a*hv40%Ih~Tv zO8mB?fU7Q{qym0o)r(s`v$$cuDiibS+f4x*45Mr3zt~;m7|qjr zcXZtL%AS?!qR|DnCA!PT%(lg=nX2Smd{!U)WzIiJk5xT~qGKJYY~Wt7!qQaa=zb`Y z{C}`4;fC9nTsUpDElMWNsmi~3W-}AkhHCj#qBDb;ieWU0;sFbPLTl!TpB%OvzuMI{ zZdFMhFRA2zy-=xu2wFb)l;FIGevnpVGW)ZI{46p%N%DEtR^~W&*nRrEI`uHlKTj{` zpq)A}NGUvJcldv=bQp6(&TV7a;KWct%|nmMclu}mmJ#vKhIzSroLiUrqg?a zI#siC9xhd6n?!%f(WT}swm|um%X6K?SlOD!?W|L(TBF#d&E*wVz9cnIloqm?HMd1e z8qMnd&)Wh*5q9Q-Fu=W!Z_ofh?^C+j&^v0ICkJ}3#PemxLS379K{%(Wb z8gLD$W14=qb{Po%ESeB-Jgs_j->>EkC;pz$xw)sf|K8F6nm2Eq;T9ENPkh3kjck2| zLzVwi+u?toik?l=VOtj^{APLlUw-KKWJXqPd=F~Y&aK#++&fYqOY_6agx7Zfy)Qa9 z6zM}nh#2#&F z7K)wiU^WQflOLLvwnwKvaClJ+-j>XvXCP1`D8dL%|zh>*gE& zuBHQqVx_uDgqxK;_VFHETY;7li6rgJNb(k|0&3HT+(7daOL9s&vaNtx^ijZoLBI1z za>HN&jY40r?{M?sNXcNh**Ov0U4>3HrKP3l?5VDR#T%W9zMa)? zrwcDK&;4Ccg7QcJJZqu{nIv#rHo(D@5T-5G_V)aC6&j2$(5C0#>7$mBrVFBAXD)Dw zo?Cf+PS#;>65W}Yn{$(+j^(|)LjMwHR3#bcM9UPZ{5Tt7NZ_aFknD*t24_ckK ze{}OD6Dfs9fFow#Rdn&%606DxZ93p8=Ga?s-DxtxM25Mg)R(=w!4HlSmE+b&wK+C? z8$AQmNT4wJ_FTO3EVux}u8Y$Hb_b+EX6tgtHCXxnhY!G)O3n;*`>aaJkLfXC97bULCR z!;tv*6aJQ6m;5MVU^BI`c>vk!g`VTa5nR$AyYapts{=t1XdVENA(uEC#Q`&g;tfnV zAlB36{CbE!QZJdmk7yegZC1rFdUH5tFn3`rd^>x)y6+uz#O^%7oCf{|A!BOQyv%vL zlyI?4Q*x0Y&^voGG8I56AviEYgeM>!Sq3<(pK2U1vp@b(XaKNELWZtGT;3IOTE*WfGicCn2L#R7|V}LN$@gF@U28 zpfWO~^XBDLM5}UDV5tckRwx zF1e)tMIBLrS)N|CdzTfbc%wwW+q2}N_Vb->(E*Xa?3XZAUDA2}29yc{StO9Rbf{tT zapt7;q6R$yOk1R+>WkZr8&K*n3ORmb+j`1_)>-RHln-s?;1HB=R|9KJy&OyYo@Ull zP#m`2?|_p@Yg^7|_b&Ugbt!TU?huDF7a&0(2g&$mbBhxGknE%Rvf3|0J_}~gu;i@i zLye$(z1=Ak$W7q}#+#hrI|TsoAmkRpjbVC@28)o?jmESKBdM#HZorgWCTrOW;dX#* zZ7a&R_MYvq25Q7WR={YM7f9!c?(If(p#6_jT$KVm{%vm3%nKO^tp}R!d}V5DE09M- zffCbtGJW5)fPTS!Ap8hRNyTqIhv91l69e5-9hF%hwAXVAZ{g$M55R!uNGcJO4`x(9 zT^a>KxRsfCO2(zh>N{ayctRt%ti0UruOk{zB(0dgIdcI)3qGyfbdxeNPKC|=_4R6a z{J{^nH!buK^65qV6EM&FW+R4teu35W6TNc);V`~0xhaRvTbzt~D#9*<|@H$KmP5wSN-_-~uvickBw=b6u!*T(NcfK|a4)9|{?>&$gS_u~Na zTY?_Fp@1*0;XaP84C!C|LceIVg!a#LWMxNJVjqZX$j9;btc&Zm6lw zz{~ckA3u2FRpCogV_TWA&CH1x;yf`yPno+!lXx(*VwcNc8983^6=OgmLfAOgg%uDO z;O*G1`H7b|EQsPk>&riLb6a$L`FQ?X@3})=6$xRNkDn$w_7Lj7vxitt)`E@ow@K%U z_gRp*BrW|J;UN$@4{+W=S&s>*b^vXm(gVdfz~r0Yy3Bd2n43$)scACfI#~ zKM}}uD6Gi^uTfnr ziU`?aph{i$AA!NuuHzRzHcbd6fe|`m=&pYldscsW9yka=^tisw3?Qu+L*GezYG&U1 z8R#1yw_=gxY^zoeRvHE z1$@f^A1gfi3kH->sv%Y!6%%k8ESv8O2U013B87nJp%7mQW1sEeTjM6iW*yi=RJ-gZ zGV=Tq?DBlYOS@Jr_zxK>u?7Q+IHcA7^(QgJWGQhV*@&^j?mg!Z@E**fp-;*!8{!an zHztDk28dD-Fr9ulANG1Q9t9lI!O52sRWTIht_m$(X=xlVdr)Km5)?bW{TV_CJ?%n( zrUcMxmomfs%V6Y>sCU2!&?!D;vND%9`B`=~!pV=CB=^prGiG$JTAaYK>-9Rgb}3;I z^L!M6x|&JC)>W-aQ#xpgXB^t2wnvWVMIT$`W}uW9N#9G z40_{hIafzdg154?gj=#fI$`lK=UcB88CVV%s^=!E?_l3~=37u)4Hq8fyj{?6swr`R zF(V_+Ou??01Li~szC*tE2)uuNBmXV{ve2xezY0Jwur~7H8YA`&Ktt;zOJo&#qtZ_r zaQ}5t!}<FhbTowl7L=_YW z;BpKaZkuk;w6i8ACM9`)@7Yu{Xm*0aU+~CYRQpomfUE<+*w(u>$2gec4UE}$Ju8mT zR6yY}Us;?1RQ+j^V|f_3i$@^vdq8m|Ypn)|T7@A$^TUKsWSy8?8DjMrDZ?1HXXu^+ zQes`iVaDDU;R5DhrVAK5ycE!k5J_YRZn>!BkK2rh(dZ4gy#aW?2y7hpX@BQM^zE)j ztpIelXod|@y-NznxB>cb_-G~aSD(6)z?c^}FzcgGz-z!n^J3&dOSo>#REgUi$K&$B zuSWnsaMc*rLjgS0D5`yNhB`7bf|=!EN}_0vQb&$a27o(=mGvwD%L#Dn;?0^d?}YaN zJRUI2V5=5_W!y-h#O~6-uy29O3awE)6>bvnn3HI}3aMG&Y)lhR0C@{~ydiC$)*Y<| zqP>@i6zIj_ZuHNRohY$49g$Rj5Pf8vlmF?{*K=>#*lZ)0jEs+G8RsEZwF8WRfNKnM zS86$Y5;)2M*`Xug?~eETtX06Y9w;=Hymtez0|wh}It`*1MsC!w)xbj_4Eic+IfQ8x z!l%hCpE`x?y=U0+`oBL-_nO%PhP=J?Y1Qf)dc&vZqRtD)gsHwbQgi9~l^h*`*xOv8 zNJqbk0p-sCr5mV{Ty=>4;oJ}Yy(UD&FoH(}n4yUD1ZSZ=b;8v4Jj$z_TyVDaKU0Yu z-r3pNVMGD6A(#r$e?CmsfyN0$`oI^1^BjRhV@ROmg5I1I|V92BSF;xBeaaJdKN0;sux=UMIv1 zL=u3IgWUqK974=)14qpjVq7M{CY+Zu!VVa&h{a-^>YWHYE0oUz#}qJ@1*p2ra43v7 z#k6PDA2D#@yGX|10MrxstB4J^DJ2yv`AYZHx1Js%n9*EF(*}OmJOkJdpx`MmS&xJw z4Fu!Pcv zxLwqdpB>hzgCkj~R-lbIb}{lc5%zmHwY|EQGfW#n{1Z7y*g|g$*~dWXngp8uWFCa> zR4l~a1o}!7$Co_gJir$KTxuvl?$K!QKy{ZRHiXn|0=|V56EJ@3Gao;CB&49Q>+D{Y zP3N94@4Nw!?D+V4-`!4s2S1jT;uVFs`*30deED4djRDj^K~WokEP-_&Z|G*=S7)NY z^$?<(fcy?hX6PPCBT!rVP`3z_PGGHDFCG6|+60;Z99{XWH9K90TN0~v@i(?32O`UV zc#rzy2iq3(n2vnDssiz+?}Q$P^;;1`VBPOkfm)6VArvIARwS3`!@@&40=TV!nIhBp zwCYgr;k3|g=?~zXvtD5QI$VIT1w{@K;vj{jUuu_X6r$imH60nJWHC*b}f6;2~`oM5}tw@A%{>5}aX2l9YVx3EB4?~lN> z6*ND9?RNsw7r=U^y?5b|onGG07cp$YuumY48N3d>tsl3mnGNY*u3OEJ=yGyI3t*PR zhes}vC=a2ppg{mJ7uTr}@v*_&|vE*@M)ayW&Bq zn8je}GENQ42Y4?~yRQC|5bF3?lc!PWU-{(Gif-?BK;%%`ySXtW_umhrF%1v;wl#=d zdSH_hwE5AP!D&BRhfgd@hV72VbN;|GG&kRv?!GJTh>ar|Mz{e!-Epv;_%dhOibJ2V z;=h6*YzKmq7c-np;MoG6Bg%*%jL&^A<$=Nr4DbIsh%H+z`Yo8IS%6jn^ha0#emzbSwz9iwR3(_O<4Zte zZ0hNWqI;TK~b#Rd+$YU66ubx%#t%>Hwq0meT}0tas0a=Tv}zMoM@ zMgodb)(t)p2?bz}z>X&*CsqT`e<58uLp~RFVK5F_H~xTg(SHmD)DptB3+A8528#*NZyHCWjTIIq~ z)hKQL{UayYG$(a_5A&bWmH)3)4W29XC3-kBwt0Lm%a-#OVC7j&n4rcso6S#Z8YSJW z<68po^w}csEhMF^Yiws4m9pYl-?bXLziPr%;`E(M(h`Er9$3iQ_vB+yGzF*Q+n{j4 z%D3)qYy~eDb#+HnG#zF;NnL(+*Gm^uTj@IS{<-xHb|w9D3EQyEi=sNiy8kDZp+<3w z+8m>2u--n@fiMMzyXp2eNB8DEi4{Gfh=c{KQvw#pb(UIU~hd;x$+FFEe{jb|Mi{o1Qz(#xmss%MFWEm z4K=5ny8i`_)GxqDSvs%vX1&e0_^pzC>DQV^1mzoGaCQ%T7wA$6nb`>bBdB(KJB>Hz z$?^mv)AcWx6cOfQW&ySF*UNejOunua+M~XcNmM#JUXQ$pnQ!@FL+8#9DstPTNz>QO zxp;N23NN!aWk~_#_B_0j&uX3hnyDT7%}-$Xg~=8u~YxNu_nf7?6ohN-gFmPYMHqDT5UYB-D-EL8|HxN)308b zcoH@8&!t`X$bKlNv2eyE0B=o*{X_}|#5{vej!VK|6Jqbh^C3T;I%h~7fpv8J;RlhU zn(rNpE#2*!FT)|3HB2uZa75m&z@_C5sR)wvNKujGk1BRLe$BC*)}z$3rbL~cAIc%q zNwn2>I7O4LUy3+U3{jpWR!F5Wf5yZog}iZXHSUoug-*vKw@7DXcG=C>k1pP`lsXPh z2;|iU9Dvp(YY2}F?hcUr?*+Q-$K8{PW-8=k5+Z78eKqfeOl^iYtt)+@=Y^&<;&BXGIO9fYkgc143FvrErvfM^Xx7s@`5 zZAngXAUM1G+NJh*;|t||OJ)YXY7ZB^FPE<*2xZa1KX`;rupe$QW*~iO>wb%!y?>c= zLY*|$IZRm!pu8mYl{>|Xe;h&y; zH{!8|J$l>VI%y(130;Z5cxD9{M-xoIeeXtToup2@7)eI-jOPhuUYWaDx*TFaPH1$> z``&fRveYEXlZ3m1Pg>{>lq60$D?X?G`T!ftAw!`5xCu!o)f3UGlO?6?!O`j*7^26Q;&CP+Sx09qP$epmUSY!@&6A@KTxYP?6!LKzuO5$%PyigQIgBf12P#D7YMcigwSD0WwUx$ zNDtMjlCmjbe8rRGu(p43mCXPys7Y5!DqI(bm4#8b0W#EVinqRLQi!_hMW+uPFK38C zOnrJRSz-?o)v#l;%gMj`WsTs8SU?6PG_Z1(uXJ2OmrH+`<_XV*&WP|CTA3P^<%^it zWhc+oNoi94TkK!&3N|f(ir1~3X6M0FKsL`))AyuO*6gRUq7Q)0C8>=w{ezkEPkrfd z-Vah& zCMHpzCF{sGA*Ke?2GWKNYtyRd&*?IfOR|{DB$EhrB>nvclofX{p^h8pr?-~j3vuu= ze#ly2fpn(JO&~B+O%fxoZorTSue5?uY-_Z<`MZ*Gi%1HBZF6ClA7^^=47pU4Bw=k3 z5y3h~467eejdZ2WOz8&R`2*qC|M>Sf{kYi82{Cd-aErIe$Qy4gNoAl`)Iiy{ z(IX;GN7eV(E6s<=`k3Js1;f{)34aPV&{_;kG3k0sG=ld?QNqv}vGo4y-6IQvokZ`W z+;nUGwj#PrV{yRdb3zXB4Q08!+=XhstR?MgGpGJo6eS!k&aF7Fxlf>&s3mBfSfve2 zFdOj-{IjK_E|r(5*EwV|hOSmcfxj1wlt+?}bJL$4qBQ$tr}6*UJcqP!r+2;N%fXlQ9F*1qUj0~pL49hPSD>l;w*uS8JMe5(reA3Nld*gmSeDVh!>tVOd zjGO9KeAdQ4PGyz*Te z{0R5a%jO8cbhU*Uh%&Y2S2mC$j1>LmCsawNV!*sI(x7##`KM18k1+Jy6Pn6yJN4xK zcbmr`Hy9qi?ONNWY%j*KZj9X_0U0ay0&PA5kf!bN4s7j;@H)1SWj5$UH!i_HK@1&} zm)V`v^P6h!t1f?}Ms-!CYBHF*xa_zi^oRkdtkZ*+;&6J@msl$>f}HiMai%NmK62UI zyth6-z2^PI_u(SXc`r+mn-UL{Ud;^k%eV~GPtToQ+SzGLa0T3ZJbZfPiP5JkvnjLb zPs+#tQKoyavqnSePtnZFZPh|MV<^qk&v1?0FHvE-?wXsLLea2O*D8gT0P|*HAqoAX z5B8WpznY`tAU4;tbGan0Y{iIVeBM~0KY9-ju3Sp`+UM@P^nME@7`Q^r40djj>u!0) zbLa4uCYe=Wh&fy$&-N4T`=m~&?na*6ukCMiPJ4aVC|_3pvm^%$E&Ye90_Trm_mJJ~ zei#madJ46=QE9x#@4$sQ)A1IApPJ1~;lLIv%{t$_BgKaLrH!V4iSvtJPn{5xVY@ty zBH-XB%5;nz#14a80F<^K^174fztXet<$Bdemcu?z#skNf?0>uo8U)ywI!O1n5ntSV z?umGO)?-&Z9*@KkFd`+vpag6h%I3P4zR&zn5HKjad$j}10{g_j4p~Tm#a*w-+TiJy zSIh$Bq9COYwRSu>Azny2Fe7{HuhdXizg1c-#ItRqJ$cAAb%_fm?P#P0F=HE)%;D?| zP8ivrRlzU-ob$A_1R>x}M&UyKjg8ej3>-5uJljiJEAqbR#$3%loBp$Zi6O)cDB^s3 zr{Xh%D4Pddc7-s2_b{cwWK{%4PgWz%=K%ZG%el(Ge{#i&T^x8J`8Pn4uU($|}< ze11*KuLoe6KAxkGxPe-di80rgxR7(SQ%Ga<@fJs_p}KPmQ%Fwh5H%)5)zv$KmzNi$ zRgI(PAljz0L<2$0M)1r4vjpZ4>oaA@tBoi6&C(%GdM~+HzuZsSyZuAHgF5LW3uCfa zy~0VeM>jPV=nKo>6=W!&Rjdb|_Ha1e-uRPGt#)^aS?~A8#p}3hVK26|J3C{CtUdRt ze8S8a*az&xN-gUCwA0VFX3ki_ZRxY>Z2}xfwyJau7 zbgi5w6Ry~ysc-xO;Xx#dRy()FkAd5BhQ{jf0iFG@n3xcAS%yU z)>$^`vVs;c^T%^sF}ANJG?FvZ*S{k1N;Bq^+){sHTpjWs+{(rgVS(t!3n zs{tzgMgTbC&M5kDJ+j*4*Jx)tp`@(`Y35v^t3eDKG?`LOpI3SN+q*CcPJn!6;0M=? z-h2yb$M%EmwMG2=knqhHNV|s;dIUoN-8x_k46j%iOHSsZWO(Sp*;@Hvaf(1JhtjT3Frx76@s|YMoZw8zMax$m>4I?$?)It!W4rI4Yf}r;b80C{ zY2$R~Fur$$i4WMA)`mFaW83aU`;`3MxD%WSK)If^VOfl7s{%a|h&!CQ=n(duutwuu zm2|9JuyXV#;VM>myFX09FfhrpE7*+vnU7C=eDd?oe?=t}VrQZf<6p7fXTYzlz$(9r zYexhTVVGPHKOAc`)=~srz7@;f_wE|zj1U0J4sa-dydnypKy7mA>yU!P-FTm_YM~^2 z4Mz}YPg+N@&Ey_%+IbWa39tU?d`9zF&_2m(@OOXE_W*>!ZHXU%`E`%yXaqM1(BS_v z&okm1C$fKvhkgiu@gQUzq)O+0aCkI+mfm!(=2mf={iR}qpV_TkDQAS4kz^!4>yhPw zy*s1`%SDrfte+5Do7Z`~whC(999pIzM%Fx$g{i(P|8QabJMK3uX~Itta7e z%rFl#z)578ET)dbF}=*tu@ibsmn59=Lx^PnhabOx7~zs{OCMtOyCU=B*2PGC+^O3e zp$v2gD#Nr7RQx|&8qT0p+#wihZ_V@Vq9Sk^>LcV>03Ns@Wbt~vJ|X|0&lh<>q?X^q zc+MhWf6DlkdbavfpQE23yle}oPoj7}xBUsVcq%%c>~!tdSVxQgK*qOKKVDT)dzj~< zFkyuCAzduk^J+ACC}bjuDCgRsTwZVit|A(!&fQvG1-@gr16i`$UBrPNblx_)8 zk&qAs3F#K;1_Ka5z@;0hVUbc&hEl>&ksJ{b2?<9Sdgy%5i+xtt-}~Rs<0I?JFmvD6 zeZ_ek=l7t|F(N5#Lbq>p&DxUV$ntrfW(;s_ha9$oYzw`GwpO~g)HXLY(@f0lXhhO1 z0bz71T_a@(mcb>@4Wdz7@4%+p<`CcJ1C2_`pf^X=B;T-Zbg<4Mo=DnSVA}9p9xvfE z>k2v2K(-v){AYUMeWMHQGZL}+v~O+k@Y(ZWCkE5DMoXYeaP9aezG(eIckK@$pW#A> z!#m4uqv!`OpHx5MY~u2yt#8F)y@h=%b|0(H)1|ttjcQWL^a$!#1^NAEY!hmqgpaZr zfI14-zqM^Jq2Bo}uNb5bxNnS9dEg5ihCYi7d|;PZd2ewjUq5_U9+%$bthA8B$$U1? z_wOdb0HrHGITx;T2|i?ynR%RHLA~EhUIA0Q@I$n;-zR$wV9B6@G=f1Kz+eo8DzA;698@38(*4u!iQdGMsw7$-_xvQdX;hYvx$ZK_tgC;az zza(+Ze|d*2(_cFd?^lyqqb%|tZXr{D*S~x1B~ub--CI`w`OL<>K6BGs;r`3Gt)|yQ zm)41N(gRHLf>tw^SvO4GCVN^$rdN-#uJ;YRabtFy?hQ3gh;FWtoc$EFeLK;0LE*P* zK0_#}eD=Q4mg}wo!cZt5l7I%$FMz}IBajWW#ruE^KJsQC>xhp3g3iiYk=i%#dENcS zLOt%ciA=2?Lmndk&DTXF^s}Wm)VAJLOx1?sSO)4FKheD0c`f$f19OwF$IkEf-&*ol zE7Kbl@f#LdCdq6$qmDfkdUBB;ie)l2ZvJ8L$>r^tcM`hkTJS6>&+5Oua zJ{Y`nNT*+4FxnDAl<{!2J`hCa=LP*Y(o#>4%jx6^c`HKh%eHA3YR=$5`d(NyJp^g& zJ76aI(nw!89N9M71A*8^YfDAuZKhuv9&4-RLdeyyb-K{QY~urga>5_`fj>f7aM5mT z@DwzcL&qK1YT@&yvVt68{AHqm&8b`|LTc~csFIK1!Q6NXsHvDz-pR@)qgvpiHOKtM?E%Hy=ZSD_s0yKfN`7EzxG zj#yIx^}K(aO9MDI0LTyrJOlueG1ux|6IxL{S>Utiy!BzI>Du3(RA99U9Q%^r`+`M+ zU=n{;sOXLDomZ*etgq5?ZD{!dAULDaf%HVH<J*XEXP7dA%Wn=`qb9ONkh^&5h`(HU);FIL^LK@ z$bSB7uXHI@<&wr#ZyN|90yZ^^JV2f=+#paGE$;%Qzj||_$Z|5VYvkLHnMl4%9A_`E z77okhb$7}il4>*pq|#gkg#SvGEpS42CPBFES~={brB~jo&CN#mC!<;K%vF#zC%^pi z_$Kcs_~e-o?s}zM0+hgX4nU?Gp4yZtMV>+2lItd*(gL$#eUJaF&K4`80Ia;;C;ht4 ze;ApoAvyyGI^?4W7Gt0)*$q@emHO4v=9;L_Jb!2`&s&n@H=jaJUgqX3y#f<4VPthM zJvt2HC|4I3Qo)SB*$p}@vp}(fC(Z+$V`F1PbFf%MAGl=K{Nsa;eiJx|zO1bCg2$92 zIz->C$DHQCr3Yu8J>`aI1E5L2 z0NU~C_p-$`gPt5=F4V@l=Q;;1+-)epVxHgFj`rZD-NbDzvr3kG3M|Rol_?*-z9h9S ziXLbMM`hS42I{9WSGghX4zft;SM)4@1;$Rc#v6W0C<%HIKomkYSjbsHslpFK_Z??; z2vSCi4%%gbcxQ7k?=mU*`j{}EnffOtlR|Kl?(9TP@zWFG5>ta9+!~vik=jR(~BC6@L3;bwFDn!fcm>|f*Zi6+z~kotV8H* zz!~?!qqe*9SO1>aZfuY$#=2Dxd-YMI*GuzsSLsv%gRfQfu?`35`jS$ zokYcz=!3QeSOr1vMUdhE^$9Lgh-KIX$z@$Ouxd{Dz5;v)WO8&^0bJ(MA=V$nX?=u`bj0+UZd;bg%jGT0>9*lfQKH23nsZnGnyzc9 zLoCnxs+VC@=ik1yaQt|2R}c@G9Jzo9|Ni@DoY~3d0(c}zX9SM_2>Q6|jB;#JyiorB zxAfCVX3eoY*J8`2dwbf(3Kj_w<6KMDX_=>WBg>YTKIa)4Z2!|0+PW|~;0h*LWExCs zgN{isnPPc?LIP4N7~#Df_R8Kf*9Bd1e8KBjG5kJeSm64^>W1X;AXw($Gb6wB3<9cv z2cUL7n-5t!9+Mi|B&|*^Im9GA*g77Jk?!syu;BYQ_4Y3c zBMgJ|Qf1rh&&-H95r2}%H}F8A(YyYJb$ur|hXTqAZ>wF>dlT=sG=>F@npU;?RcS&Bi_$9H*y8U@d{yqF2Gk56DD-fNbdD$Biu%I4>Oz6GQ!yFK^XIXV#FF z9TPZ<2}Ho|{>*ypD_>rmU)dRrwxL2kady|cwa zfS6d2mq&4}Vn*^%+qLIckoyLMw-fGgO)(FR19yP)>e|ZobGAh1|K^eO{K;Tbz!5o| zZ|{Z{j}d3F9r^>AMOFLs!g4Uy8P*KO^d`F6+M`+44B!H?Y-K_|5AZcbQrp@EK#k|5 z(QWV`fSm5&;Bdi0y=13-UvW)hk6Wh{9e}!YJp{omi0BsH{UjE6E8AYfdb(P)cL4}m zU1N%S|NHtt@Qk=DW_>pu2iLF9TJOM*q=EIbb#vTx^KOlUxsJH$Of08&;U2h9OO&Iv8cgcNtDjfL*0w$b#==4=1n zayfFx)#TasTvA*OyToHsZ0M zMU;ZjVmXz(ldI`SgycMbm}1y#((&U=CR_c!S~zwN+7KS2;%G3|3$#hfgD(K^6e#H( z$Lh~Cu#_O;z_J691H0I|Y=9?Qa_6gLTf--+4E`)}vG+l+_k6Q9g>yFWY6ioOou>R%*#RnVQSaiEPvF6T)V?0ic) zjqD010GmR02H8#rc{N z!ehfkaTKxzvpF;g%|fYhcJSe$;)TG1b?}(IF1PQWaTb{aOJQbRQG`kD>zA3AJy&xV z0Ll`YwmHJ$(>AOkGEi?@R2i<6`w^0YJlq--q291j;|sQ;-PMa<8E-;1OxZYx{6l%k zy91^^%Od_!mCo4sczVJZyU2O}ejnaCAKDnD$Ek&WEIxxfQ+=G6fpsKWdH2Eu3rzitl@Ew=vW}EBg zn|Tb1qZLc;Pp{o97^b^?I1vL|7HHjPr_EPWR^B!fdnm;vvpWrxGUDbX3&CDpTgG@n4BOCnzp&TC199ZSH>b1?vkOrCBNtdFY@KCe6R4g6O%UM+6*N zcj)N*(uUbkwFoYc^~o47WXHYm8a+^M*Yn}aPku!FY%)1~5883&&=zFTeO*GS=}6Q9 z18O)5;Vyg)NkF0_U>a?C>w79h7tq3adc(=c1%~h2?Wjz{W^Cc4af{tkoPFtnSW%-R zKy4cxlf$}>knJ?4&v2pY2c zZDjK3W=HTpJ1g+^7X@zS#5+-nec4Zo2KTW9J}~hxBg2j390gq(a=r`S{dhrk2WAiT;F>rl z7d)kVY1MPjoP3J0?wOt0$dVNx--S#j_OOta+cC84EsrwI5WbpwU9K#A;mbP?gTB51 z*^BwyCfUKk>PqZqF}iu`yfi1n)j1Li9}(8Tv0$zX`A&m_CrE_0J+GJKOF!RK0V5*+ zdG+j@JQ>z}LC0Gxj`o(TD}kR>wNm2^NlJepM>`LyFOX5&>o)TsOQIGUECp#m{r_3O zwNqYAj%)zHG@&17rP)-c%$*Evgp)Y3F;8Y`Dl+IqX4+ku4aPX)otW@yaV50k*&uh_ zxpBe5I0F;Re~~jfHoyiGt7Ujbi0OGjM-|CgOOD-f(er+qtLfN^;f3&&&R{)SdFwI@ ztR(GGFe;ZPJSk&OkNWqX&3=e#AFFq@I2spD_og|aaphb{W)>G&VBy$uZ3Gbu4!G-_ zIxmzdG)3C4(kX3^96Bl(D?v&ieSd*#0Hgi34uW;&))gE!^fwtaZ9}~DsmsVNaB69d< z^tC-$F19%_#^z7D{QkU-WKl#=%vPpLB4Mo0le%j1}LYTq-BT(f3i zqdBhwoC$1ui_wFX{au)DJ^YxX-NV$4m|9%ohMiG~|>k~1rwj5SWrqr*Kq z?*hS-!saz4Y$1_zW-9m>JbAR*XGtsisXncz2=St^f$7$Q1`!tcnDaG_o+qi2Gei-k zz(+!bO|}LEzC+hcOyG|WMLI4NbE*Dk;aLj;{vhSI=m@Lg@D~dJxqyHe!!PYabqDw7poX2DtEIBQatR;zlE_p;v=8n!n##gE}$n&JSu4Dg#_Q3$>% z>1pJDlAm{>xfnWaLMnHuQ)_U>S_vlw$R1I0U0O7I`1uz0ad`dGw+XssY~j(sla7=} zkKWj!-;E>fdT=6lnJX^zC-zj_wdb}UMvutLpWbmbMw1?IB{cwzM8@>6{~V#)i6B+* zdQ2)$A`UklZn1bM8XlY(oID9PBlgZS;?)!6?N-A4l%}7#q+ZSVKSiPq44==1WXa^V_Fnb+=tL*xy?C$2n~ zbP5l%k5~9U`+!}gR=QD_?x<#7H?%gxvC~vS!7oDHT^wPsZ(}IlPhV@7e726`ys(6L1+*?ZuFKPI#Os_WmZiCAj<%}|E}XfcI6vRJ24-<(f!zAUt67msGo{{A_c+KOXWa9tz4G#D z+|L zvHn>Z#x>`YW0J8Y%=jp(u^YXjreh2ET<1E=UjLJjHMn89@G%sV&^h{RqfSl8(Yz1W{M)V@I;IgCZ1zR}NqXdH} z9KFDg8>m`ZyERg!_N$mM^ywpm_+yG&mb=}6Ddd?%Jv6%Q%sx*!oK^o*(B;5`LAGgJ zD^V^uDr&FtByI2!@%_KvLvo_7d`|0!l-4o1?iC+3H}P&)Vf~!7b1?Vj=+Io^vsGp{ z=-&U}8R_iFMoy)w?x;Jz?hyG&)ihms_Sx}B;oxfxNz^%NOgIH~7mfRxO4#)yLmyGP z(USRUAN#=l-41B z1|-mJ?|h2|iLH$gBTq0%1=ek-7wGkkb{v~wfpo2#Trn^qc%;Tj z1!;twAct5Pe#-p~{CU@-m!4Bo1GH!QAx8*A6sq7sLxqc3Aiq&?poPOX}$XU zn#yGx)`Kg@ca1q%I^Hq zod%pBgX?B4&z8HEN3^X>dEkzT)e_cc$G+9g*F*=EOb$4Vtxk4Z_HMmcSn!+p0$S%O zN_i-1K&f5#on+LNoeB!oUuukK&J0w6Q`aP2l(L{zq0Yaml{ob2>Q9%T6k@t&&Z`2Y)8VEMPFyUj$1GZxleil5 z)Cp^4J$4qdn%<+0E=EuAun#kU6e5*2QZfG*{wEW{CmTPCKkSV*rsrEZWGmjbe|<|q zj{V<}HZ+QsaJF>(aW`ooOyNTf7Hdjxo#;A{*^1FwBNFA@3)2Q%wuZ`I2vgbN&8^*I zCW$FOz1gS!R-oMWcN0?XRP-quy%-v4aa(U)Bu?8&u!gHK2xm_>FHXN~ecrG*EuwmR zi_PW8zoX4idVdP9{t@vMW5Ttcf_Tp>jN?`XcxlI71f5`=(1d<@Fxcf@InZ$TEg$aa z;R>!)pMkWp&M}1t=T7o!Ddlnw=su@ibkk-jPnwd+7Wk|E*YNOIIpc!8yM3+H(lyr@ zUy!Psm}8i%>1l($M=u0;2w4~DDKVDd_u6OEsQ13I%3WM?AgtM)9dpw_Ihp;@u0Pl9 zti07IW^Fzh?*CgaMrF)Al`#=s)z$ z4K7-#4JTD>=ibRyzS{Yk&RR*xMk=kN>y1-!=QOPC3IEOIe@#+2o}_qi`|AtNJAvgTu4-a^?uHUD6aH@m2FIz%ZKc!v!H)lI!)I#ZZoaLCR+@3OmM>S zQ~C_3ZOZ%XGHRS0$eyH)oa_HJYd5Y;WEY&z(Y3tNA@(@xG*0=~L%%(42pIw_;*ar} zQg)Gpzi#+HpN7D&Kc&@ilGZroO;mu|zoYD*k1DEzihCszHLX{Z;}-|7h9~{|4F2-~ zvAS=B&|47*3xYkUjYF;{U;?yZcykN9OfFE=O z!WIT{W*6&{hPU{BeRNTo>~r4?=}}@iSmele)&HJf;!D$VCMN_IGK4id6u-vy`)#?U zbT{QnR6TCl^K9dibl;hO$H+g|f7a&2@BRM&CJj;Z|1MSAe=k_`Y&xEEd_uLz`PXy) z&kK17T{n$tarsyn$Hp8?oDi#UF6jwZqhZ^f-tqE|_<(1RSdj~)%d6mlQyxgMH~wbn zg(|785`S;PDfHt$-Zo2|?ws=NEnYeCM@UxS~DGfHNMjJ*Z!SzQ+gk_cs?sB&i(&?qNlL!W(+r?KKsYD3U>a+2}$cQQbp@D ztnLqfK2ddKVsRhWuXDB;&`}v?TPw0Q!@IO`hnrG1T;v4EQ%((Iszu3TgyRx5_e9;D z{N_E%P;=?A+eIB@>?Rd*DtO2&${fTc#)Mzdn{tm@sP{ z;07K#oYKQhft#cxC(-tpS}s^ebyYXn5YxGp_WLoaW=L`Rd8X9?&;=GHV)plFaOZ)A zsH-h3e2IolHV$JALF=Azalskn=!`<47v=~neXpDFVpHxF#x+1}EMDdW(irh3X+1_- zB%20X*!HEJ^N&lAtdi3|&ev+Y;=_V-gy@_$5-j_#3L$p^b_S>;{u<~hu+Txv&(1F8 zPMc)G+XotEN(0!e!_?6(C7_UKl_)$qtBM;=wzm${< zE#e7XhtScc0-Sto;daI-?0zw1TC&t;eLc)--u#hn1#~DDbO<{Y?JCh!GOtSG8gR|} zO6*=WV6zOf5XuFWZPMV%kw4(czt(^$HTiSS5!Jbpw~(hVULI+k2xlGpUXE<|G(E4b z;Ke$o7CNtUOvds}a`6@@Ctmd){3I>YcSXrsR6dUzTgv@KONWCLIkRbMr=eLK zTWHzWz?sdjQsV3?syZl`@`~23Ovk$8xz`5*DJ3thWT>2X{_8__JzP8g6V8)HYpeg~ zhuvQMbP@LMQ@zk!d={ht-ae?If?3*NGP`doyKk<=0(&3@8mggxP(mkgm`pW=R@Hjs z$k9x}u$!{-IV=&%)wDy`cR52DooO=P+D5VNUJO0eKvFfcKR zIR!v92(nhAUBl@_RoM5ge_mEm%00soDhD;J4-gUISTh`P+_liA$muI^vVc3VEbH{m z)I_rFbclsG*PoZ5C!I4}GzNCIm32k)pZafnwbK-zQYRF<*ckP1uEQ|6LkwIxBV}0l zGsd$R^P@W-g)@Cm2xB~dF6l%_@IC(U{Taapz2&nrnGqVcf}mgnh8R6dNnsAOWU}Ra zfS0;(A#&0mh$ZQB~XK3N0Q&j_`4_A!_l~~4KC47UNJj`c#nTLnKGYembMRldAP0_z z?Ib9YBs9}QjQSUJ1YHjf1%lF$Lp0GnG#gp^{;xkgG*^HSEsIZw{_e{l&jF=rMEqj|(Y|n^ug!;r! zY&h^7!or}{S&7rj%WKCr$_@liZS z@!Wh3JOrp9;~c-#c#D+Vmw;&lv|bNz2Q7mM=OYYA_*jxNuXjUbt=&p+8Em}oGcJV| zJ3d3t!~Na+pbiK;HUW@X!8;6TYS#F(er{X7`TkxgT*nHlSa^MVoglnqXCbT&Y)M0Z zeEkQBiN3lB;2Cw^$i2=L?eRU34rO)d0`6x;j%8p;43Jse+W99C8{Oek54<%5iji6E zh!AxeA>R&4tNXq;S_4IHA)y|Di{QBhco`I&;E7kplRfLPy8u1cnrc!_s7D?Pyp`!% z-}#oY9DOclyBTDRB_Ip|`t`2kG>R0tY0+u_9S~%QZx%>rI0zK4A@G0PhE95|541y~ z7Q-csC|2ZV#T^6f%5vZh6=KJ^c$(!j^_*%yh3dY1d(B7!W&l+KQ|xr7+NPT@>m`^* zp{xqPr5-}L2A>~a+nF&r;ar`T+UZ-gE#Uo6&|Vyq{(q@0N?vQHR(iiIZHzqHELfaf zET46;5y@`xNvwOHV|y!Y3M=ZWG3FBf(+6M5H(f2=Hy6i!hgjG~iu`b*x194>C|D&R zB>7oaiXjv3C$<%m+JF_o1SBW|^6&&P-g)iF1oEg-)j(4m_L4{+x16lN;Bf|F!s;vMZh5xiiMxo z2j~C}lu95pF}{MV5r9)53Ic*!fC7+zO}2sm3!pOqSqSc-RHSsJ)=C4ROp$sobPzcM z$e@4ut;pQ>L;m#HG-${?lt&*1<)DBC2S(H&z*kKBt36R*9X!~uH$zti_Gd^j11ALN z&6wo8v5zd)2mW~CiYQ^j|3Z){(O#*0vf5~EVeE(64LubAT|x==@f^^4fB`{)#uNf{ zfKwF4JXpyAI*H;H@``E`!63a&1z3?>@M4C2IM^YYW0Cb zmG~BDY>sncC{zo9Vea_hlkU!Wq9{{%U?Zd!>ES(h*KI0-Sgi~|1|?2dTcHO4Fbjvd zU;V(^fhM*TEJ7>fmOX4K0*r&=m$qN&0n^ zE)I_*pm8p<`bHxYIN(E(NfbJ(!2ymGYUGbY$$~23U0h)$V z9Lxx#w@uou4*WC2@Gm=}NuX5aHMMFQYWq8>B1i^GlYj=VbzHyo@F>}hIm141&$ z?@dkJK_P;O_cq(VR7Ay?N{~Rct0|QhoduHXb67D2fuQVUNbQmV+#IxJT z1o#vH;ep$SMiB^MVAq5LKq{f5s|ynDZ+90OKr;s-Rsjl5od&eC;_Eh046mN4r+_Pe zX5^*4B;RqP-0K`bYyzN`1xjKYsAIkNaBT@?tyJHy1c5a)_8bAwPfsYR{t{s>r4Ol; z1h2-cg}oHVcy1G82@u552hKfG9 ziM86_TVDHmW{^6am3npOaHc6)s+xNxMemClgNT=ZsoUXA`d{lqPPtlvcjWi)=@XTI z$J4QY<;_XH^y@)JS953r|L+!sJ+s7#AeW>XSDFM!dZP=>4mCYiUK-Y!)_R>~`nmVk zR`}cs6{p-~`YXxrp_+qKQc;)9lP72TKAqDoCPT{PVccxAtB}et*t39ONoeTQe8!A##DMt3vX3~!*&f>P$>;?7gWHOQXd5tl);y#;M`&3(R)RW z=l?spnMuqienCyTY17yH@q?$LPu}$&+^DG;NnuJQ&dskbFiE?R8{n;k89vzx#q!+> zGVSHq!*C#YR-bNwZ(fL02F?N56q=*c9gKm1(wAYTw`H?!DmuN2GHe?j?cbE*7Z(VLG zI8Ua6Z@f@2LGt}k@M|h2)bWSI(hHtynoEsm{2A9!CSkkHX|=7^)ZEG14KITIWV5)# zwjj<>ge_rbEHYs~vYs5g$Zw4uP3e}1bLYKAh$%ET-C0@KT)*N>Z&X@5Y4|vGqgR4X z);9T+LHFYf4x)_%cza%n4PH3I#aFv` z?}nKJikXlT4fI4rS^|0)*sS4b$J90M_X1#1_PymVA*9qUKVOSJs0USl99P4XNd{kQ z9Bv6u?>mgi1(LNN7K_FB@a~g+>T9IRuCe3r;r9`Wf7Il(LwM8X{#{Xd6|ApSbOXow zCJ%CRB(K}+7j;LmdIr6J!9JO;E?#C^*Dsw`mU->xe8^wz*2nOHGJH5?<4)!6+b3ZF ztOB^Q`2d=3?>(CwjH@vPV7N zdnj3p_?L#uiXV$EByz+&IT_Qh$o*>9T|{Btd6#J)l!tqvE7{cmq*RfIV;h+q^#I{d z!#l?oW?C~q;bK@mrr8|Md2q7%x~tx$)|VA_8p($LsUcd12dL5&CfrhJ%v1a}p2`<) zn(>)l&3Z6;uX?Fmlps^ajh0V%d{m8NnRYh33E;whZ+O%p7zVS7IWZ^Z@(zGUcb{vn zFNZ$ck1Xs|?$H{sL(NJqF1y2Em>uQ?@epl<96P=mht)S7ezz+1-KxC2_6V9wa2kLLV7Dt! z9`y=}Z@+oKbE4B#ilSiKRh<=csdSOeH@Wndj0zqTVD$V3RZQ#U0;r}zCpiynEHApH zxp-R(f!q@bm@ZTIAkoQ_>R8p)o0HA(S3r|&WAD>u5Y1)?dmC+0^UBmyG+|v(@Uz)?*kO1 zVhnN@eRCG#C%@}gl@c3Vq^X`-N%;|K%OClbrbOxUikNTVP5-Q>CUc*0@rtoM#QWex z{E^hGH;Ue_Y7!0nFYG1q%z!LpQIZgLs0@}AAnMjmtBvXLQ9bW`nVIo9Uc>R0h(DNK z*J6W|jOn4t4ys&BVZ$)-mW-C(APX!w|5W^8mK#_GZ&}BGVi9aPHj!0#Bef5K55fL4GdHKr4PEPkDN-jdsdJFCFKzMedqEXo*kfn&x`{ur zsbSyA%-*Q#U5cLC%`B_uO&_!OhS#lbkFnLETkxy zY;r`c45f1X-LBSx47%|OL^0cf^Tk#aWthC`KdbPqdu%d(q;xnvPIxY)s%5ETK}hO( zuHJbYv$6WPfA2Gwb9sLQgg=l(;IFkZFE@N{xwY7O2^V8uH@Td%hOcE6aOFm$25Pcw z*5Afs=gn$M6frBjll>Lvl9nEl<3GV}NCr;(JrR34L%J4O*<3^uMO zj|iYP@q&YSP}x3F{8&EIc=B|0znDLkJ4Jj+WTVV&_1c8iny3Q&?Z(QpcfyW-(RGaW z^KV&f*q{4bu54w>1yvEOHe+2Z${!ckEh%Efi`-k&THCF$lR04)WAs}RN?Ntk z;JJZQr$;KgO!DtZMf72-M>alJe1I{?qIaw3dBXO4u*udWY-V@kn@jKWGIw9a3!)hS zh83KFIG46xbPv2}kntRMZu!goH*9_6aE{{B7Xrb|@b3oEGF(gC;MExy@dos|53*Z{ zDWn_$mvPGE9dHy<%FPeWaETPjgl!B7T56zc7lL(~1e$a+UXT?3QG>rhAYd!Vqx zEz+r6H1f-N2q7=QHetg^3DXbmJRg_A;dS-hCvc4Xd(GsM=?hA#%3H7MfB9M^`biM_ zV~TgSQ#HgF7s4HLtc@?^9$4HO$dbt?e{lQ5q$5-FmjCZhYe?^89{1)$lVEMNUe& zJXT7KU%xUAqpL%4172J-XcAqj|Gn+T=F{&V#LwBzxbTFw9^gQu)yFC0yE?Z2aUtLD zzh;4P-Fq+vpF2KLSmd0jLRdv#f9oGhg-Q~>l(Si!wT`s7xYlZcwNK5TpT*-sGkylN z)l9DeN|@lO8M_m@a!^+87qUp5a+Y60x&e7Q7?SW^HZCbxc>WUDhqi( zKiTM*73LOeE^BK?*}%W!1w^Jb+QtRl3RBDkkIV132VW)UoB_!;tAv4@nGs@dV5g&2 z((|zp+}uE41$2OXx2sd-x9GLs0|EW0p>?+OC{Z?S;qaQ@XWGB46L)MQJ)yB}{jxejI)>`$(9yJIGB9FE0QK+D zXHhllIPY?MwaTJjmwo%Kw;CN0f2;{>5ug=!mwKoEkIZZgJ7v_G2wT#6ejvPC_Dw^0 zO-awhxBoo(=#^SE(f473VgY*$4mSw@iEB=emg5W@7S){M{^ME& zlXN2K|JJ6ncJg$JBkZ76OEI*@7RvW7D`N{mE(1La9#Mj4qvI3Q4h8vcP`RtaK2IJR zRzCF?OK4Qhf1VH+*{$u-dzG3#x+I)A-tiuyO7?0S7 ztTDq0ql>LLVsfkZB+d%kEE>d-y5e*!1(yeV@oKN^fj1D?ceLO@KYn)O_hT??lO@~v z?o=Ef;X;axcjrW&uNNCoQ*?Cb)M+5(;+@QTgUk$S&i>|tSJC%d83iD2;5FEuLK`9o zpOuvqI+oN62h#pj^M{lr7z57_?E)M)SVXobAyhv^$5|-4f(islO$OaY7Zx>XF6qYk z(9v`yino;N{~p`$)vJqO1S@mjBhq)qun!R4Ef!ITKlTtj=6@wQ)Fz<9lkb|Fm8;`1 zWc0RJikfQq^CnU>=Yt53)6DAvyD97*x1|-(VW4UTwH@<5Vp&AoPB?al4=eY4Uiq2j z{V^)!&(A^k_qLx5(|WZU3v%4_+774%6%ygJfN-hX+g%hV6=t27;*Qph-#@=`kICou ztHHz2_Ue(k?=|c=kSZ(;vZ^P`c4r{w8*a%L&JFd?DXwbI3f{VepS=TClNfDwiInH9 zlPMF?%sIy~9cx-3cBaBGmohyy8|n zESfr}XT>W2hi=`Nz#gMo^kVU~Io4dDMa_LfA5P-Oo|NvBhRW3-aE%GA64N6D=(a&q zmNx&>y~5TU(4Vee@FI7+*#kXHpyBJA`~clp8|sZ8*UbW|5kQk3i2z*uAy8qs==nk+6lV$Y_~x6KOJr7xbIXe|&8RBLV+CO;hxyee=mvtTs`uiV@gwD{qfpi8lR64C{r?;DEE zrD3BqfHgn3NA_MJ`*-jzd?`pG|^jzoH`h-J=uh|r47#eECnQ8o@0(o?7fGwX)xAj3G!+W(xQo2 zP$mluA8KETpYw^}2>-jwmV@Rzzk14R`kN->3rEG3|p?p2$ z@6&HJ3ZB1O?0yjNhZ$k^gICuhRcjAtOwRmEjQ6)i^T~=biGF*_C?XaHJH~o@X3*8~ zD6K!(MgkcX!g45gH(c4p@RjS)J-g%_ju6A@lG&{pA z9{$yF`0UxJI|m?a1DhJ4X1yZ@oD%04yOe4>2ib!o3l{`cK=~fT-f=q!E!sVH8b#gd zv`i4yx_95ck~PW_37=9Oo2!HW*_Q=S7gg){{+w45Gqn0t zn=c@kFj?*l{^YFh*BNfZLGA`yPx(gV_-W`i8GOf22?zE=(#t0XHm2O+jKntp%G!#| z7Gdj!GeI@6<;*ka{?(1q)Dv^XWUqZOj46XZMsXh{j(9~DpOKT$Exnq?BERsWPs=zP&beF?m7{uO8HUW-r5?qd32pHe6I`lv4ZI8aSZh zOM`Q}YqzhB|V&eX6i^IN5Ik4jfZU9MK-==HtYBl|jC{t@WK%!NcJP$yYDM-D^b zs@#rJDtBJ&2HMO4j|Jd%D0&T2IiSDr=kAe^F3yNLG@jQ-dI zOc*Ytiy#IzU(haIpxwOa3A$@AJ>Psn zJ_N-J8-7z2FUMb1k}oXPu!Hh1G$&ZOW-iU+)lg&pHkO33Zk7@DliQFc)aG~TFbezv z!5nSw5Uhf*1TY_jhiGY{Gg77x6ilS5U2ft-89k3=){T+KjiSTRgG3#468W~{wRw&~ zo`{b$AW6JEP##r<11t36!+1db$O!lVJiN^(5k4cQs|ixmI|mb}+Gg1O=h$JvBaNx~ zC(3M*WC%!sM-M!Fl;Vku(B;8L(v8y_5cR@WyVj@gwK7^g8+&WH#eZvhB@n1N&AfHH zdf$KFvy;zm01~O@=NBM4PhCkxOk~V8`_nZ;+duYWtvvfvRAWM-I8)NTYd~Smfj0i5 z$J%2a%f##Bc6a-%u}^I&EUjGAEs-rQGa3T3KxkU>-Bz2dTy-G@_t;JA#J5mPhY1j5 zIXFU-Bq910CeXaDnT8X@)`E=-PcnIaicMvO%VhVVKx4|D@|Mrbns5U5kycaF5rKt$ z-UV93&c>PpLXG+I=ty>7&rTP2_K3q|JGW&zs0D!{iZu3qm*yl76i|csH9y+YARZO; z#W;|5!MXD4$GGjQ@sK>E&hFmW{WGz3V`-obuO`!i()S=6YVUSN6twfB6cwmv1M(5& zzcla%XK*lHZgw?*$X7=uu-$`9ka@lEZbuV(EvnF1-ps0^w2oPI*l$C^2rz!`HjV1E z_x3hZiBzYGeDk4LF}d$lS>KeW<2XBh0|Z1c-$P3t0pLJCK5!Q`9`b=6L8nS(5Gu8} z)VIS~z@wpz4+wA9l>1|E?J?Q{2-+J$0xBhA;Pp%dUN0_1yu!U$ld!1j_JAWn%eHp- z!r4>d@EFX;CO|851hVGyN^VXiv*(!rEA7!F z+*C#Uc<5$VP`^m+p`d=RklPP%7YUGDRK$6VlyB&JeqjAa_kHpKcksfjEl&*M2RcYsIc@{=I7D_JK#(_pRJ=k=3)4$4s8^*z*g<*g?c5I8FUxIFLP z%Jey3uL*%Cih&(7x8idwh;8~k9d2LJs-a40-Bz1xN}5Q@uMSFd&`Q2$L6y?@Sg9(( zn~e{@LW;AwAqZ9DJ?dle*SF;eWT$|nUWka5KK1p5VW6Q{1R!3hUZSVtkC&&YvQHr~ZtR8sPZb)q1{RgW8hE4%yi zM4Mz);5iAmlF^ldCr{LFbA-jv72NJm-dVd+P(<2VmoYLU#TGD02`ha0pxpUWq1$D` zC)_7DcvzIa;KkK+*v5mCkU#?76O1y*knNSZ2daq1`AXv%x}BrB;Fxu^Ik zQGgdb0399bVD)$A>b;AW5W_42koufB-XXEOV92HPKtb)_8!si7a={# zsgka043d(Yi;KI#O=>#)HVlDtd}c$9%-}KGhKxZf>ILEYWHIM>@bDla8N%+ZAao1b zzyQ8QjQui%G($ZCsuQ52uw(*<1P(iQDDj0yz)Vgl4?n(AKb)Tu-B<)Q-F^YY>4O3s zE{KW|;V~iO>Ja4FRgaznX};FiXT{(D@z&-_8|jlKasT6vUg;p{cp={fZWO?o(8{m! zinNT-+bYR-yCANOv004YWas36_bmY)qsa?Uyl{JdVTJ%kb-<-xa7;(MyqVn8)Isu z%l`A1*T;)Cx!@oVEy{4ng&oi9b}HygF69b3$RY`a!DhKaSfi=;!p?{XZvyp2IJyzB zqTvnIo}^bKgXtepY3pZTIbQn}68BBa`Q51fkAojace5_jas$ zpYO)Q!Irq=Xb{WYKu{J!wP0@L{wAKbKg zQzoF@0E%_@Ke*iARnUD!It^EDZv~?=MoXxtT!kipEHz@<`=?T7kKJ8-n8P60ZS4Bz znI0Wv8Y#ZUYyLi?snIbYee6j@cn36u@A?3MWQ?hm_^8R_*Cfs#6zfA0{`Z%v*Hoov zX9vEUvaWFW&!lZS^t+JGEd%LL&r0cZNYwQ+PwMTq@C4ZJVvL7YSis!%epB!L|Hs~Y zhhzP}|HD#LG8(c&QiMYGF0*80@0Db4vZ-WLM%kGmWbeI`l}+Xq61wb}dEvUxSMN{n zPv7tHJC6I0`#A2uevYF>uFGpZpXd2FAHxv>cRjpY2kN>?#sY`zUr)y%agN{}ke?B) zGvUewhFP;hll#17=8y~b>Q%CSZXasg*Ca+&*5syk84Zz?Xf%4+)<+14aP2Gw&pEyW z8-_WpVtdSzGw6d(HL!32tplCo?=JQlwEjnl`ad`xViRk@CM$|v>o%DG_?;m$j3X2DFS@PKCBYhkPEf3@@k;p$rU>S@R1sZvT#KCq`TO@0o7#<_l=NM))bEQ=sw z5(v&MGXZr$nF%xjur31$(ta3rF}N$irs>Yv6HwNMcU(0>@@8>>(Dx0VTxc^Lz*?oy zeb53@&|KFVPb$eOUDp5=(`Cn>okr^^&44VLXmgKWi*;YW@xjvlR%uJdEhpc;LM4Q8 zf#iREo{>BsQ<5u2tvAdm&$TG7Fu)B%^%euc3Lh~DI|6?Oup{b3_P9EL>kw?Ts=6w3 z&;kH&fOFw0ju88KUKWZ1YR_Zfq$3Wv%prhX3m^U(aB^``K`2(C5ET@I@U5 zegp_I=;arkKk;|AwDw1A|DaHWSyEPA{};~5o%Y$V6iFkUNBo}GHSTDS$=ZbRK*%L8jG`Q!%(P>v?i)s(1jb-Hr;SMcJ4q$( z*^v)M_i0+0<<~p8dI}7u#G`KN$ezWA%?>f$d>`?!z5gmuz-fIB?2+)#aj*ba1S1#7 z5|4Mn@Non@6L17)IfOZmI1wT-PjIQ$()ns%TIN;y%T_+mTSUINwK%*93m1}f+z*u? zEMNL^66oGW-*1jRJtMU)bfNhkVO!8Ps3_)|IynV)4C{XS-gm{y!-zFhbf0 z0FUq{NnEkuL75hvlWc}w6DG%l!$b1EAwKIm*{c~VAt7l3KmX^h7JiJmUpNQ2fm&(n zy%~qZ@gf^AJRc^lH}zb~hR611#NBU-#}oa=!C`0P4}lY3p_Vzed%!)JmUOUeCHm^2 zlb*4sCoZ^v_n1N>Ct5AkOhA9l26GgsBUZ&Bf1$&39b@!AnO+LsPZNnt&xIdYFJVOe zjc)7cSh|l++>EiPpK#N`6m_En^Ixklfz&S68f$tc_%Rpiko^M;qWS9b7U5VFGk1>x zoAU8`iWHl6O^n}bGH~;A9k^i&WKgh5@VBv>Tf2-XaFBL3Em}9$*}B}T^$D%}+-1Z$ zE3f{@9pQpQpeHk+Nj9?-L+Z+V^A+BdgjPvGCB14^PhmQN2C9bDmN8d=R&IriPuOdKk zS00U7wxb;*UJ1B+xFZ&g;DG}@d7M_wBg$cYmmA~F3^;&5#wRqD$o=5|8O9U+;AZ5@ zX=b51*{jb~F!t}a+(B;(DmiP2A@m5jp;Lx*m<2!@Thdh6*%zGDyW1lbSbXQdjpC1` zFc9;mTI9KWQ#CH@H7-NVeyII;sOd~wexdS)&Ek`rwKkvcxtPda2lN!8;f<{3zg*ur zIX7Bg>!@_C)(27*v(*6be85~Ij=nknGZ&CgkhNrfzzB9g+yc;^L?wThSb)#MXbI?W zaWNktX&GoTFKp6o;b2IX&|t643q*C7ddzr4N4rBBDTH4BcF$h6G?)&%f3rPG@tt;; zXZYYf(E7Cy^gCZd6@kwaq`>J8Hy2)d_z?K`A0t?Msi^OjeoCJ1Q+O}H=`~Tp@}a*l zSGY%l%L(ezGV2Q`?mR(3ML4TWIqaBWm94UGBnoC>B0JxjCTcwp3)tHrUO@6)q0R2B z`qhEngtmWfgc%@%+brx7FaOP11v8K*l%70>`wEr|{&*EjZLqu5F3a`L9J?>7QsLmZeJIn4Eqdde*9togOd(>M$xc4 zsH`3^KSJG`oHlP`dBM*GChves0ek{nJnpWgTCt+Jw8^w_Oqt21_gSM@K?ttM$j-L> zf|jIu(Zi^WqqI$#4pf-qF!%(M(>C&=qfk?9)W1bjx0pFs5;;EXt&naJo|yOJw>mZP zpp8pzEEI*B?a$OhtdB|(au1J|ktKl7@pa(Y{HIP<$=tdrJl+?cTBB6FZzSTg%vXrw zc-4Bf=YLJ0nN@y=E*I^w_1@*%`JVH&YX}e{Rg15crF~cWfALBs}J-bRbf4{+fw_-up*$VMxFI zo5!)F_hN=2wHjPawGof(NQ1qL-h0tL?|?>$SmleqRW3xNpZct)tayA`v@eGf`{U}F z12H|F2}31wITrU~D_r0--4x~PZ(Z1|^g3D`V!P$g9}?2dLA(Xz?GfmTkXU0x6qSVe zhSn^8Oa$o{XMDJGOKo%g4fTdWFo{z4kkWD&spXdEIuilTy6F`>E1G9dd|lkKs)KWe zB569?#G#$6w^4Pn=mDqK()Muezh@um!Pylm`|rh9Y#Wcd=_<~Y?c(M*y-FeXAxeDW z2+B+(E)_|2NKB-KhX97KD!3{9A?r0yfo_^^t~FN-X|g5i8kR^TQ!juDciyPd=Yo?Q z;uFpxFFR4kCNh*Yc=#Sn=#aEekc1MdTB<>*p~_}*QK3XU;~j3~*O#h-$4007vPSxx zFAZi9a1l;lt^eqvM-Bu_Oy{E_^)$`9RTY+1@?gT1IlKsLd(c!A%CYM3dD%cJ0oAoT zb4F#)Q<>FwIVCBW@v3m8f)t+OOB$Vu6~BIok8s_WTQcZDE@y1aHN2$EPuJCKd7-n# zL6nLZwa?IJaL99IKbH0V!u$#d>ab@GLqD&A+uWU%{=|GF!+h+%cpAcoHBLjxXUBu= z044_TjMVA}s7D9d(*S7+$e;oAryh&AhzF{`2=Y%u@Okud?K_~=!3l&?#@fD0BRF8Lo_#l#1y~u9r z(~FQP9RU3Dvi=3AKRJ1NRW};LzCOR&%6^S;ZM>{yfO&82Wjcwf zqUvv%1pxLCo{Vrckc42s0L)_{#_hVAllj?bd>QkrI_T;2o?0(AL0g!$^5fk(@9v%$ zu*ziY29Y~{ahbm~OHbhE-f-3WxW5ZQ%~aWtQG|qqh_f=w?PQAR z{%a8d&uQRhGidC=0JsEI%*}?2EDbx8K4Q?#BSxEaM{5C41(`-NE)!wQ&7j1FZcpnh z4#-PeAnzPZurQ(k9cTw*suF|zzS8o`lM9yy;3p*i-kQKix20|Fl*i8UY^&H2C+veU zkA;(>7ZH_b?+b3qFI#0jbG=9#sG?;dZdz3UW=Cp{zBxHNn@kHs+!V|eoX2y<7>TGuWUfczoibm$ZHYlNc`$pq zjQk1g3v?28FA2tXfVQ&@eCjozVVaxWKiaA9fpwDyVXz}&Q7i^~RF7O;;7&s>%#fxu ziO8?B2uWnGFVq_u)WK?~HT&mtN_B#8v#KNJy(?ns3_VCq*IW0cLmkC<^IyB1RI zKsd!CNQDL0Op!Ap8O9XSLwJ+Obz01;=C}Kf0~+8N1MHT;D7;GE;^X9jd4d~%6Bm9& zGA0kWv0MLC%-Fm?7My5A0WlKhu@Iue$uBHfr}=)0ceK=$Z|~u;fwEqwOXD1Y$La^Y z^gMvxaSvWE@*XVFX56sikMhePD+J0k_6q;_3uOpn8zE4SP3&lF3IGgFlT1<=vsA3r z+&3TQe-Ut|+n_mJolH_&2Mn}qd8B*aH1X#snzztnm3I5mNY6^G$4Ipg9Vpsm2SS97 zfxHtDjn?jr2P{^ClKX^Y!}m1~?3yC>v&PZNDg_PYHQYp>DZG{a7QWr+Rj+7{ke4Yj z{L1Z;5l;E!wG{Mw-Z-){!#9Az5M>Tzj>d4yB%HfteeN}lgNadKxE1@FTd}kbr z1n*TUU&epV{JF@Q4V6B{tK@#GA)=o<0u;UijR?-&dxd0kI_X=N$j(s&+F;~MZyU!s z&O+I;w~5AS=In_+^2xtu+-pa_HzfO7d3&Fs+Z&N_>tU~rUd;2k;fO}+*-!kRwnQGb zm8DS%nh`KXUtxIMQ&&4f(7tF7LKArUZ$@`i4Fmnr1kdijPY$r;c+JgKR^`H#*X=X9 zw}jxAfnaTkN3n*3;6VREaLKt!$0NxPn0G71>E>p+U(Tt1<#e9Fd?U;Lpd@Dfq1-A? zxA1ma2DKPL`^7UOdo${*EPHIO=}DF*RpmAcnX;l_47B1|d4 zl^x)Q&H)u&n)3+2 zp=(hP8Uhdxewp2PJ-s1mr0#s7)qZGP@7A``t)Z`0G_CF3SWuFK$_65Fpr|jEPDmPu z>OINLKW_5ZwH25eLq#vvCgihD{xXt5gSq!t?e*0*f3&jo*OxeA1mPxm9cy#U=QxdM zru{_kke)xTPvhV^M^Rsu2a+j&4rgThBxMiDP2BXC4Jp z5QzoiT`w;f)d6}!h4-6x*ko=zqWUZ&DPJe zjE{e2KHxZ!mP{%A#ELrcjI0Eo97DsF7h7xcRF%`tHpF8gLRS0iGcsc>p?~v812c)sKCi<8v>cGIv!A0Ty&&I;FQ`H{jT4rhVaFfmL|Y}fO}j`$Ka%gNF#!M>hUkMASTB1LR>GkN|G z9t{I|(EX;HI}FJ4nM}P$yMcrnGM2m$@DFfk2;17d)4v>N9#dSm?^sC#;RipU{{&tr zOl$Lf4i{ktk2Ee4Bx0TYvmF&%WvGu4)xGk^nb3^k2n-70LCurcrpe9RRnIsyub51( zzVZBq$@}lvog^$5vf46<)doNpSmZJ(iqJg#rDeeG_bEL=CH5J{4Yh1*4E6(TxnPzZdkRZO9~ z6%jBx>hP||K?czi(Ew$^E7QgYGl?F1+1UNbks1>&5ib!o{i0KTKLW7Z5>J*U8l%t# zh8;t>Ut{Hg6vIXhXVgWJP4)6JP9PlPQSWu5)e3BF4`n=r`xJ+amzj|=YcDRgvp z_+M~Wpw9Gg+UnG_SHQK4arVk;qg9IE_V2thkJU_%^b+f=x%dYs{c%9vat;*uP}Dvo z3abzHnp7NX)%K&FpB$Z^>`U-*DK}WH$@S7gTO%G-pr*daJ$?qOYVegsA)ZQ3oh~07Wf=pNOXQ*mtI# zgA-}yHB;$)9JYqESB$R!{m?{{zf2-m$+)%q5@4icJ)0HlE!3c>m^31e zwJ<1EvgPoZ5S}=Q;wy$vMzQI9DZ1JL<{cc0!bkk8m{NmjNVqx&TfurEC;rcF69Cm= z3?Kp^##GYLd=i`>1pNfL`o>xl$>zZjx)SCbAYK7Ly50ovIK+UT4b;JtJEH~m$LTO5 zt#(T^Bx2jA#17|cRln%zI(G3ODsZ2}ayt7ysg8n8G6U?s9ykX?iYI@bsKxLY!yUlR4$jH0L_A+0WMoZ9QonmI?3!(DlFtMeAb!VCqoPjrcbHx zA#aMLvOpKWre70-*wL(dt$N_-x{sz=jTAfsw!|g^;R^51r2@oKv{%`M(4Cq%!dv^D z6OgIsHMneGx?rluKnyH8+X-R0fe)(8g?4@781<|)|~Wn z_UvOw5;scR7T#4o)Ho(_l5M;3HXYgK? zr4X2cHr30#$Pfl<+))=hI>eL#K{6rf35}lI-$YLP(r9r25(dF{x2p3@DhRE?s{k6a zeRqtueBk>B0KNT?di{IigoJOry2x7e;If`6uX$72Vij}R+`_^`2Z!?@@us9iq`h#S zAk1)*y74x2ZDpq>&S!s2OsVan>q2G*xDNd2vi0UFuURLHVmDB@89fcfxLGbi z+|~P0y@P|z&+*PwKsZUo@(@b1^dWfn{1d|i4;sk$6h*d3fmsAvG3d5IC_DwGP@o1| zshB37ILvd|+!mVXMxr_Pw-yC}O@?$MV26NQ?Q$3yBSb+~rL8QK*j+5?I4LC{TA7HDh`9}q}=j5hEVfJXv} z?#a=tqtQ0@lxmPHmHj8qD}R`?uh@q8QY)~75W9{|f4-V$h#%7Zo0Tt^l03tJ%9`i1 zyj3%OY4!Uy$`&ne>wZB)&-vo2CtB!t4i;D?1Q`qbHqbzo1*2^16Ht@xLUc#H0mvgv z&1;3|B|wr;pV8#@Irf%r?OTiKUaHuxgy`-OiCycZ@=c-o zEtK$DF+9b)r(T>3u`x2cw__?YE;1UUlnB}G*Nh^9x{j~KW*amOYUs%qK7c7(Y}d~S zRH)3bvl$-4GT>kk9RS>Gud_dV?BFzs^0ESDF<$w3lhfMx>42WwshZqwbwdLs(3{3vo- zS7s11Y}(J*!_;F~uv2_uokYV}8oHxk68lNTRg;HKgqQq`wSTErX%<=uxS=i+i$}5P zzOdF_`c~&F;B^Su9*dfo-3tf%7_kQI?vF{ZE0>UHV!kbLHNTYqoeP`LVqLdw)f_We zOf>5z?j3ieoLg*tKoy;%9h}VVM2b&UKDn))S9L%@n8sP|L#$!o2GiLm0&YSwsY>XR zlNixeS4_R}dmmz@2q!PMy|idJkCfDuU9@7hXgez=>dCGhNO*WLWXUYj`ac>K(YIhw z5|`PZ>A05-f-wLpf{aqB&2kMhN39;dBlGVEUFm3*dFIupCYqmr$KZl>2cY-xSisC< zEi&KJ4;j<3d(GeXTyadAMxK3On*<-!*4F;o+J>Nw`Nc)!_OPq4;{1a+q-STpMp&o^ zFg3mUJJt>@x+8G()Aq<8V2N6w0LkaK&+VFR`(pZp7Uk)E^UvjTiYK3e91>!VP z`Emi1!};H&2CZFi`~jyyz2SFaKB8A7B$h@Bk4x|sFIgLbzA=Xv9OQvFRY6AerBEEhwjJp)f{0bg6IeAbN!X|`FqmbBEBu5FjS9-%B zl7VReOhN#w=wN_Q-<#2hF_sc3vVI_ND0&lQ2`(CZ{zkB1%Y@KA1_KN*ig5&ImvwUJ zj}9PBlFMMfKYDi~y1o+$tm2MbGKGOQ&~UYLqtvu-KwcC~6kzqz1h*GtL!$%2ux7ot z2!deaN3j20j3A>4Iq+#5Lo!I1)}WcBQG{`XF00$+D0 ztmovPDh+O#&wJ9Kj%;&k48IE*P`^P^wQMJ9*fb%k7-qu6mjpkr-93L382jMEeF_)Z`G0m@ zA(Q}Y1wyuG$A)z~pp$?#L%$2)=~L5Z!6_!s2`W`38t{P6Lq8s5sfuz!sk21!yDu?U zKUsxAm{o^DVtGH*PPwL@p7V=WPlXkx{1~dZ0#pGIX+uEKX#ju$qVp0v6KaJ@hY1;Q zJVlexXnm|B5_SjsL7~H}tX$J)Yu>e3`ESy`(mK0Fab!^@O1bLu%L@us+$9dWZhIFa z9It}k!Glt$r+|3V2HZs1>nZ#{kS%M+3w)s3q{#IoVGat^YB@OtyWKV;z?I}ORX>%5 zPa~xSkmu^_lR!rX+|r8OAFnHa##;9FBBT`I-+ol|r}*LS0yacv9AC7oK&GJw=8{;C z55^W742V$(k}a7k6Z&KCSaACN)cGF(nUY-(d&9&aKEREnU_mo?I_0Tv010>p6>q|G z@=u=+<5Mulfx5X;6)a?1T3V6YTMPq9ur)KKnGp{Gzk){<_-VzCRN<<8eFTU7!={MV z<*jqZ|HKC`oKDcc(;s-CY(DKnmA;#s*X$(Ev=+VxhC+0%pL355{bn+#1;(Ve&&`>Y z)dGM9TRdVGhbpt4M(|aga+|BK^l2h&07wIK6PU08XAG!s&;v%9K;U8zLUltrM%eF1 zOWZDqRbx%9P=)ZA7JZ3hCIBf2U_252CR+D^2)I}SBTrh&pZ5*+&trE(v2DPFD`83e z#QY(nI>VXtG1-Yb>MtK5=2g$>DJL!V`b}!KOY>(DV>txcB@sC|iknC80V2*32<* zx{@5=h8hCQg??6|6Q>bhJCG>MB2aQ+EA2GIM42EiDLXqzx3E%=^+pc#XIGpXbQVfV zL5o$LNkU0TEdfDDcB{2JMNpIIyH4RD;3Oc~cW55u4}-80=20*dhIkP0tcdQARQfzO zM}Z-sc?6iQ@X=>v9(O%bHb=jf3N#d*AF>;O?hoE2`cPj%fnhie!8XLiu$0_63y?Ap zY$#$IpSzu*prnMV23!;GoPFc6fjp^nqTd!C7u-%AZ794{#iEB0>|xZ7Q2$^vb)9)D zgJ7Qs`)6xm@P*Pw>;q<5prna2J$0`|XsiNcf9*O5{L$*Of!)ioaNgQ>NLRV!@~Esj zn~UbFlW5X_NSBjmKPn?`RriT7Rr_|NGN_3&Ra5pRvFVOH4)vII@KOGNiN%MBTte^k zz=AlN9$#+DF*`jzYCcnG`v^7uK3;#NoUpyesdvr>jHCyfQ$a;{tiW~sAyDv`X9&PW zC=kvXr1~OMa$uc;9ba*eXa_hA&abYb0fsPw=q9ZPK(hf{<)NKfVBLg1(2-uCt3W`c zo~POPeD6#f+9AisLL@E;)!BIbBn2(iCp6zD8&JzEaBL+`9Itrmdy?4j-L+wTN1`)7 zdos#10*>lLpRA5GgXfSbrp%gX0d3 zSGodKJrsyWgI`|HvuLknnr1oedS+rce~# z=0B|U0)s3>MfdNO4< z{mz<#coFU=fNitxPV>V8NRJq}P*!)e`$)Kv^ z^I~y$p2EDK>!b|%TPD(I%)O-RPO_zwiQROzTIlT(=1!_Acq(B7G`4Eby1t0>YiV+# zk~w@r?6 z1g@tc3aR?0fMFp1Yf#=0`a=HcZPpk>l!=_+F$p!0P3CvnJqucD4vwehjN_|U<%8EAW;|zC zBn%1)c@%jYM&uY#sQXB@Be_f6gOih!t>TlNXtKeHTg06Zc^`0XTE&pcUWMLs%`-IB zw{>oL`Kl?}F7*RORDu!)(J>GMl%@$XK`4Ph-$8kyy(mVAgAdiiK_CDl61YJCf&lWL zkc+kH=*DYpOEWojOG2WM^T1#7!z05>At@XH}R{91SQ&wMV>QDrP^!S5L4Yr#+kP==uJ`rySa_l+w(9PM;t5~)rW z!@sS}75}v|-|C$gU!BV5Vq9>ld1_u|l7JFIC5NwKM){@Tii(PY0C)5-ba)QGhS4cy z`S?kkG_xvspth7*m#AOyk^>{g(uZ+QVgNuO5_4qoc{AGSj0~$2P{cH!B~x>28*`?O z|Ku450+1={ZZ;x(1xMY*N)Xux#eH=TdRL-Rgp(`#ZpQxM4;7LG?Ll+%)HE>aYD;T; zjdmMl-x+wo0aV9lT!h8bS^ZhUH?(M5Io(+qPk+LxU9K3|uAvGRbYnWLuMD-Hl#+v0 z91(Cla0|9k^q*dbc`40(ECcHp(qre0XiCIoyuO(FziM<0N29EGuaoKv_tYf?WlCnp zHkM^1W>=Itvzvj})z^#(+H=XteZOM))o=Y&&f44}mr~-T9dKHWtKfO8xvzh}XEA#ikV<%DYsWNRlqpV4QJFsN^idY}ldx=Aqo zhMpi$2P-^mts``rJmy;b{d_(uqTeA1VA!WbW2D0ikyJY6qgE*opm6{`PEw3dVpH(dff4wbwfA!TB-N~zgxX@DqMe(Gl_jj<)EArPEc5D}@w4%MOLUrdX z*q+Y+Kp#zs`STUX{eBe*3I4ILn~kh|W> zeWd1uPQvJ2WT2Yea%1Wg}M=iG7BlkuMh57qh@C>q-Ha6aF&p_M%^O1FmShp z5m*fK4tdTB(4;~zPwE>E0(&c_MGJ-FglR@*+E3!0@j^0Gl=E z1FKg}9B!9Jwy{aISmJQD>K>tiyRycUc#-)o+Po`_pjWM{v2HKHWJ-6Dewzpa&f|LQi+FMC5$mO3nAI1e9TFw z_E4twRXX?Q?LRaLuEg(*MpXW6tv)Ks8d|MP5I+SNnm&WF>Ou;|uwJ3RB)FcZq|=t!CDWi{bM z4rbQa{X)~Ht&G4;YOqrcjpWn3?9* zOg!qB_n(t@?(Og`KZT+jTwGiay22yU&3Qg+3FwZoHflJM1qaCBJZ8(KB0kMmMLotl!h(N2gp?fh89Mx3fN z@;LYE%L{9?DwojQiX98-%q>*+mAV4Xx6V3Se)(=o@lDc5GF6Edl@j}!kUr$0#l2vT z63sJ8k4`^lS!xUt&*C6#-?-IFDa4-ivqM^->jEuY`hdL`b>n6G%hsV z2cG@B0ENflm$zEtYW8oivjgA3tW?``zelV&;HdxPUOmw>*Gx2pz`Ttt&U zCe&q z=~j1>o>CMpr8-0td~`^kT#e3WdUe|OjY4^`{Z@1?GeHrXjz*yxDc-n^ixcXxqfBs! zyxKZ<1YZ2%MN&B5c-QiNKV?QRo#bP+g&;QL($R?EgPSZc6k=*fjcW557@Db zU|jHAi-%5u^u-HaX1d|ATUJ!~4>q!RJYToebJTI!j_wNV>h-is&9H=}U(e3Y?(FOg z%HnyQLJuLe#N#FQ&W{y@xvJu4LzoHpy$=`%1_nq6X*`+?y(lzoTPSe?Qgf7fS{^(y z@iT$Akx{ddy5%~q$>RUqD3e-EqAewD?~;--fYFJEQ+F_4fm)8j!HV zd=_~7?;A);&XnFs4!1(V9g}{YLgMU+54E&#W{?yOK#Cmpf0^%_(}lJ^2rf78eBJGG z`Ok9$f9Bs<*oxNpY!_2Ye-G3$dJxTwpq35CN!CEt!NJET6Pvck*UxDE*Y{`;A}6)ANlK;DlM3u!mKIP`L!(Pp9p>zsnrjK z;pF;=aj5;yxQinY`U^~ZXj3>CI&R_n9&?MDibqul{r8lda|>3UE5Wl|E{omq{@OyR z(q;XNid=>>!3Ne4AbnP_0T9x+{AB(~wVa~jfJJG6CgE(nqCL$8>2tiNcvlquI#v3q zq=M~bfScmLJDJCZh?jwFP3fZ8Lk)@EYkwVNwDKGys;H(YD48&9LpWo|!e}~%{?y%p z2oA!F+%E(F{iq>E4((lRJe8hJnfA<~XedzB4EDZgfL@aERf_W>wego^d_F=x=D%J% z-f}eVGJdkhjcI2;cduDN&;<&j0sRydCC6 zPX3VQHKuE@qn&Y*^Y(0=aB3*p%tju=;D+(Z0)H#S4 zH|+K82L&ih^S%1_Gz7L;M!{`McKoxEB&pQwMa#>l8Yq~tijR(maePu=MEqy{{^w7` z06$TrNz2%*?cax&9VE{5-yh`MxluUWzfK%O5uCZdj!W_pid68gs}a~%cHQZ(*E{#t zT4j#$?-xL&_ul*a1=5wWMRy zU02s9@~aj9tWNp@zn8k1EaMG$DPvS?X?o-NBQp=JmCB*Pe~ zH7@?=o8M|Y{m*})G#=6mnRXpN{Iia3e1Xjo{-JScRb&El|K@Snw%o1c|9#iA|GaCr zYotnS`ivWK!@>efj~Mn~B=+BzkX4%dB^aGPKUd^v!@bj7;0T-aOzFqTRb%VDo_L7q zYkK2d--EbmY{b>?#T~5EVL74hLiYE0CkU#`%3}WWetX-@PVNcCLm@|0-zd1Mmf}$b z>4C5z$>gdZe?Po<&i^umP53JI=(mGKtM>gwbn2^* z!abZVb0qA4u3w!zL4rQI1zp3@62y{JvqLJy8SCj)1G`nt7*Tc{j_BsQo;6S&L?N;~ z&tIu@$V}?}xXpzVjb@j!7ADsrNdBLX8sP=Cw{MN+xRd-rBa8T_N$BFmMuY%)V|L3LJ z%I_o(EPg8pGjG!tB1rIWS|VDVq!2|8QTq~vy&$Syz)Uv~Un~y7y&vTDcQXd2YFEA3 z`6gVa=SbL)*0t)M+P_=;q^KX{NKRnneKX*2b!CiTZ?1_#IC?@W6~2h!0-)Kw^G*mZ z3zZ|Ct@}V6GA(VPh<7}32M3_&|CR@3jlYUi%|BiI<>0?GYIUmmpAP2uWn;qSCvjew z?Lybxwz;F^(eNIdn)$2qPJ#~(XLlZ3k0`kapb~<{161WFH!sYwYjYmSUG}>LYWv>mykYcXgJ1s^M$Zn8}rGlye%L5B$vnldC&_$bRF7 z-UgTD2o9LCx5y7^@jDH)#Kj1#cg{IP38gT9-(3%fdm7-5vVGrR;U0rudqT&*+wJ!8 zN<~e_gUem76}ODf#Sf?C{<*F4q?>40{O#gky69c5;KlNdhY3$SPr@NAiyo<$ zxqIt~97Rrev+68kWvlype6kO1T`;*;rC2zS)=3L2YTTO@HI98!KA8u{41ZHc+0M$Y z!k}VghAy?KqY1+<^QW3>i%jjEUr2+uuBH;txV-A+_JbTRk1EVIJKUi8LhEFGKMc#NeVUqDCyS+3u*$|MgA-cKI5_=Lw#L1H>IKU`T-Qa?D}G5YZ2?EU0F z<=@N~r8%dqD$mfsyBA|Cv$D+14n99IZd$P(DzltE;w@VqtEi7g<&^}7mKR!^Oz%^p z)Oqu;4k#B?3wqN5`A7Zl&$E|X54RlFek+R%UbMB+8Lc?#utt7wK|WF6Z_P?Q>{E&S zJ~{Hw3u}DZrL!T7`|~vF3S>D|eadT*&9K*T{%&19z1DoB9KYJ||MkYR-HMjg5vgPO zQ!eMxs0r%Tk_ze>X&BFO9}WLtAIgM=XTzdklcRk29{QG0(>OcFzpT(Ww|Gmftl)clo8hf6Cx%a>iQb{ z%Jm{=+EtMjim_#0FtcgGI6T})fwTd`k~}zDHQdu;mVDuVxBgPkO391~-4czLXR&#$ zNc(@_W*wB=<`B=ybl;jcHnpCh-zoi{AIpJ<>S!o|Oki_!vqbv_v~+8;t1aU;(U66~ zPJ`1DQGc1*SlasA??-2H^=}B^??csR`1dsZep#d+(MKPAoOQiL!Nccm+I-AryenUo zS~PlfW@DE1zCb{$(Xy-VN(|vDwT_Q#2DOfoetC-+`(iFONUz;-wR&`@e#kS2$hKOw za(s66-zMzsDs9)ilV5Et+G*qV&qk`J#EHjRgglQuvo-V;oWW<~51$Hd8p8>=>JlX! zz&KGc9PHn-<#6e<$%=V4kN!lzp_c$(OH>ETz80!(Ki~0tYmr9rk(;~N(NXZ~F^Vu z+)C|N9FpSt)qAkAwyU-EGZaE-Y4ptN<;mlRG${q~#m(e~%%|AC>$y#s94D|nHT`kl z({HgGwV<*U7VI;M*=p}e*Pod`Za3PbB68ibVQ-@_NUJ(A{kr*>X^Iw82|hE9Oze(* zw~>{cp>a-dW4@1HH>u{AO$Uy))6WB$gNE{_FG(puWPBB>^jZ+=^?6#L( z-e9u0#w4O_zb$qwUgpaGoMG!pM@@?<^70>aPaXxT7PHX6B@>9>HleF5Jgb`gwAsir zWXmwHBl30D`_!W!$pdS(m+D1JAaRq!jw)~G;i?$>=H}jb!8qC{t-NOwN|~E(?7T-D zsGyVTClj@mo9WbW@Z2Z`gUfha4ZM4mPiFek=ltlY`Svzfu#nFmiTlvwJ7b-_#XBKd zR@oXjzN-~97hiWN7VScDde7f#?BXg;UcCa2$9 zcrn4Iy0NSDX0Dcr@FtyX`;e~lk?sneN51pFTVr1Qlz#=SEfVyl|w9z|c3v#lMJZ5#$nA}1*| zI_Dl(e>9{LwKKapJYO@!Su09OjS6k2{1ts#{M7R|^g;K+Uc8*A5_%$DtR`w@*wO7A zZ6cX27(?84<|7{aysPzr15JK9W&LC*oCs%d9O(a&qdS^%)`C1Lifi8&*O*P|tS`Hmc%9%@ z&2_P&iQ8|}W}St*F@5g4xS`Fv?{!1UTo%hhn+!`1&9KHD!W~~0C%$)$9d)3m3Mgps z>X?Owj~lfqfv|l#{ z<#`zR%TITC)YpAc(l2X>YJ^d*4#c8L{k|Fh-D^;M2e~@1E&aY1El3hJaQkt?_^-Yv z;U_R{kiTto8aJjM$F$o+AxZB?CA!CP>GLgg50R$s!oaEv?C)&fwWw_}o#th_tokx{ zQffwPRP5LoSym^7?Qqwc)*EYXB<eE4dtXx7o655iG-UI9Bh z^5F^Bz~h(C*LU0qEITR~o!LLX-F_1;>s>F}Uo0IC|z;;ZbXyC zH8f4FetwNLn=6$*ep`R^(IH#3#n*79?}CJhdFgBB14-=0{{lmv82@@_R;Q|c*tRKTb)`8Ny|k=RByML=A6!?Xxo_w z{JNUl)w{1?ekPSY*jA$w+p(0ouvGW!sB}@sPXV+?**5hHrK*cs4n^Gz&#p7qv`(L5 z==kasEP^o|Ddd`(@UVqnHa~9IzDpZ?(e|_h0u$6j8fBD&4R(LLN};IDZ(59lQiZx+ zjvXx}MwgGPlOU!ef?fT*-9&FpAxN|%Ak5C*8a~}-H$#C-cx>VsyExmiq1%a#XV?i{ z_bbEWh3e(Ltqt=V~zeD&UWi$bF(Ro#t;1G*mL$DXXdyk{|}FZZmpiQl~VL5^;$ z1x|;N*Utw&y&1r-%zlLWMMsw9G#1s6mtRGQbQyQUZUwpLqIPSm-u_#M*-oyN+xRJH z(7~?}^@O46o6DqiPL=B$Pb8SKj7m66PVUmP-c2F%^WEEALnjHglGp#D06L!V=6yW# zF$O{x<0aaC-{I4RSo(iY65d)-ZEj1=Jn=?e!CjI@0`Y_Q701uX6Vz4R_4?PP1yl3Oo;j zZw;)liSczhjoPQ~t)19qVC1+gMVz4Ky7Ep+A-<-qFnrTVkk)u1=6y)vxgr1W%Okq7 zHiueL1m5zRA;>{4uoLLoRS43dfhr=2t~B zo+UxfDpy~@(l}p-JRcoc*PEJ`ebL`OqEAbdjEncNl;ffQxI=6C#WC3qFF_%|ru;ss z)Z)?QqQc7Pqb;_$b(38Y!UkV@=Ht!Uz?>{QJp46YHn^Ane_oTah!Zr0s%zWt=_w@2 zyfA8bIlVT^&b}s8zv-6fy~2>KcXz?V&YPf`=#>eW*~c&@EkyyWrD>Y35)DfiE1TVdi%%*R87dxpKzc^AKaTe`pH zP*#Plw$5fh%uU2XpK?%3vC^R~af7%iqoZTqx zox0OY1(Lq`A4WJz;I%04Iw&yNHpCW1onPe?y|PB>Rwb)ktn1QbGvS1m*L9Kb{Xncm ztTgxRmYgrqK(rK;mhrFB|*?|bQ4(3iGgl2D^1`(_gd zWTyU3|FHcw!()=Ydh*}ea@oN~V9M5XUKJ8RtueouI3MX-N{jX`q>)=z&ELk;UFE@J5 zYks+DYWB26OS&FB8P{&VQ6#uZ` zei7FVZ8s5J7)0r>*tEsQzc#XEq&C>T9BfeSrT3ACFp{Q@`6f|9%TRvvX>(C}MJ}4e zhUbN3H7C(#*EkQm&-*QMR2MgPl`l>I)FH7q^tl^P7^?CtxBDI!6>+}q&pdjxv}YMTX}oqGQp;Ql+*U)+*oilH6}x4T*XeyR@$1y>aPa2isJAN{&((PsfRwcv9!- zZ?NXGhazZ}@O^K<_NFklM3Xycu_%}rFi1aZ9m2MH{Okz67QWGmO-ERL-Cq=&s;||( z+;pzVpAKv|>H4YYOTflA7O^fHs~C}jx*Aa`X_C<6LF_{|tq*HQ3&wvo%?10f>Xauo zx$;%jHW=HJ){RZ@ix=}A7~J73OKf%Zr6!eZQQ5s2Lt8BLFt6H(Z*s@0y!T_%nrYqD zeE(GL+2Rc{Ik%P!Em@l8_#~mwokYfa;F`$%s;ww^Ql8q1Gh;vvqo^!iocbj--q#&nA zQ~Nm+tyt?z<7~DYGNd;6==AkGHGlb-tT3kILQB0onzmV2pEkSe+`KOIL}s`(YSyHI zI*5t!`_{I={|$xo5?cby*G@ zEo$Qn=kAO!T>9)ZcFt76#r69$a@SwydgyBzV^RGr?DxGc*X>&`mFQy=wd}}rh%!xy zehJ$B{Pr|5GUB-)-f6tkA@BF)A|<{ApHzsswfk&6?c~8o5ux~St|_lpPgIjq-nc>c zyRtHx$-1%C-Q{R6QJceQRcyi9wT6@DT80{~{leI-e`47rr|%j*S}!~b*Y~UPo?bSI z;ai%UbX}SivZdgduRZ%8*wpQ?Le93TM*oo%CCw;(%9`B|$;RU(qD}1stfYB6xsm77tz#`xgSS4~(B<(>e0eZu!bfhX8<Mol4ldo*US+hKOuYg93P=lD=4Bw>qczl;#dd|YF&yTbu z^*`s7O}z~W-56vSlgsBix~V%+Al^_}v|usv^(AJELx0QuG|98jFFj$t4WB+SKsa>B z?Ci0YYy1rHr|sbR^8XZdQL_7|$=OE^?<_ zqqGk&9V;tLa463=>}=vdLF7f+kvH_OPJa?S{K~~w{;Ba-^(D{g} zRg|o(A74A>>brHvnM$=Qz{F(WW2D48B_i{(vI)EDqW_1dvyO_o>)yT!(j_2WDiVTp z3@rj8QgS0HDbn2x9nuX-hk}B13rKg*AdSS(LwCpUp1I%O{rHEj#d26M-#O>(bM0$? zE{wUE%FBUNo3Xats1C27v!qwHNtz~-U+}IRlY>l~AkJblBa$MOpYq9LMUhxTJK(<4PJE?_j9{c&i?Z6 zQMak{gXpbvmv1`73RgdRNHbm6+!2ChA`y&jb0IOdO9XMZi+3simG0EqJVb4>epY0R z7qpOI!l^ak5q`Np6&tDo%`*Ha^?y62?8>I=MV2^}zMLxYElJ|T)hPA>4*E5217p9>X$3LOG`1_Blf5y?`5M@>@d% znU+CjH-|*XM3eome#62>)&acBo4e9fCN|Y}+7jmdP$Rm9P-C)HZ-rz5z41cTjqa-^ zj+rSP&pw}AAHo@jY?Ct$1oqZ@k=eDsCiabfwie2?U&@|x@PVd@T68N*aj6@7zxw*f zwUxfMW=J_WS6ry?s~~u3V~==jXnujC@Y`rNxX=I3U!WCQ!Qc$NsrV#~5!5Eu>!fY4 zQ(j&zn?_7U;OXD-E_3iLEQq|$zqM<%+$jAWx88_QNrZ#P^1wxG;iGy3RAnJw;O6D| zNbK{TN!nB6WcmraDyVHC^0ysBhVohgySpaQAK~KW567MZ^3*Wi(Y+tvgEDgVGsMKO zTNQ_*gfvA7IdE3S_vDaO2dm#@yNmC0=p@BcGSGIdm|mztv10r*3*kw^*2vYrTF{!} zmmoe;;OdWqNYtkgAzv!)vs~ll9qah9r?$m872?#wr5LauM)*K-3$IpFBA?uFhC@?|Ck z0pTb39O9f&LZ|_~5Yp?h8R@q7E4V63n}Kl8n6BA4kI!0-FZ(|ox+L)@9PrN}h$3jR zdy`crWoA9A>!L?Lvj`PxRQ@*V-UHO$MWo+3l<{_c#69J|ZpyeqlT(>B?FP?Iqxq_ol^YUuZ}BM#Y*)~%AxCY&7=nG zg8ZIQq3yurW@zwJDyMS#DE+Q9qKV_ykJ|k&dOd<%7yk|$GhA8{+Y@TbkaRKEh+6LM zH;d0Flc(6Ud&N52z1t9Iw7tHJq-I!(ZtGefwx8M8Zkb&z>gYB(?59s25}OF!OFv`K z6H4P6q+QP!xe>_UkIUhC_? zP80p-BKnd&##zF?k_16NlSxHW3lp5r=`t(QtDWW+_d|m9t+z@LyIsDGcD|s`VCWUI z^=w(F;@AA;B&cVXKM#2{*fEVbru#4wwloM;P1pa=FjeN0e5%{G^@jZA9aHSGfcE!T zma=KW8)q^+ce{V9aX`g=oDz*YEIper4aZ*RV146W=gcxH{PT2ALZ(O(yX&Z$X5iyZ z)yj=2%Mwd7CEvrnNBhc~2bj}LtZ+W8CdnJSH_vCo9hDC`pO)WGB2gdf#irOYxtzOu z@?DzzDS2>@X_8$7Hi17^4KqtP4OKG9TXRdtc$+x2$UJHn0^pbRZHab{fmvni@+i;2 z{&~`Fb{{I1`w-4!L2NOht0chEhb3{iEj~#XwO*QKOu27=u`+ZI42XTV1o6e^R=)X=m`M#OTLA*q1l?$+h}TC1 z_5{XsqpIpl-(N{7_u;E%jJ>uJY-f8^@Fm%nBz~1h=>_>Dj}UVd>19xq07tJA=*m8c zkvX(^K^lrlv44NU!>T6bv1<5BMK9M>W+L#f==y_TilE$Hbt?rz!`83K3HkSv{Mo{I z=Mzr(^>%S>WuF(QDOw#jVB|36C5_=Iy^a@gz%qPc^m2)(jdFO8=+z97C%eKsJH}Al zo83F=8n}cbboTcu+edfuSBUWJ+30d78|$iHmT1!YOf1#CDidx^DM7q2Ay7zop%GoR z+k7XCVBQmt81|BU@<0~z*Q@82_wKq){gD!49sRj1(8)X7qvzQ%U((s}~3-fAt9L?ngZPM3h(%``#uK zSJ#JHA3s{YA(8#46NCFwi6qfH>oLdAP(dpp<}H_3$Oqbu!pQ>Vn9{N&1OuX>wK7ym zk6QlMIMWYoxottU-!>n-g0AXqw5%~`EKbdjiDf&~&>f1Zc2Ag@FOOurZfm$0kUAvVZ9LNT zUBIw6xkY8lersDT=c&cKuPq*AbKiu|O0Z+>BcU~4)EV<4dFJWkE8+>9pwGb<8aYfk zYN}x`V{6zg=UHiwEyr{PXvsYOp{dF6e13-#kSIwnm`psa5_CV{*!rJ6QnN_Oj808? z-5TV4H^6#$ulm3)9>U%#l7nOBE&y@776BebuT!D=HHV#O%W zF7-|Q6(&6%3vJlwc~j`+w7JJpA8Tp@4~W>2l$IzU3wfYMUR77u3+NDi{BUu_vdP?W zE5O$5t&B1#1M#lR3NM?o;!H($>}a*fAzGU7<13#=?ojgM(yyZ%!!|;|x50yV0OO@P z9>_QU`p2fCx=xnl6EKH@NpHAwC1(efOo?WPmn!(alRFQv7T3LL2>t=xID@j}D`J_5 zob8Xo-C)91A%Tik`(H!oWs@W@m6p&;+Ubd5Eo zoWbzc$t$D=aD2304HZE`8qacvEw8;fIh^~MI?^jg5g4qhYjk3gp}fj;4?=KwGUnk$^^v)=$2G4pQNyp53_@~=T_SQ`9np@>bXZ>(#kjMzl;W@s- zuyXW2VxK$3(~)d_OVWM!+exQ_k_DkLUBaikYNQFWk67+xBUg0ZNJLl)T6d~R>p2Ug zJDf?_FZ3!;@(D`xEsfj@62QKvMDn= zoC0adgT7m(Mb0WQw|~B4v2XdcYa8wD$mNhR&^PXF=wX;Qp<$AbS zaJT$l3pIYSxn1n{9G4?qa>ubc$}YoH=R&?n!*5~#Y zjc#{qFp7SWFj&cY=AyfxXb4Se-R`OP)QK?`H%*3Ee^VE%RokQ}sJ*0;id`0{MNBWr z`(7JJp0}}=))U$JsSidA17we2-9ZQjotJT{dcKO&`92XBSB4RCm!+xg-VO}Pqu%XFPIr|GXZJ+NsjPAT z?e&>&8nLfY2)=n9AoNh~cY2V0{-!=IsO|P7sb_%9sHDq7m1<36UEn+>SVf_v8$cS+ zdjfFBo7c>(fq~%a8C#Vs@xH@$QfO8cU=9I$x3jbJ;Ls4rFGnXDf=7gTS0RB!?$N)a zI%4X}%sIWzG|?=^_&dd zk?X{I4go^U%_tCg%N%9D_Jksa2_|8t4KXP@eZc|fiO-t7o&fD`1w6xm3d&DSzZr_W6*@KXo zdenmP65F@rJSAmShs)~^7diuYG$%BUR7-l&8M0uD#=|yFAnASplkqaewSKNv65^aC zz5CkA^knRCTpo@)ivcsN$f3%pxId)I-6G{r^;|YuKN6{s;_g5T#XK- ziYVeojxB#usa3Sj>Dt~FP$RD#*&$)!4L*}qh;C%VDgGwBUQyZj#$bhtprn93s=~!n z!m~+I+M=SH=k*np_)s72h=A80Zk}M3WWR6!Yg@lTx2%&lDkz}1N-?!X$DZclRuX{@ z!P%Evd&{A|9)W!d=N*V!tlQDsb4HMXA=>km_^kOph-?%C$hG&{y1p?R(+rf93{MM& z_$5YJ2;gdU%Y$&K&Wn0`KHmDl&Xvcb{Pc}d3X29zIB?QW`Kf7EiKnqkRs2e0VoJ)@ z3Rd`U^_U1Kc-!b;8IMYb)X&vFEs85R#lN}{uj1zzRH;eL+WuqX0x6gJKCk--t~#N$2mHF9!BfnU^s1F zjXZ1qt7`2GD~`-kUH9=ILG-K0cCpKw&tFcTWvY~AEI%t)4sdH2SQ_E$rlJ`_Bbx;V zv~?`WKB03EKSE*s{dgQjMWEdGi(6{SpV|7eMc5k|2#F~W?l-=*ApwYUV{)}J)=z4k zz|;v=A2Tk1MdY!|sSWR(fdxu`6Q`r2V{B|301mCSHLe9ckqjg9;BmJLQ)J-x=^D^L|Rau8D8)pZ|Br@vD+(yXX!woA?`T9i!v`I2s6ZAf)5 zZ#NKddPn;C=A^R)l#M-I05d-=b;FK^|Rl!#QFaf$YIYiORI~TM%W9G$5#Wg z6Z=WRh;}$?`>t8uVyy2iCD-N`~)d*w;1L*LQu_%y(CJ_UeM%z2Q68$B4RE;+x^Q*@HambwU*$_X9_z z@=mdNUktxxdv9*3t3U1s$-esxyIwQTy)Rp?fvRrmi-MkR^LoR*h>vP-PgVD|Ks#zF z)Ne(i@rJ(kaQ%tl>hG=7L*iS6REOYX+cER3%MGVvW#|>7HrjF)A&nx6b9Ir8vYv9lgXT4a%IOCTRmM4)PRDwGg zI|k%uc_%FAt5nvNw;uJx9!%xNI$n<{c+PQwy^)1A?RX>#8Qa|YBvz{1Bv!Kfj|uni z9^ud0w6O%56SGX;GPpWivO}u{CCK2l%^SlY@rEj7J7un|FEJmmMqBy-R#B(1t`!h7 zQ!+CFLuWDLNsI|7mQFlR^ z(X6!PRFumg0|}Dem%Qe0B#Mi%0A+E4TNYX?fRvpj+{N?pLi|f?CBW+K2OFNkw-ChWj%ml+YcTPObn!1#n6eoq|%0EiFNGV9xwuM z=%wM&Ov{;L^I=UHu%VyLx)Se*$UvZXvXq zOa2S`94-9M#K~STvat!=xp)nKW%A7i5>|Z;LR8hZsC6CwHN!LW_4>cuSrv~Lf+d=%jF97bzMM zSqp{^+biTDE=Jf=SBAHkQu`?CO~DkaRv&v+3uQya{8}Y<)A*JPVZVr2-(&q*&-aPL zG%>Ru>z89Ji{}O|s*o2bu!V)Q2&9)oux|_o^QF$1VlIn>Tp|)bfs+W9B{2Y!eWz-? zgzxBN{n$@gKjO!zIUGDozf5G7U)H!=`n*x=$RVGq z#F4<&>0Z8|NZZH=DZ+EXHw(H(O_7|O(&7*RUhz2~MUW2S;O{HC0MZ54rgPz7ic!Q1 zMF%KO%Y*e*oX@%W1^I`L2RVCGvlh|;_>`MYs{61X&m5j@;^-Lvs;F;pkU$6z3Vz&+ z_T=VX5H1M}|9Xuc@xTs3%F4;j?F{0sxeOX#llFYK>?fQwR4p2R7Hj!XRFEdxO0sC6 zA>kX@i>!y36J%3&9anyK#mqIBSxhj6zaJb_N+j|e%*C!nVy&a>)Zzr$0!Hj}c%If-=++FMcHz2F^i!$n+{%=`%4F|OzLZXOL3 z*`f|(uOY$+Cys#u+rvhq%JCHd)Lhm_9I4QhV1Wt!=9AW&3`wsf_r{x@S%;w-?-LHg zRYMSzczBK9Y&A%z}JFd{z}c_r-f>Qpf0l zUhp04e@t8Kn`fg0TF$Oo;HgK>=vSk@!u9QcCxUqaK0!f+CN&P3e0hfDKRt{{ee@Uu z0EU4a6hqXiU%%gF31Vq1b<_phC(927Wq=+(aNC}01Ht1CFmPYK&b~OiT|^vBC8|33 z+Qq=W!uL>r7!P|*Vx7;@_dwBOhjjPbTfWYZy}^nch%7Gt1qA>xh}SNi7ZDxE7~Epl zlTI?5Y9-NChYi$p)dGX|K+D_zIYyoWmsR$_v36u5d5Ew53sPa8v8fi~k-G$*4u|J_ z3gy}1f1{Oz?7ml#IvnVQlJ&qqnZ5@_G|hVAlZ#xZ8+ygvZtS5re7A%pBL^0_Zx)8U z_c0jFCbtb<;>QQu(V~XRZtmaCa9`sZUT2ti-t#?v!nl@aO~k8Ki%H~Zdp^e6(tW$- zcXf81e|vr>h}^CjO1$UUlYU;%a@f(hY^xm8Qr_AEVh$EneQ(|C94LIZxe>k3h3VIZ zo9Da{UPR72(&yXxDo3%XHv4^UR15K8Z|y>saC3a|IgY`GTKe7B5nj$y*_*sVue)Xt!1p@Fo= zxFWF!B}RM8y;QmB_lG-T52wgf)ry2Kz=%p5<4r~O!hG$#Jo%)@={kO=LMA_Q4dQ$% zF6O?sdgZr(aowqkBDeRMnc2t}A(pzt8Q2+j(yq{LkI@&a-x@jtq3wQQj28!txf4H~ z!Ew#S#WkE_Vcg;GRyMG5OfEd{{fY7BfN`17zA$Vi?WXnE zJ8O<@*z@7PacSk3&oon4B? z0)pt!gI~2x(O`l*I)s+9aPgv&bH<^%r9{-k6r$mOgYEF+}PY;dR6 z0A3=$3xIKR$5-?t-r+vjTXgldNz?lKy(UQL3K%YNFI6qUx;j}TTeYsDSX$|LvRtL2Q&?kcY#h&1txsm5(;&7vK1%{4P^y8@A`=+U=V_@f^FBJtY@Rj{QP|H zmI3%OwZO*+HF;%Gj6zQ4+`C(_NB+SOIo*2GwqV?XK$aQ_K9yvkaSYF zE!W)Fqkw5DdAvxx9I#c|lJzsm!OkuQY3LEKF-`5e-Q$p1|+ z6x61nQP&02)M$+FiSS&nvhZBzfjTP?f{cEsyDecm0?@dZh{HJvI>x7UQHJyRb6)p@ zdms?OTIhAcCu{q>5s3?CLg{ zM%9xHi0$eeiL1m|EPj1p$u~O7!ewD03oI{g)iwyog-iA2#!9qSEW<|_KO^6fzZYkR zNvE~_L1r(}BF;&<>AWbCkr3jhtfDo?Le#wVTvJ0DgZ^9Dy!_ z8u=eP_tdns@1q+$R0OXcT4$cOEN{>XMOBnfw&o1bLkjjknl-{xfzB zAnIG8Y5MEjh6CyvswryQct&tooHlCHO4+=BKjZ8gEG-^j3Y7$C#vZTcGaq#B5ZFA> z)iS6OP43JdhRk>Xj{88SJE99sFKfB3zyf>}a3>4KyW`0h;op zcD9}PW)+-Q>2~cQcL5tG!@{WK$0(42k0x z8)>*-&0_omLwKG~#`eRP)H!y_BnD*OdsZm&%Ffs`ba`n6f7an6HdM{cN(y8pfpj(? zi~vP+TIc8FfK>)0gflch+c&Mvs1+2T5I>Z-!ap%0pONfY#>g7vSONd`Ol>TBP*RsZ z2WJVwq;fAW&xmK;@pRjA4at4j`-FRcybK0*xOywZ>ZLg>2q;enc2M^`ro zu?OgB~=SMz&7C;>}^x25Sey3W|aHmg4(JC(yHI0xO5_XY~Xh6@j+C z{?qbRk~oEbdPqQP3~I2T7em>8VGW|fsFl@{J!*C&BywFHz!KU%0S04IXGv`s_N}hx>i?Bf6fmWp8%Ze6ELt$K>+|r;B70T$$_h)TquRim33*MBTeO^FZWq)f2%SeCn#;Av=ZZU7m zLPfxVC4b`edcK3N?|u31>w7Cifow)`CmZ?n?GUCj2jhwk*qqpYfT<(3=8R8{XV63^ zdCm8(H|dq6Plg7llQ_i>&-t$t4X;go4{onb8N81jRkB2fz*gCo%Ac>TA57n{_)E<0 z2~@`<{p$^3Z>3yY$YGW6QJK<>_`nNC4~f0AhmWnif{6`!{U(_7LZ~s4C|er zPr$fq&Q}8G3}^9+l|-=G(Yh>9Qhv&a!=0BPW_ET&NV-WqXF_}Ez-BsQ=AVFtg~qw!)4MsJTY~+M5zUW7Jl`?ARRP1Bb^(xIz@~c_Uj#a(g^l zSp|R{xip~rD^SHV<2sToHL+ISO)SBEX%e0jW>#W=6(nQ?B1!wqL~h3oQN6_Ij|-2O zMhM@x3Xos@nATzQ9QUUvDmf_$oqGcDI^`^-0S=K-H3$}Hm%L8+c--vPfY^CMJiz4Y z@|XRFoF(7Q0rU%KTc~S{E^D#YkIE{nywGdx3>=-`S6^hWC&zq8JG@+O)>DFUW;0g!00yl>@(ehXe<; zzmLLw&*s2w}O_222@cis)y*L2y>txpbtqvHv7YfMP)L@%U^Zy_J1H9z+9N7V0;kL zfV;zhb@M5dQL^9gx(gpfjQB`-url7%XRoxN-b?kX-VBDGA7nSCip{?xYMR^Bca~UY&r017 zF#I-xl=i0Th5Gb0!rzgb3cldkhTd|~NQSE9cZ8F%dVo%c$HF)4^NHH#E_4bY`hWxK z1_(fT`ZHvHpfbv>fv(>gs2-rR>bO0{tCB4_6pT-)t)uge4KJ-mR6X&z8%(9D+9RIZ zXVDqdY}t$N5$7~;@f&7JB%EK2ziWO@tfKp9dsOvsmD}gvY#8z5g`49BTeM&ZzJpwn zi9}r^)6R(_RQe;v?;WCUC~#$6Fm>chvhB%u$)^312ItcAfc=Jq{^^alB*NDz=qgU+gCRmq^zFoo`kxE2z? z(L29U++q`;N96uF-)(7;WD3t?KNOu)k*-xIO4TmvXp?cc%EX4wi0!WCrQ0)ye`~sj zgtUfjh#GaP945eDEa*X?QN6isX4;nsSZ5(?((upWm9h zisUxol~8~hO3VXs<{Ni4kCHsyjaDGVxyR=89+h&iIzpv>zZxpIJMZou)>K*PwuZ#QkkozdIlbuoF%&n*-!o%)@8qk4%M88e z4iVa_n~&JyRVy>*bx*S3zswuF^@1DI{((yt4c6Gy#^OC^Vg$s8+|&Z^=4^7q3#X~3j+LN3!~YAs<+eX;uq(*6cm3~ zra8UbRQV)k7S`UM@>Q8|<42-LG%{Mw$1sdc=C4pQj;&;|Cjh^Wv{c?+N_vmzjg~jF zp^hVH!sBI~~M9@WkRE#U)GDN>U z_t1G~k!7B4r3eA(wqkv02ag)Oc|9#3idBmN+ZXYc?xk}my0iqsju;Qem_ zl(e$8R3A%9OGm6~97?yp*G|NQB+k5Zb&%4xi&#;VsP?uL@ELV45*P(!uu4WjyO3v(I+eV$BCZQ zS0xD{x1`RHI5{T+s%@2tX9x(tK>q^{l8*!Z>69__CQv9y>Xj##+n#Ai27=;Z1~9;V z367-PIJ*LBh?$vL`2;Y%D3Cq~xeEoZ;awmI$vfYT&W>HZdLt_Hk<&HKj9zC#8?FoCtniU7koAZQHbfFCh-(hK zKpoktI-K^i!YkI)$tr@aaM^$SOXcJJdL%5-EaWk9KY2e5-Tkb4v>yAb=G0n*^+-IM z1st1hTDz8jje^6~_t(EC2`L8uUR2c8y{?+pbC)F8YoH5ajo_Tn-8`X49OZZMVpo;8 zA&8YYg}|l!J}36XI#24`FFj$r;BlXKWzDop$7b}Uq79in`&H|>a!LC-Fy+g*P^=UR zLM*<|gM?+R?gwBr-ZUSgYJm&;iwGTmc&$GzO|%#NmphE*kVr9z%@ z`G7Us2x=MbwbvXruP2Jan@FT`hrL{xxRp+$omip^VgE`kpWUoLy*>SikVc-B}&op;s42%W|Og zP8Bjf>~Fo-71{W_j2#9IY^`i;Y^NF?O{Lr=d3*Et?KBv5RyB9f+#X7J2*YCa>$u?yGM584pDFR;5o~@{ji} zw0&-k8Bp$SjKam$e+fD7)3@^H?5gY$&1%Tgq;VkzHgRnk1XR!B{0neO`yH!FDtz31 z1UKuCv0pB4CJ>9_Mot|F{b1^T)u5wqI~qk#Mpi_1MIh$BHtTj24N9xz_T)@!g{D4W zPAk%bn6JAN-t3ZM6Ys8+dfAzMc=pjVuWfUav?oRiP8LhQdYu|YYu6YehG-1cojaUW z#~1gwRTp^g3Ua!b+#3(fxC~O34??q+)}GXY$|)9fw%|UyEk^fba>rNZ_Pi*QqLx{+ zRtHl}&^OPS*v5EfOTP7!0%2Y6o%d%)vsFFce)9gosaq2rnAvT}?CJsyRd&r8ThQ#2 zYWqqFvPqpMC4l2cZ11syn8(_Nv-eSjdCGfj(_u$}&F5fPQ*8a8x+sXkdj2Y%V4efB zLhNIofQ?jB$%}Bm!Nra^n+KwVp&Md8mjdXzWq|MJvy_mdb(_3?p0Gq3b9m+|s0#_O zFxnxylf1v}kp^aF{daOEw<k4LuMp$tq%2jD9Np_E7>(q7ewLat+Z7OWWg+M!?vqx-k`LbLrCD+O0YzW~Al#mIzmu_SjX0-} z{15Gmq(H*Q z#lf@rkYv(V6vp#$FRop-x**H>hCRviTDOUW;VOfXP&+Mo>;PX#46dWs?WA|b})UnF(_00xe9|+pK$Yq zAeUZU>&meT;9xVXQ6_t=2VmEq^tz&d$HSE>_`wQ4d_T`?zI~?_@zT5oznKX$f#>V| zYf!o^R~8A-KstB2U7D4;zXsiVbOE``d95CF>3L)?TMG-Q)6r)noAj ziDM%xH3SqEMt8&&g`K#!KR=S&$pKDOS-r4y;XDXaQZrph9=EGiNcf)SJhO?-0pf9Q~z|iT@%~B}7A*7QMDU z=vR^b;Qs(bWf}neGWysYv`93IfIdb3f1qE#6`glCBM7r~f$JLsFopv-M;zOL#_?N@ z(E3AKc-Q6A5tJiM&E3Qy93+^x9#-@k+f-hIar2Lhei9^HgTsw6dIoJG8`s~~F{s&4 z6L@-1E>~Jw)x>?T@h8NqL}6M=F)2L03=&FIclEr{V56(~F84R~bpnmz4m5%J8)Cq+ z8XU-q1S1bCKLneZ1Zq~MtC;(r-4}C*9c~}NNvA}sN_>h-VJ15wrA>CkMI7uw0{KFJ zR%S0Ew^Kwqm)fD5HA0DG$`r8gV`*YM3hi&){oyaI6oPmNHq3qSl8uA>1%wgf!O0*@r`jaKYd+i;EY7^(HwN6TB5OiHYZX zsk|D&&#nh@r*njv?ZZF=UI?iF}g4#|+H5}x|E0wdHE-N45I_Ja@gbHfJw2IKW8oo@%KzOddK^Znn4nNgf>(972lB%Y;{;T3zRH@R+QG zVn{c$tKJDR=yy8MGRP`e5e4^5yF40t?7(_YvtF>6xe}QLNPQ|ah3*_my_Cjr)$6gQ4ss9$; zZL(9M+Wz3QM6=oF`#Uo%I5OstG=@NOaBfoSzEh!U{e?8Mv2ngZ04=)6}cRbQ4JgER?sOM7TTeHs&L; zjd`ZJ>PLsIW?R2X1(-wxep+>7OqW#}QsV6$QELVL)1bCHS~nNB1Gsc$e_dGoVFF=l)@ec%0!3KB7R)=HAkMPJJJ|z+d3Gp-Gd0p|Br_NW_$WQCsyU>68~j z5YzgST=%kxXy{e6bPJeQiZ{oI5Pupjv&6Zyfm7V& zWk^)3rQzxNTLLZUH}DY1cl>e{&HUrF<)hPfs&9&yz>@M6&a@XtK@F3y6Mv*kOFk_C z6_s0X=cN)@cYPil8=lBQC8N30*~bh63pVAukYZ6yI#1eTWtqdrc{3+dUo~J`AKzEc z#Ek#yxDqvngbzIN)Rz}6$IO#!5+dX>9u9SDP)!Ot0+6tdC0pu1k!7ay-P?2apbkvy z=Q>FbFz;4XjaKWLjxX&W85E88ll)@q=-t?`jN%0_(fql*$z1>~Psz076O#beq$;rG zpl?gEMRIhns7!zys!b5UD*(Q@_xJw1i3Cu(A5bZlsb|I3=#~&9kAAK;3~2{OJm9H{ zm;JL&r>(EkrrAN>ttOjmz}S>EJ*{I`e_@6iA@=o0N3{ZXT9m!-4ej+btE2*5&%Z>? zwRC%~(3oSM^|OL5+!M&+ewz%)^<2W2_j`*q2NTgtG7-=9+Xsxh|NkfIYWM82&pnx4 z(Ke*&hkwT}2LxY|lkjb8lC`jQ0IrZXd3#4W1v9HnPwRiA26~lrOY8vtJri6Y)yn0j!NDkdeSJX9du5-b~#zQYtzIVyp3Z}WsjAt4TF8fI#%?fQ>Ql4+-7NukFx z#(V>N&~i{H5yET63Tk6p=j#S@L7?IBouVwAk{#zB&#K`gFhkaO29{fV!)E;$?mflY znI8safZPrU_p6R9H>;*mW_eGh-EUP64w>f>eV0LpR4jdL?+5|`P;`I@ z1(|oKHnjXz>~ttSjng($@f%y-%QQgxFAL6B&~d@yjiItmm#v{5;cS1E*m=OL#wSfK z9JWN073eGVbU@<9;FZ+L!jr#qcb%On!NUO!&_{357GyNVBu1o(eig4*8NaU0PJjkI z>3k&^rTki5rXfA?VaWH1VpKwBfTPjj;Z}D8pLX#(>(9x`4>vWq{}95-wttK>l(p6; zcDm*`l-z@zJNb$xn=1*>GAq3EozKW`_?tJm!9Shx(dp93~R^Qd0dDc27oV-B_GiL5W;W zKvx+&320Vjz8Fl!#alm_r|TvYe0+@zujj< z$pAqb^~vuPj-tM8@jHyXUWvV7^X2(k`}+;~r$HqZUNOO;_Tb24G5yOq2C@>Y>94B1 zuTxXgIz@VBXERWEum2n27W%PxQd*v@zEe59uSc6q%WZG%AyHVb`GL)UwnOSRIrdPo z<=9+yG1JA9|NH}u{6DArY2L+Nb%PvDj&+NI$ckpV^8qj7yjChhH%JG|5yB%%A>c!` z3N-(IKXPXyId&(vBC1+f$R6x;o+gcjqMtCNmN zivf|%5aJEOlzZ!1WpE?CmvO2e^uwyyQ|_h|k|>Y=861JG0rl zjDB3?HSb@;EGRgqGRL`ss(WNP9^pAZo>`XnJ-({j^~532Uglk)GW3aSx^L1(gQo=( z6A}?opbE*|YhuDNxW1aK`*Wq2RYhSgbfqU+%t#da3w1ML;@vt!*58j91G1P5PqV5mM6Z<~_Z*n@Gqwz9_`o z{nR92to#{sp=jQag@kvBP410$RPFfhwsoDPGJdw?<)-qyO`bE{c`&o~+ zxXxx@u1yB6|KA*Bg}(LS-iWtA>@;O6b$&Cuz6lyS}HvbEp1x&GfN7Na_}j(&c56gns1TRv~(<9{Oka13i4 z>3$@nhaI@-9Vw4Iztjwt>j;@1elt%1PyQ;-8dh}l++aqv>->+h-Jk%-JF@z1y)p)c3qHp}3fZqqd?sqs7jLL#wvgMmkl)9w&pX zWwHC$3>8lF%=Q0&VuqX!AFgXWw1nI@eyy$!Wvid;G{Ve!0B7pXf9<5+@tyJUD-MLC z^qmR+vIwwucmECJ3}&{_e57)rwEL>Uwk_T)E+HLqL146tT?07 zFz3db7b)lywPB(p;+^XFr*_iY-}UZB= zsU4$z^RI7&wKE=9npZm3=a|-{-dF*+z+Etexk7Am3;(G94p7n$Ih~9)U7W>$m41m z?C}Avrp^CxuLBD=(xR>8IXlvUcO20Xn?yTdqCa`i{VXN9oK91r?Qe=!Zbg}pZ*}Na z$@76{@s)><9ZXk8FP~yI|2ELDj6&Eun)Z?dnXt8wC=}@oNlYAjA}*pZN7@y_xd+aG z0Nh8VC5%K{PTexBdp8!ZYoqUmcq0ipM1;1K{3;wAuA7|4q3d+A0jyC>a8W8gn}^~$ z{hCCk$fA_z`yJ=8mHZ31s>EG3Z1sr=hV zm0}~9Y9+|7_bTZ}t^-d0;xgW{gHV1sR3wAWQKO5bq^80ur1Gz^2cmrGTchOvW9v=8 zpneFba?;SIe(Jn z5xepq`8JGF1d-wE9UgaLm;(1O9pB!uJWii`=JZXWsJ1=Uq>%KRLUryO{5Bt^Efow! zxVi3@*)W>iTdY4Qe?9*$iPP{!=F8X{!!lQINK{OvJj=LJ>v!qqx%D2^!j!*W_)jHg zWLkN!XXY-vD_apAn|yikIZ=-E2D~1YFpC9ebsdX`D+hymB|Xw>JYBW?gg6iJYBYs- zycl1H|h*y9)KkFND)|P1Rm|IDV5nchzH8nrWg^AnaIPilTC3S1B3W5b%-fU@>b( zSh`}ia<;HgZoFRV0dvvrv6teEA~#%^Q|CN8!?oK#cWw+-tt|GK2VL(I@7wXPP@Q>TNIC8Eq*zIyvHtyhj``F6&KHO`f+oQVopF*vu zo&t8niG=}FzVti^Bx!>ER|QzHVzVtFcl;&SYPU92%UR=^b5sRB7*-Q5^o17A z3fF{|dQL>Bsw~R+VIFl%9*^qGe(>V;E>S^RIzczLU582#vh=~gW)UMz=wfr)r~QQf z?fY^Td|$3-$QYyUNFHQ%*^N-Y(aRA){r$FwC(5z)8y{VWz>OC*qd(2xiiz1qrFcug7TA($+kZ!99y|7Zmn^$ukoDlp+B8M?1js{ zZO_1slg-}cZgfWG+2qU%JBruq&C{EmYQIXtv|>8eL}uUN=oi9Ui9H@|zrGziqb=Be zwr{nAk<|O<1-*RtlLd2Y0Om#}4akSP+5dAv2Y;}v7@U1n#T|5(m*H}7i#z}DMs@fW zk!gP4?WrTbMR1jRA_0je)&U_UlI1oBsU4GHqK`?j9KxC&-(SAmZ=|I^?yD+#O#FM{ zDO#}M5&|2wb^+lS6D3u&@K|FJt@;IF zkvr?vBI}-*Gk+a5w5Bi2Xz=;M%AZj)UF{Jk6^O25lL<=7QgQIt2Qphh^ zM$x}EZCQQ5CA0H09Tn3mvA$cgdetoT@@n|Zx;CNhL1;f=g`hYfbPYcwK|IT|x@bv0 z@b1c#&inTZ8NvbOwS-I3gasM>Tid6j zb2ZKJb%JbBCj_#SV$oAGV;QCDr$l@n9^y&VKgKdX>@7*Z5zrBO= z^VN~>ik^s_riY2}F!!srmbt?>m5Rp0Z1^sf|6DvFi&?wNdAqM8{}eT1y81Z^#Vw^6 zi=VB}dhoz_$NG}iA9BnE~o`28G0R<;^y0Rxv|w0E!_fiiu*W z#7#R{DI!oT+%E^eJ)8cVv}Zm$_hipJS073|C}^!T;D%^yif8zbzc z-nXQaGdGV)u=^UVd98p#(W~3HFwVwLdv_@F$WgX6LabZ-;b$kl)YJY9ms%;|I`#RT zEC=1UYHdI9;|kK@J2J+dYmSjPeE%xxv@}fCwk(Y`E_yY)X(lAgcdnQCJ7CY`jbIyz z^Y4O2#1ycPw=XOkV{jo5uZLW-_zJmUg0s5;vhf#u>*wEF7B4)b)z% z!dquX zX>GYioTS7j5w2bxRr*cEeNrP$<;YBc3$vQL2KO=J#AvOt*R!n|oO;VOx81)5eJO-VW4wq4nt>9b4rmK3Hcs)pX$yVveeLoHf?Bhr)rBQ=| zeufsCL`8yrbU?3D*+QZ3 zlUEfNy;w+-f4T{X1@Y+Km<$q{c=>2?$yn& z-SLBNxE`+>Uywi_7=T9X2y=rtzP4P>tly~kF@1fyrv|cY_V&bzh_6H@@!|H7t)YXp z5trBq4){5T6Qdsi!<9R?8-7nw&P@sM|Sc%XSt_^ zSQ$x4l+0s^G(3--$6={f3G0DmLJ5}HDD9jC&B5pg$wp@|pHp{+D9iNJ1YOZ|gQa;h zMvb<)OCr;@UhDOtwai+$Flipw|Jp1FV2eiW<7-S5|FKtB)x)-E-nYU?*K53^D!Vva z%Sm1=w_*3>!3&bD5*}Qr4DJg`hw_JDaD*2qMA&NPJzV??+0=YoF8VVU(n~SOwyn$kXmV-8_;oTi-*=(K}zCEaQDT+sj$!*PVUca|eSH z$z`4tnar*Rx`>YS@-1!W)@qZ%y4SG)w8AiN4`%|EQ(LE_)$z2!K6y1sdOF2HGZ9hYutT!z4m5J+=s(a zrVY_8#Q5oT{OxD$Vk`=!Pb~wj+NwAu8!IoqjbLUuBj)MT?@FxXEiUe=^>GuMR{S`Ypt~kbSRI1DD|lR(TG9S;Xzw}0 z6J4HD{W4c#OcyHEXO?GPQRkQPk6$cyla9KhKEs>2{xsj^!K|#U^_zPKQo^)Znb|M{ z1N2_QU*8Ue3fod<9)Dfv5jUcbivFRyNUYPgi1!{vqJ}ld~Rm;9U_9HJC4M!G0$Tpc&90MF&Mw}5`^XIDt8FI$0O&6vf z*-80+l8t!r(DkBblqPDM@bJxvJ26$hL~CO~)gGNzKgtM>I+6bB{`VGNk+iPK%a5p= zrwL*Hev@7K{|rFw*=Y+L!fI#-BcZ5J%x|%vT@@D^9WbzAl+E>wLk7)PRc%h^BUM?C zR(XJ1dF6@Tj)+LIU0i;~JtD~6EqLc~YliJhzipSTi-|o;jHh&}4n5|zYLXvGQD&v`czawwp@`V`V|0p(JfIOZuE^EQ7deO0>-6v|>g$);7!~DdyadxdYp3 ze1)1np3XGc{n(aw+zC$q$ckSkZF5G{ipF-XnYW-?l}AH#VsVB-QuJH`e<`5`XE6k} z_v&L)ulL9cH75t{CjOR^wcNgTU9I5V!s+(Y;gSiQ$(vEr`}I(gd{0m!z0nt5nOCE` z=!`84oAu}-KE)`74FuHo%4MN``5a7Ie5mz7@#n)H9XVHr;XlZsxmLXI@4!1#(D4#; zt=|&;9pQB2#dllY<_`G)k#{zyZ3tM6e+q#)=P>ez{NDQs`XwxyKlKl)B_67eB2x8; z39bX?pQEXQm512~irmDQu>mEM{ce)qgYlP=%el|@JRM|zp=Wq-|1*{3L!U?}i=Q`D zB6y_=z9v6Sv$2@EKPh18<}HA`&h1QBy**Q~?8wR2foEVqGenM(qA{e~tV05h5pKRG zj~D2ja&o<*F{9r$W2!-*>MqVd2+HHn?%1cZ?^|^~o_KmW^#qz(oY{ehofbvWitx%J zsGc2u|6(jgYtz;Bb!KyI@+Z$Lz6+%?sKZ6kX*y}Fm}0&T4*v6~+>sbAo(wlVG;?6wEj;h! z9(YHL+uHZ}wX9ec;_ZFPxTYYU{7xe~lNPgj*=-#-|k$X?N-5 zzABuPn(yfA@I7cb5u4?c`Cz0{;iI8O^TaYq!K3B!6-_?>&Q(jknIGA`ovMEJi8XwO zeqxygI;;8P!f~$By7z{>k`-2!l1=|NiRyG4%}!fG&}!M4*XNrfw}+nIf%KsU4JnDV zmrKtIPgB-+P9iDc7n|Cu?D{mI+;j+p#sFMH0Ryz`neJ^I`RpG`0>mXP7{ez?51nKvJ!2MGDPS(R4jVQMnHHZd*^`_coXaFj%E0;V zOl)lsbgk?D^+-e+8FNgP7$oq~^b)bLI7Y5lOnfTnX7pK4lKvj)Y6&Zh!7<6Z91FN*~)iND0HegxPG_y z>+momN+AAE`p$9GUK9jFXvq_ZM@!Oc162fDPx_++;S z@BxiW5BOZZxVO5x8fGfyr$x`!Bj1mgF8~7}XRTrE=4@<|6pa$7xyfN=6@oEE%v2ZTaUnlVG8#7bzzzoC%h~)y($v6Bl-#wn-`mjIZ zS56vFy0NRx&iIPXv#s9b$@`WCLtZ2$EU3g$78?f4%#xq3sQ`lxLM*LrQd8)v&-&uz z0i{zXPB1Bkmy5P9NqJD-=4hS;L!Ogd^k=%PMpkhGg^}d_3@9U`*y-y?zU`51UuJFb zyg${MAxUTHpFJe%{I7(Y(^;{gHFUzOMtDb0mpMa`@tSoV;nc@|$om(L)} z)HyLZu7YMMg8lmKn-FNdDw~?(+^gT#omtXcE*%U!Ey}F#@749K zJZ*dn)X1|{ifo?SNZAgJUCfdabQZBp(sLx=>V&i+G|X z^olH7O3#<1;?ED(%c*K=-U08Htm0dg8t;iaiiDu=qSpV~ehEqXC40{40U99aClRxE z)Eg6XEZ<)|Vu@p`|1f=yrrxx|yRueak=pV* zP8A7%10Shd&%y!%RNX|6!G{oFl7}-l~+p4%rb`ldxQbpQDI=g_T@q z5bQg=?bezuUlT?yLL@hh2Rd6j{Va`PmmQ#b^7L%xp$SB zub|a^cG$kEzWzbu?tir|iHS2$m3DvV?q1#9rvC`$eK99ws0W-Wm7<}^Gn@{_S zw+c(q27V+;s)4IT^-~?E_pIvRZ_lh9#&dOXsYmdVVe~Di@DYp)Q8p82JtfhnlJ=CS zR5r5^BFV?#gf(;tUPpUST0q|S#L`EE$+R>UK6sJlSwy_%HB=v`*u2Fv-JRC~vE^!B zc)TKEjZSxGA^Bxf6ML@;NTN}cYN-1cY9cpl)|Pa4Z-#+AC19caicaT-r~Q_uOtv=2 z&F#=1=j?Lx=AL+*i?1(tnKhg4?s_+jx(DCM=-ox<9X4xgYl)(_urWY}vH;KL(xpon zXzwQF?XKrR#VuIu_D4j77z+9hW0Q3@n5oUXI>V8>;jpF>%yd@IFEC5`OtiCaLDx`= zS;XR=u#F!jYwc)C+aG+abL8} zU?BVZuKc{P)#tp;2WE`UbSSt6`uqE7Kc82$E;W94v^{ZRIp|EO%LB&hnund7TqnLX z>PDRSDPt@V!Zr8t5*+_CW!`}CCrP(|w7v|L5fl}8#WJZn z#wlbT!JmSWnKYC%5_`@cV6^e@;X@QXh|j?avU`%Z^=<2*Y~$AF=WBD`k!+Im@M%G< z-UF~}b#;7I)iEnMzO9+lx~&@<8WB57d6%wVXM+>n48}Sy>*{WS4;Vi@aAoAdk8Dk; z31ArkBp`)So1g(OCUPrz_Xu)+dQOXvZLG|i8{^3r@YwbB;6@n#95h!&raAA2dp!2sZDqITMu!l%QQTahLF$m?nMk=JgrFFC`_kB=7J&Y$bbrBiGShgY_&V z(L)N= z&Gdw`D9mV5UyfgYv~`uq+_>eHJmeQAsAm^(*&M|`kIwj$Buq-J^q==dB@Q=zp+b~k z^co=~m6aXVTKRImQl4#O{bL{m7gzOrG4?fHW3Ryp4kk=HHZFj{%ydqmlD2k|_47Ed znmf+B8|19Kyp9Gc;SQX?y)dC$>)oWX*I+Fg4&LmF&(wQCFufWt%5-iQhbLRx+;}&j zh7I@N8klT}FkStaLt2fP77g2F(k1RvJ9Oi57YDu#w@*7q++xdA7^?$2yzw8sGB$_S z7P}PTZ4x$q9)iooJ8VK2IX_YS&SBgora7(1*?)btdmI|MYW;rjIEAlVfM09CGD;Q? z!=JS&AAMhUGwVTi6z*c}>L@ymYi)U{8%-ioMmEKqLMHiOWZzMB|0(RF8-nFj!(yk} z|CIzM{01N`?m3XZRe8L?jV^-$y~kL+=f2ZR4Wo=Qb7N@6r$1Iyk&-Z)XJE zK6n;YzI+*FT7v0Sk&MMp2hVVBWaZ|@LixobwzzkG9@K3&+u10wt&ij^G8P<);LHnOI6k#^>WHRpf^iwE z6soscNmG{zCv?q2Ex=mpUzw}qrB6Rj934HxAcZBqAxWfOh{9P(d{EZ1Wwv54wvtFv zK445MiqU&uUWwv1-S1++eDt9@ispcJZD>hG?HQ%xnQu>3D9m$go5U+;AR~;K+0QuTCG9jMdDF}V^ z=u!BwXiv(Sh;i3j2@j`e%Kw1bNLi+Vg@VwhwK6}J!3e{}Vg8I%+HIJ2Wwca|&cbj+ zv2gn943jJKYmVOR=vP1SZzN*Tq{`O4rb-1FrZ|)l@hyBf>KUjT;Oh;=(Zf5?XY;q` zi?Xu6^9;y_L^_bLs;Z{uJKKL4ng^kMlC`37k=293i1P6EHe`;1+~-Ak`5zGLgoTCI zW(t#Ov@t_yJ!<^@?RstSlJxv=(;W$BpXrZKn?&85{s_1o{9j%f$W(N0pPG_}TPLb-8?RZ}#V`oeB6SVNv4ZuGzR{{}Wr{ zq9?(uWi*md8b-9L9Mfl1&>Bgo^bC5}E+M1;;ArJDe)AjxbIA=>rD@bvbn6SE(qR;x zfxwtD8^%Cj#;>8)e@RuWyu6_I$7sv8>tJ&vXO0sma+Y@b)xg#M@Aam5YJuHVisc_} zZY+>V7#8*r`<~m70+pJUdBA!D(){_Mh6HeHa)ITCeFy6V7wzdPyEdqUARY0c4r{-t zs3^279sOK%7E&W}g>wh=vLi+jK~bFKrOH>0cVA!qSm5;hQp3xn(hyO$B{nWDngf6H z+PWg`cOU_Lc|JZKa=&yb9HUn5-0}OjYCpMON%Qp{e8?J25?r>dZEWCFc_OtbJSgzg zyQcaOiMJ5#VEcd_B2$T#!KLeL;^AxD^TTy90ng)Zl-|w$A!v2zg1oNnshbVA+WYs) z_eM{L4A_nk+t~P(w2!f)^)A~S;^sDal*U@@<`UX)>qZ^6C_wD!p`uu>!sL&}D17wo zk7NU88QI5aa5Q{84j!-m|B`klP9+6g2xZ5wb42c$b29by#OBPe@W2h~uiE9xU$s!6 zh#&P#9zBMHV{LC%S_(jP$ysJ(AUDhW@Fwa96tA2Dn{i3L^ULpZV_Axe+ zoS{H~)iLP;MnBxAP8AQ>No!GWp`y#Dhg`k2^gNhfOuO99X=K#LgCCC&Y{D~gGyp2( zyILQ56K=r~kV<|y>!*R-z-|v}Z%i70W-YZV`vaEm!<9V%6F@nQWUk;lz~13Ix~Y>J z{`(@Nhko^rS@G!7FXh&!kUIf>+SN|!9M}SKdK1>AMJQ3{Z@`9t0tFg;mDIFjkWic9 z`gq7|0)hcN6)3rtuoxuufcz)*^z7H_BnjqLmoJ0zA9Ih#1e9RN{?I&|2(AZX@3WCR zr*-g`i1DQ5fZh95U$8Gm!cw*8Bkj<=la|>ge76!gSYk~7Nk7_-%q)NB*6z;nu=*a$ z7A>K9?TH^=Zs*b5Rb63ADL!!BK&tGs(I)y_-wCUy(ac-`rqUXeqM;r_&ub{zM*XbN zf2SHvOo(PFRKYad5R%D3-_>~X4D*`~+aN^PBWG$ur_|apCo5H1RTW2=1rJoAn24ZC zB~jt@8?yI&(I{0_Riqr0OHW@B@2B)>T=HW6Q5$=)0r&N{IHE=RMbmzO5(= zEeGbPxjAIlqIwornZlPDF%S#@mkDSPCCrwJ5N26W-*QxcR%vX+0MTezLpfRJ(C(t7 zS7yZ#E`oIn`19JX+%kum_FRA2y;iiCFiBFk_l22*s5Zi&(|$P!_vzn8=PLLlfvUZcd5f~TB%s*V?3g!meAsYw=Crq z^C@7}?qVI0?1OBPqWUBal7pmE7#ObbH+%(NO^|NUp`aG$sl7ci0?l1LflQS-JSe?q zG}IMl46EFFQN1MAh>vi!^Q9=gGTtpqso<@^A7}PX7(mDzWaK}Vc+_5WSg%BC^Ul7}bBgWKBfiXw=*HfmP zt(n)dXhVOrp#Kb=0BXonk6AAZZ$Wp0kL#FT8hQ*>; z@$1D=i$EbIGt3m7iwS0m*X07r^^{@>^T@3ztRy8P|G8eVP646qOg64hC-ZPw$@cKk3>_x&H5@ z)JkdQL_;*cC5p>>O-8#=h|t{m0&_}K$sK1dH|)V{S@uYP?!PmqV4*r8iESD8`BRcN z_HlkJrvU3aF7sj*H(PuqdJ-kD?e$cN7AraDvq)!=F!JtS1%4t2tn0xC^s!75zhY^U zvDmzfgDB=bmICJc+`l~eY}S4rXTywvh5GM*W~>0hsz;&dyuPfP(;iC{UNL8F9SHV3 zzx7jEmRBN7CPA@iO|vC_O3W6(%#Uyik=g--@ z21B^eCyhr%7wgs812*SxzbmdG-}~<>+{++U9FyLsaRBS^KU7>)1fEB&1TzW+S%k4H zBy{63oI1pVd129rDft?Bv(b=&4=@elI26_le*^AN4zi=byp;z}KYHlaspq#$n=`RD zG2-FfarB?A9Xmp6`rfdxrQMA|$rPn_6R4!saLBeG=R!dVGB7-BN?=1_0#lxdckkcP zqG`Vgbp%UWBUE_zP&>qIn&DOtk!DwN@hqZ>G?!Qr=TetLxfq%xsR`2$9nx z&n5ISMWkA5nLMgle)V>J_?m~*NYbX)D2Y96c~C#KgD@>Up)GhIGW5nlZxo=_1!llh znDm=8kC?^RP&|!fC0(`nAk9qeJ~FZ@Srj{4Z*D9#t1U*JT}f5>OgUFUHUyh}13l)b zzP`TiH|0(FQG~T|5G*wxRk)zDFufeWn@J+Brh1+@HTkPk;;ZVog5xPzv5L!!L(BEI z8cdsOuJ0^poGm#DD^j2HovLm&q#tP`HtsKl9;~a0y{_hcpf^Za?{#mx;>hkXosGGJ zYrnb_eP-uH|GIW@T;+2Yp=Tgkcy8i{*1KyX$>cxNlrNU(a~*IMt*8iSfQX-;_U*#& z#0<%X?l#hRxKwW7)^VMP|L%mIozKI3j zlM7G>LWT)ovkz1TVJm$$aNC#9wk~NgOTv8(=Bno?24!}J-k%bpqh0MF_*EVjX2p#@ z{+=drR|(}}2l680sjC9g=ScW8o?^H7ks}`tI76U#_2RNnuZo32W|WN46+0v|RK)nz zi`M4eaj#+}f%S%%M_0TjrJ=ayu<*9Ncn058AD8CjMB1avgncVp$Iwwxn)pkaP);n7 z@Cm>^oJcb}`snm~C2UqaORv(Hx@i`Fm+9v_HFqRv3vK+)VDL9O`1L09WmFe)?;;(i zr7^Q1kr!uYK{wWjJK=iGKrf$6)t6-`;sw<7QbUwo6=UfE$mTpKr-~fTWPI>b2#70``;giM5fXY!Oi8cgWnw2qyv@$9 z$kHpz&q^@6*sDHCO)=h=!jr*WeiI&~y6LO=2~SBU1ytpn&+!TWnJ$UJ$QQJRTq=4|pw^1fw@3cH7Q0;SAS?(2LJHkqkVChA%;=ceC!?j5=a}}t8PR6I5z9Nt1%ohd$@7q=W{R&hCH2hIB0_1feR%A zuzUIWkEm%fk=g8v1~QV`12Do5h4ri~#F-R;{X!9y4>QNMybKa4Kp8k(9GYGE=)`N^Yn5sJlv3Nu`ObPXB=Pj2kSi%T-EKjdlzDql6~vw^eRR$1z~Vnu!H&n zD#}>(>X;87Ogub1h%1heetCF!>KU}Ek|Zva^tKR%^$h-a{qj~sGbe^IOfWqG3r9<& zBkGc-Oy;{3V`=nXwLC`^n&+khcqB?MTu0%PJ{(3tN;cqCJ-V*XE3!wRR!mD5?`oHo z`+#FwP=;RFqv|t!L>GJ-F6C`C+>~&J@4IjoxSXgUpS63{7M1un7ZGh-MAxfw--Wlp zQ-Z_cj3DDDEJ?4*?qtnKOsqkjrGA}d%au!qUc5bqHIni0RQvSvjpSo7`aDtC5B=)Wpt*ko|e1~k-$Z>l1QFTlggOk*0W7+sHr364}x1r7oBK{OVZ z-#eH8atKwR=MecRA&qCnPBh1hsVRcT%IM>t_fu^bh=+lz(UbQNIFbc zB>tQaMWuC-GCbU#B&?Q-8iJ}Ce47@Kc@N+Q+X7xs0IJ0}Z;kVx*>ND7e7=hb(C5mx zD=n}rM_D{STEq)gnd?P_>(tX2u3;zX!AkTuOsE6P2j&B34#|-HduCThgPnL9t2~A2 z=rY_|s!4O=rIUAR_;qQ8KSTbT{Ad^te0FR7oZfQZ{7un$hXwIn0}2SBx?G_cfC_&C zjH=dlR-2Qddi$FYHG#4eOi*3CA~-$-2+z*Kd<`P_s3HUdY$Ui$>%Ja;;r-$g@T>PG zh_6W2f&_6!FVyH5iEpqzcu!vVZ$T(;0R7q8(n5`v=x)oxg$KI^Fct(<2yzl|fF>xw z5rb+8@d*4o0V6^nBy15o055{lNoVK#CEE<-c?{P2BG6D)&O>(l%@c>s=>%N>W(%Aj zL|*&u)1vOC%~d3f017P7O3VaQ7vXE3KSxg8)Ba~q$v>}E!B8bTK+ah0Z_C7eC^JD& zMZh|)c_bWecObU`I{@t5s5;KY=tSS9n)eVyWp$XLf`B7MMN)9$;?@}eeS(=E$S6oq z5O{hX#uBp_p<1I+Mj~ztt9iKq!~z$xbp_NmV@ut-r<BdrSRq-?2AqWODN2x)APpp3RwIRXyC|ISfpEk{{V6uNXv zRR|XZRx0{{askx`JU5JaK#(lX2Bs4$NGHXc0?H5RXW- zaSrDL_KvHS*mvD#fnpYUN3nT>2PP3fg%1Imgxo#?!;Qgvs7h}N%IC4}I z(-Ghk_(K8tHGn7=47rh(wBC1rIOsY*!0~SKMy*N2))9oi(h8!XxQJiA@IFhI1N6ab zh!DrAvxX9nTs#sh2`hA;IADE=4SwNTUqwXREV<&~#Kg9YR~+vWMjRnt;7}p=ERu5q zp@|^#xe-5qRGuujRA~Ji2fP_Dw(tRf=`5d?KKyiks5t>axSv0FmfBp0Uke+cYL+bF zinmwu#sNQ9?Kx4?ynPr-^BM}mj-}3dz>BDP4H8#o$@ep^FD)$z1Uc32)TkolT~!qW zNd=+gz}Ob%Fm16TBnLnPAg2IaFck#|IITv;&vxMRKLyWI^=gn1e?NU-=9W!qY#IOf z-jmxA1FVL!>2K^3#Y3+=kcQ}qEE+;H0ywiBA>O>TQsJ{b#cq3#U4Skxa-AV!VU?4~j~*5A>SY_#ixXsSdjb zGs1w40^X`&HN%J65>DhYNA;`rzx3M`!;;>HHgAN6{cf8!-{+94NZ8;J4_gz)hVcXc zMD`vgerwn8fSZgGRQc+zB0NOBGxE9)EmgwgmxMYrc~t`7jz3uO2gBcliV76p3Mh$b z|E=oKO`7W8OITYxJLnvYVsM!pGjwQ(5dC1yW+}@|BDMn*J6w;@3^NTF57la9eH@yD zF&_fh$X*T|Wtuf(8)6P4F}FEJ4?X8Zu70~5)%tC*oh`*3;WSik#Baej8lGW`KfGUl z9WW^DYwepQR}Q(;#p7o~YyGEHy+)ca4EbNZ#-=_zgp1@%7vKJnSTPh7AcKT(VQ1ie zK7XD5|NJw|Q+U98We`cff|Dtw!#y!i&LU1jlCfB}tN}?I~*JPd|;?-BF$S zVf*OW(5EJSl8=3sejirRJS>AM_G|&Wo7rPo)eJ0g?oEsh-%;_zOqeVQ9}32ujWXzD z%#V<+I)~lsOFCxs^gvG86%2KE3iHC-YxHw^*HbgIvCQTjzVe~kdjvJ&iv5`CfpeH* zPu!*JdyvI^_tTI*r8ZzfX_iZn{Y=(F_ zF%u2X2Pt-79L&MP+d$h+=SAKFf3nBDKlF@mL0T~DQU*0MCQR)T{8w5m;0sRKb?{6X&eDiRjp~8U~^De%)zez#A zaG~OXVCVbMtzb}n7j9m%bEIhGR!8Jrn1IBN(DxM)D{u_Q6g$B98$fs3x9b&SU$5q! zguA%Rz6Qi8N$?S25FL6124*ZIMA8zFSs*^UJK^#}&IZr>T7iVal8EVtY-pd)Dn16J z7edX4fPux6%xKKmrEjrZ$YFnyb}f)H69Dcj7*4X-IKrla$LIohq_cT9F8s~nO-@ce zIc8eeVqCfhWr<)8M3q&56QZNRCF-V+|YlaA84ao_)+>r!21|+5cxu7Q- zcd9TdloPSs!9o1Y;M1vp(dUlbwZN?*9Qy)%Fa(jkH-QilAXh>t-iY1R2+rmu&1Q_O z6sUOe3-;cCJS6MkLlST@sn&n^nqLL#-8pQ5!6|5r0ZFqFOWrsb1ONmgq*b81;k386 zUmJsAC*W@w8nix{2}~Qr?11zi5CsWKIn+^mm{kP#Exdl}>A~#cTSu>cbh&bc5#j|X zTYR+n;Uq>PbkJB}#B?AH2%|qhy8&+_XpM&BF=s$I$G3=3suwn9D<%-euH2z6`pWBD zlasWbkcIsmijMWM2z!j2bA&ENlK*>?qiW6T>>#c}(0uW&QKldW<)BVLC@#p~v!2Br z=7g;RH6o%jcz?8eG)ZlkI9t(t52!NWr7H*#f7%4^=qPnK0XEu(? z6$<&q0a2}oWQqU>0XdO^ zGvO}SO~dezCD_1Vw*!-{N_@v!8GyYb8EIGhjC%@VS#x_g zZ>z%hfqDlv{&BDZ)Hh~K* z>=wdkFLXKYAh{-Ndf3YVRZBI*0_BCQ0|-YNh6RiAq1i7HNE;la%0Ocv=V&^()@O`P zq(dJPjLEJ{Qb+bzRb=262vdM+poOdS>}TP=u+fqJ+gI<5@vZD^!ADM7Nw3bE4xgM* z=YSf^?E#1AQCW`O&nZ4Zb5uGP?Fi|5D zop-h?7IxQ~cXbykc2;)Tc7N<{US0dTyHm5a^;&U(Hm1QC&Iw%pQ}Qfg+HPEA$@3nK zha3nBh*98{jI{t?*EFO41|g>elnfl?qxOpj@pG zo;YMlbCYAFgthhV$a}k6dAmoEnFqXxJZu3jd8rl{JR8axxlN8tLf*~couTH0Pb> zG}ZPL?(YT>5$G!rJprai&tGj*vZ!y`2hiasZ^OJJ& zr07eUv@M?ziT{+91%)wySeuqs7HD`O<@%U92b6gbP~9G6WoPSG*_jmI!pOs}&v0!4 zc3yTYSd7ujwo`=Zd7}jO92zNSqisMDefZD%A+N=7k`e%>T=ck;+pVnzi$=$~UV#L9 zG#Dh7i1ZHGLDc3Yg`E=d?c_DxZSkGtwfkV44NYy``Z7dbMvqO+l=4SJ=wtq%-CRPmZpu2TsX{kx4oBH8@s`G&XIH$D_A(YJ*Yep*@XJ z!MHl+)p>&o^2wSZ{my_v3TOtmH#%7bJ~=TYniA{!Z*ta#v- zA?`DrIQ;4E48YGI;({6Pt*eV!d#h`NxlkV;X(UC|Fr633dYB!zjPdaFDCl)Bq|C>T z47taySAmc=w?%xrM|>;+=0pW;&ed3yW=Zt zt+TV4UG_>y(`EZd!2izH4-5{zh1>FPoNDQ#X3)|HHRLXQQT}4I*GxB#LM^1s6wgnc-cM0QF*N7>cg ze&uNJRiWjjhn^fP^0OjV=t6yC!0b^|eT2sNG))fPC*-#$N9GFc%3FDH=7{F8Lwpwo zk0DbC{!Vr%b4Vs*`1u?`AwNqRU{Fe(KgH@S3HpdT4y^lxAVv5Z_jo73Os z0%{Lixv$L!Hs3)chq_y1oJa&EReK@@v9P_cKoF)jw_z5#fYwvIv9%SXw|U>uT-F zJ;0T!KI>gJ#o zDF%V0h3piGF>@r`B`%Hr-pqQ|XC5HJTr`@sSDu8wbaCd59X?G7mF6Mr+7X3z@r@Ac z)?UKytmf_T&RyA)spu4Y=_34$&*Wz**kgK~C`P>L^SFqbJ7@V7@~unrd);xN*V$H# z`O+;}l0KLW6yHkqEF78p174`wjn0_P@{eL2=y#I2b^H9On%>s@k+@VHtc+j7VUHIT z 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 literal 0 HcmV?d00001 diff --git a/docs/docs/assets/sceenshots_ws-tester/preset.png b/docs/docs/assets/sceenshots_ws-tester/preset.png new file mode 100644 index 0000000000000000000000000000000000000000..31d576b695e0645d80c827e626cb4f87105cc45d GIT binary patch literal 232313 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%0A`>oGS;-Hd?S#=55sR66HEyyt_ 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 k^#}jY0U!Tg|1Or#V-JvP+|pkqDuI`)6WpPx# literal 0 HcmV?d00001 diff --git a/docs/docs/assets/sceenshots_ws-tester/send.png b/docs/docs/assets/sceenshots_ws-tester/send.png new file mode 100644 index 0000000000000000000000000000000000000000..19518b9259097467a046aec9eedb64e1a1424466 GIT binary patch literal 360006 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 @yp$X$!Sz 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

      Rg0m0 z19Se%y^H7eS+lf0Ouk^#X>VIQ@#UR_MZWfWO5<>&Y(xYPn&#wp@E)7rHVI+v43wgP z$gI7k(T0QLIv)%=Hzez#AZQ>^KK%R-MTzRXyAI#@T;#t;MJ9Gp2U4cZW?G5Fa!q-V z8Rp!U3$!cCrBS(OQ%WJfKlegXj+H%vy$}pCk;aFia^`S}9M&XF4<{%jj&CSDAM<9OX(*}+rdBuTVgd&DvUqff z*}RsSc4J8rMGz_=1k-^EAVzh%#GoW8lYbfQk%VF{iCIQp`|nfPV2ILm_;a4v?Fjfsnrm$z_r!^8VG(yMS$>$}tfTiPhFd(c81L6<_?su~EgL*0tROZdZQ{&L4jUi}|FU z%Sc!Scok<6mXO%U<%Xlf;mc~cIrkbM=q!!ENGp+^^N{AT-~|9}G#v)@J~>hvHr%F> z?;gxsjeNnS`Q^2vZ?vqyG`$5+&n~GtBgd%>~cixj1$cZ-{r7UnL|I*yepj6 z3M%f{3?h!Jh+{mVXF=+FwZx@aFFBbT~oicYZuG$zRe!9$2}Gdi4}Q^K+**|;YDCG0XJ`oPQH9*KSBr{^;#@I zR34XZ$2&Ij+M6yOY*EQk$~JQ#iOkUGD1c`n6Z0>`*S{}K9Due1a{C-RJuU#~<1~bW zRt~qehATICbZ*XHd#@YY>@}D@ia-1bl%KkU5&-Ef(vR~U2R)L?9w52 zCq(Js5&13pFXaQIfT>0_<9KrI&D zTh#ZYi7$d%MwUhPCLif@9|;Z66gxUnbd_lJJG#5yDCqkHt7O>l>EvIR5RYO-bp)vT z=cfDaZD|(afkzBl&`ZYP)YWD=wFATqoqWn*wjhINn6Nj6Y66DIzfSUc9AEM5^Y$29 zG2g6kQ8*@lSDQvJqu~zF4i`2=pjDZU5{>%N z$hP~17J5zqzvp7pzPab&!R+}Yth`|aVnxAoEJ#pmX*sJwBVRqngT^XdF;ug-=!o3`5O(eZjTlE61=>;G63lxp*?NQe;xwV@Y|iN-bP0flo`O&(h;DN-y5Z)pSwv9}&t2)~eG@B)n$#*}rxw z8y0qUD%%p&m-3aPr0Z%huf1nOc}X6`!{JckN{IPB%GPvTSQ_Xn2t+ zvWkGO$yhq6>UFbDuj~6-v2c{^o8b|0O?cEv=jsmS*2EcH1QNaap0g^QxL;>CXD+c3 zuw1um^Y0JutesNp7Vnc7lGyejCT1(gFAnKY#~m_U`WC+?Tv9h)GvLQaX-vM}@^iMf zZan3YZu>G!N60c077GZi7VMynz$fvMwS_JI+mbVqV>{C47mwJTL*9KdAo``wBPvjd z5MntJea&m^Ck+1ZmXFQexaahg1a%F2_31E=uChu~{6TUXSq}avO0__~-{Tv}NP4ah zgwy}bPPRAQ38Mw~_v?4l-)ZIE;UgJ!oIYo;n|pg_i}yfO)cyCvosyYX;g?+1GE;an z7p@{l2YAgxx?Hu~It^msf<&FA%@%{zI3R6R5D(-5c_g8W?o|-v4@Cw zN6(jD`MuS5@x^AYXYvXi2b4Kuc$2{I%D*^z%$sEfL^xam0;Qy6>tLM+j|UKUq+S_a z{9YcpQUg!WKI?My?F6o0pUxLpIzD`(RfSwXz6f2{(JBGD7$hYe&Q3p9LnMlEcCzs~ zvGiwPJs@O!*T=`F1>$T+KZVAri-ZE*uM`g2!*z`#8i9mITgzjkUR~USi*87&6r=-$ zt$2lCJK(QU$Hj^5&)N5i%-YIGK6Lj=G8AQ*YN7r%ur%D=-Rt<}O3p)i;kL_UCT=48 zbKh+|cSbBTl7;EWcptvK!{+F#@Z!{VMOVEN1u?=B0@C9_?G$a%F{p!11iK0(Lfjc5 z&1AK3f}|(RwA&UwRly_9z|5?GcOzpK>sT1d2emb!Tn}^%;FXK))nUbk#5RH-K54)p z+0AlX+%c!#oeA0a*6k!o@xe>wUeRhEz042(JH+~VD;257N$Z(TO;YznK4*GfH(oqG z-Iuzen-J>hXDljN=1a?=s_K^JTEwI#c--nRZ~_C9N#@e4oSOaqxs&1PAsB|lTU$FZ zhv|F!#q(@zY|@h%@14fpT?!9AL-cZ{QgppOb^9)3?i)wjo>jAM+C;C+E6ST{nK0#e z1Bx9y-Z4}4%-$+$X?*m^dnuRdlG~utLVgUf%|T0B(G$`{f>)Pk+LPgGe*QEK_>7f! zv}QN^IL^qxtR(EgqscBJ%q*A;*-HxX(;`78^x5V2UHb729*5|Zf@Df!HiO81M&EK4dZsCs=_5%7tf)U?(Euu2CF#ZoAyy=9NEj``>xR_@5{-uh zUY~->oSS7UvJ9YoGSsj1(Ln`MY|miysA|Q zNWXT!a@+0vbuqzL78paX`FAOfO=oIiCrz~7tTaETl!KsUK7plGHb%Z-)ZG5Xf z!GHTjL%ZO)!tL+(b)PI)A$Js@XX=To+%EN9Z;1!_Creh>mr{ZlHMaG zAXMw*uPXZQzwnuhB|px_vOjqmqG@MXAc#U37`UFZq7$9#>w-srd5@Zqka3`;04H$@ zNR8M1H}3nl4!pZvwa`%CJaN=4>HJIBaxH`0_a)+3{d|x7PlEqW0!^ftU4`0Q`$SN- z5)jJrhu9OkV(y<~9{v%#l4WjP7wFZ%kjOHy`g;_ZT4HF^34 zATEBbbv)lDP9Ut(&nIdCFp1)-zhACV5K_*4x)jlyc2kh+&V!@}JQ?>BP2U@xt=#zbUz z^=^Z1t+ViR5}>j=@ILo_Ub{+ro?k%pc7e_`620cX^~C>$9RY0ICnn)jKml0|Zl`}A z4wA%%B-?^K;f>jq{9q)gvvPMK-xD<8Qu#wukQGOUWHljNlz8*&B77?O1XJjm%Hve| z^I7Gnh#}@cQ;--Pz}jn17I5pL`cTVuQUfQKd@-Nfn}Ttd_2Jl<|9!0zWJSoXqVAq1 z*492#>+v;-t4q#CBe{YmM6|pyrShP7eD>%s7Pq$deJ6J*|Bb4vS5g;C}k)r7bPJx3!0M!eQay@WzHoaI1K$A=~Q3$y)!BX=RPyOJ!Aw zYUuXdoye?sMPyw?D90TIc?zMTX9Ctc3(5KW=jW->bfy8UKne*qX-VYoeDV(S<tBBF z(F@5*>hx`5q#)WJ9hca>@84s*zmtxHL&7c)@Stel-;ZjSvEd$s`&5##jc(ro1UhG(tebZjUh_$!Fq6|uwu5~mFUM|VvbA8i;Tbr<22LHcFi z_SnRM;UWHL*LZ(y!Z!s7^7TGD{*J%8`jFIiuz!_U4b}MQcQ9WI8JS4Zon#T@fayV~ zK!Tw=ga6+{$!VCPz(%y!qjP@0J>lp%+}yAC#~=7b%Tm8UbYoS&l58s12W}&WExhO2 z1??g%fKB!l|4gS2=b{foglxWj^jxHq*y26W2;iHuDk(*#{MODN@568-UDP0*q~^sIwM6IdFV@@%}#DJ z$G(pdP_y2+0Dld7yMJX24md|hAt-j4RXe0PuYp2w z`^v9O|05?lUPGCmJ3iBk_xW|_r^||}p4zwnOEgR{fHaa!j^E=A2`fG;HTz?d2OQ+$ zyC}(;`Pr1sfV;w5t(Mf)%q%+{inoA*>M^}&+*f{@d2--3@65#E(gX$Z@vf48A>{Js z4O>7Q7dh(*$TP??`|XRZaH)uONliGB&5}79ee(r>9v-2dQF<^q>v@% z+#k2AhQ)B4<)@f9y)MToP0HVY1wi+IuW3Dv%OVHve+DE z_P?J@kPzis8{B54PT@~mfz?tuK$s@5{(> zyr1P6eZ0ysy`xgZ`=un}&7ef0jdbE__dq_pYsz4gtac@NO$@!$Ql@=HxE(=G4(TW+ zU)@#z0r8kfd;SxWUqq5u##6rO?jq$6k#|e1XOP3~=@S4Zu#X5g%I#o<3U41=BLGYT z4zmHAK(+f32{{{2yxIqyt5gfLo81g;3BURkS!P_)_ZX2j_n&vU{{h|~Q4fM;H;Twi zPXYJoQ`8$1dXOz}b%Ea=jXcm(UJioI$1RtRayyY!GQY_I+acH?>&DB|?|vev1$?$_ z4V!^^1EvnBl=v845g8S3$B{V z-n=Fzu{YoM@Pe?rWNo9$)lukKE*I8E5sJMYXOYB8ilaT!&<#Gn{hSS-CvW8tts zl4ucyXvUp~PzKk}kvRfu4di84?d#{yZ$|;)Vi^F{FeKg&C~eop#d|(~KDI~R+LS(h z3mHKaGGoq;*142tTg~OE(8xn?0JbQ>D|`Qw2wDaV13~Ssx`*k8Kpi+E*m&du$M0 zt79D>)tvq}Uv2P5d#w}6)TsuIFE-d7dwl3z_tw*4mzs$aizb*JZd!2L^yPfJi0;ql z#VuY;oLm1bh2OYm>&(bm?u-+J_44-*{Cs>;H1hCtT1Q?8!~ThSQu`cQ9{^W{80=wH_O4fygGQr5}O8RyS=Qd&%fP554JRbBq5Tn>P3zP?W%{M725lek}cXNY` zb8s1B_BkVHEQbHqtvBBocEJ#{w_(4l;iLVS2(;6$&z_cvP1G*JKk^nc zFxlW_I)yM9;V`CU736d>XmR%YSyz?@j5R8s|R9o+xO)QbrN zNor{5nQukL8aX&v=^E`0B>ZiF(_<+)Ze?ZaeI2FoS zvNdU>Q)tkMkfmXq8tD63%>zQV5aokb&<#67#PlEXqd(*TgS5@gu|DiuRs^p4BS6m# zHjF2JNEYpjcyBJv#3(*NB*J{6qFqRoF~n>>;yL&!iF=Q)n)+J=M(vqM@(kG3ApuHA zJUs;6;!ELN@dH+P2?&UjdMxz!G*yT(_^rgWw6-lnB;W+;njt&D{NIBH9BO2|144on zoTJCABlWKydZxWdLV1F@tElMhGX2()5BKh^97O$h`hj1hUCAOh{@Wkjm%csdVc~Y| z2dZ4l<=+Zs|Ayq8XnJN{dw;w?7N7oj_lFz`drYpwneiBJf+`l}r59A^z&ZW}V--yq z99?&RrIc7%?Ihj}%%tY3(@s%GZF&m8%+f7s)a_6wFJGq|T1MO;yMH~5?~b&5)Xa|0YhAmn%JZCWH$qu zz%D7iW(0uEz=#3XZlhJJ>h3G!YE@bpxcNV@RdAl0ls0m1W1z3@4L*0V5;UZUx(#kN zi&A;8vx_ymVfS4QT#Auka!|e^AOX^$->+NH?g*2j@USJgljCrAM_u~F881k71oQ_1 zT#;+wf8bX8Cuk8B@KP=L2I4;QKYl&`l8U;%aSyw{fZk`RVxkw!+dUz)sT%^e=l;h! z21q2&P*d47|e64^>VQmSM|iLcvNxlyRi1@U}1))q*1? zW^BJq$^r2b4XsY%!7on1T1b|rRqpFqcwFH$#bQt{r)1veoz;9}5E>S!NzC5e+v^NT zQGc-K>q3Chl!}PekybwC$+)KvyrZAmMP9U#3eQ1-=sfq8?Z&A4#UQPigt~omQgSkr zC$?x!v*v<}w_G1aC=~P#4$^=^KgZx|@oKN=&ac-KZ*?(WC-Ch*f12O7AYZ zywcLzBj1(jjiQ>6`)ONe)rEJ=m`BROeR|DSvGVW30%mJq6OTbrD1-z^$)P9_^KvL& zMQcm>_03&Io6fM(Z7`6dkjG*|igs|#Z{Cca>lQ8)e-X|7fA=s$ z@)VF}jyB&_g}302e*4ZJ9PE2806VvX$V%Y!Zg%e!?4Ywro4P#3Co-$ww5rU2=~`sP}MRGUAwpHd-QQ8y;oWi z>7iC?=`NL>$!wweolx^KZ1$WC8Fi}Cw~0M}9yo#qF^QMtgv)JgZX_dSQ2=@Z=%OS< z`4aKXrX%Hk>+qHw0MH5o^1IOSI%j+8#y>dvd<+1kNN!EqIEB(Eb3M48B~Y0xEG?}B zFF@xF9o5L+nFz9oSl(a7T;ZL$S73(?ruNg?MLx7A4i2Dif%_RsoCOHzF<}82vlPV7 zXs_u&S=K8yJqdqOR$B;a98R!Tc-9sI(WFX89y&Wy0hx4072NS|&9}r&C#l4@Udc(3 zsWM^1?>=uOkm=T&DP>@4_7|6cC zr{{vUb|PcC6z-h5#EcoJN4E~{VO~>**BYf$#M!fD`C`~*K9@7;6bYwY3O{d>bUDk^ z&PR=5^Mm=U;ghp+EeUDt(#}TVSQH>s0HZ({8p6=HQ6^3CFKiZ2c|VP$Y^U*SfbGw8 zT^Bqt_7x-|phTw)(l2NOTZm3LZZJRBfHuV^DsY=8K{no;ERv;Z%P08MsTlcC_(xs7 zSD)t>yA_exm2pADy`rlcXQWhokKK|Ulb57FM@B~a(X8JTEv;I57D$1{6gd@V9nEnp9EYGg3#pg(@VTGXW?jte;vw+q-_IJFonsS2+51{ z&RgZ)l^gNrKooJl6w1m6^%oYU&$6#2&H?cDKd;pfoOL3;d01R}J$V`{&qVU3Sy9i? z(VSQ*zE{KUK{^wP`fe)cn2q9jHD<}At*VxOiKG7d^>MW=>g)aoF3(NZjQ#gNQOKc) z$mL%Ha|KW|6w%A9e<+&AwdQtO36kb^B72wT)8apr@f*#*pb@*}?sYJ+CMMP=t8VQh zM(d>`tVzQZ|AQv@2jvL@JY~_GNgT(v(oRcLoF1Arlj%u4>I{ih<-|u1a3!a`WejF3 z;gYmx+TA_RvdQ>)q+0mg*VCG|rQa53Av`4i&rdvMpEk7Lz*xnoShs$&y>TXhwd{FZ z9D6AsV}u)U(74)yJ({YN!R_2Tm`P7&?)?7&)3V=_((SEg4v!a(*;C6?j@h5Tva|fj zz<`67R2Hs!)U0XJN_T@mO`@e(N$& zM}Y=>2$Kh6skyUrENCl08i*o7E5kYT@ChT#sbN??A)(fe4kGySDyA11sQ3f~PDoW5 zsP~-{&#wX7fefn)Fhy;gs?{O;+Nw<4k;gnZ)$3%j=oSw%AxwM+)le zf=jY78uVwnb&?+kD>n+>i4&2RrjVl9%4e2w-CntmM$<^q+}r0%yGbf*VPOGl5`ZVM zm?pQe!8C^-9Wns+TaD$vZfANi?G2+;1wkGsh?3DS(7CD=fBO=wpkC7gH5ELJlP-gT z+P=c-375_X-Dl={a?%iy*W@;xq-{b!U|(^a#8c3a)AHv>@zcsN|IFtYnCKhJx$kL5 zagifWEMdHeAb!m&5io%O}r z-d(e6FP18r&%jq|ciP7L9}p3itD-@b8vs`%spnLyX&Q>R{@2Qx+c7jG8vQAY!L#$l zm;QI2<(MA%2O;71tN}t2OUGQ?*PiqA-SoRCms6El`n-iaO-`+sKb4=~&$Foo+qLBjS_zugPdD0%q0p`R zN;0*MU5rc%RcBymSLb~=zL%awzR~+644dV9Eh+BIhMXA|SCk&0`8j+lDHe;NB9=Ct zpv!IwpM?@E&y&DWG;8IaG(5%w)I4Ij${&x)>}pwi$6*_!9x<;nNXhtl z`Z%!s=tyAT3~c#EqbYUYO-@c%CSeI^qaVK)e>?`_+EDwOh4(LY ztzI7m=Idw92(SbQ`g!yG<>$U=Wteychu-4mkAcQFRo%A#`<`81b*u$?gFhf|=qgZ! z#Q5Y062)I%GI#jvxp%E;-E z?o(h{52FH&+E`a-=Tg(tR9$5v7CmFurS;;0FX%gNT?XnxI9J&2OG&*oujsgV^845g zEPIac8&*GKH5`nq+94Gat5XvZh4aKFTzp<>3E#RFLRG~dSpB2pDZk9UksqkZWmMhx zh%N~&*tq0nO-n{8=f3k1Z2~jrCI0r|W-~9g(5vGg(YM~ir*R1f`b5oPp z88kYxK>)cMyre6Q-dfwg+|yl+o%-R-zaR$u#o^FjA!;srnV2vz38xR<_Vc+e>VFtv z8K1aII%NG?zd)gMq{Yn;l4L<*;}zvqmlg((Y8Fx4tY`P2<@<_7SRuib2c*(lWWYN8 zN&;pIR!N|fgi@1jmEAs@HrGGzI`G-Qf)L5Qna!D7X8o#%A8wvkYvvFkgldN55m^}3 zP(-B7N-gi&`P;os12AD=zJ&>}dr)bAzP%U@6zGt-@`FqsZ$Asi#E|KtWLcy`@EI7D z-9#uy0s;asO25-W#8pd|vrV9KzGx%Y%pcMsw80mz(^S0ss?uJEo{gkf-i1bbqmnq!C$*E-AVm05jnP&jjc` zjKb=ET0VyX@Mc~@`~o1_K_ut@R^#iUkVJPr(rMBwvX<+EPlC+S)$3j}y>n)#I>J<--z$)2yM&cOU0Ko9H=!fMXwFr<_a)>k? zSfy5U^U-B)UW+<3L8iqWj^=Or8#R3((fEs@c7%H&1y^O}5>8!enBFbv9FL}Z=L7k02TIqCvl@!XmBW_K+7w*aP+*D`c^FJd zVBg%>xc*fVwwQO4xQFIl|A>)KY~e1b)^%g97mwHivb`t-n=$Pn!4vBDr#@t180;7> zkF?c23Vjn2Do(jH`BNIUyh+)b;FeAoj_Rx$<%_tLd|fzw@%tk^4U^c;CxS?VR_AS_ zCa{zN`sMtlU_tM7?)3k(JMi=BB05~3IcVp2-p*20+vwA6m(?|MGqJntySTfwsb0^- ze-Y2aoE_D>FSQdhw~;&U`pE zudYEf=|P4NvPI;@$#(OxVF)rBe`?m7y`Hs&L9C7|X?(Q;H-BekFt=hc_vt;J1l;eW z@}V{-#wOttPrUR*#EU~)B+el?(i}8Wxg)u^!a0j|e;Yp9lz5;)aiU|!RZOzCEy-^1 zec1&ox&*XxN2t6ZY)(-`NvSu6EK)~CMw&OZCp|klXUpwG$LgzOzK*(RMDO4m*7o!? zYK1S<^^H>x9NW)we;%?zh91SBKCd0Di*7dETDzLwmt$(UB7h2z;z_vO zaD)lNyf!_qO4GW&HE3NncUkDY*Q|3hAFAn==m$>-Re{kvx{;Y$YkWox)@RtZz}#q4F+$ zqQ^2QZn9I#J-c&^*Rj~}Md9pJ(D4`yYbSp!m!!Mud1jkqCR+EER2Hspv?PaM#?rx= z0c7XyTq2u+AD{g_WAL(fgeQIo{4u5cN>n61L7*y1c}isjk5cZhz)4Y_>z4_*s; z4T$Rz#(x~CXn^dZOOLd{c{Z7DMy}U{$jgIEjxQIexeuFX3zf` zd_fsYS!Fs!!b0rim{pVo{2w!M~^j&bVRq1QaXr3o=y41k~a%xofP-S|Zl?f(ICQsY*p9D59ihZ9|WzR~9{=6wCniV^4-R|2Oy5~_kmW0DkOVA!KQ%FS(NEkTW zxFvX9ofsr!(lnO|FW37qEvw@D7}fEuVmr93=5HJ*-O;XwiWF0}XL)n0{+g}jpeV6# z4Dv%5i_OJ^ld$Y^tl?A}){$7vhZfdJAjYU5yz775lE|4yXPMH$Z$fijT%dWqo79hH z;QsGSn;O>-JZTpxPO-A_3k$bq6gvs(J^JxSMuxY%?Apw2il7%hy9K)M|9!rpW3Gs$9ukN zx}DUcPJAQ$GzNdiHB`SecxdS<&5;BrXO8eKp~gYgR4>A>$)BXc;u@JF3RF@piYSA# z!DpZKWjsIaXmA>YTAv=Msz!2LW#6!foL>+W#z|>ia?X=RD`1jT(F%dYsFJ6PqOK`q zu~*r;*sHTCf+WJYrm75yd|+CG&h7M*<25wFebm{HO?wV!{hPcNB?j}~L7;`M2s?Ft- zz*oObtVy4q5V);B;Lh|ZMH=(edam+WG&WaG#^>B{mo9r4pNlY-4-LpdNyM>FOLRdH zkwc?Nd-C`lv&1O7jH_p3F@Vh#PQSf+wgk;~o64>x2!rZ~4NMBj(PY=}>AAxp%)@jJ zzj1}`QE|^2ldECt8spiM0>VXoEN1bHuLAjC2l$^?+KKK+N2a<~k!GA#bada*WkM;_ zb*H~?2!Wked&-Zz+NtOd-2SRI9QAuTlf-1&`SW^=X5KuCiBA^gK9p=BKOuLWOu@Up z=kGg`Gno|EDSx-1&8}q;5=UJ(buP6mQpJ9!gTf+B0fR&PpG-0C-(VN6@%!`o+OrTArV6> zOS3k)F`5yN7RvG@gtrQ6evXzQmd>&lx{}noJ(fwK4B`oDLye}TueL&0F2|#X^k3Jj z7*Jy^y|EoHNSy5Q)jGRbZg6u` zv!}!hOsIz<%A-Ev&^@(9;r&bAjC?BfilD;WkO1P2K(-K~O24OArS5MwkE>#o6Ad+Wc;lyS{~( zg0B4__kV+!0L=b&=~cG5WR;{K#rQs*A>G2vj2K#c(c%%Wb5va=DcRFOt5WfN#XRA^ zbYCeb+c6vB6B85Rzz3U>D*wF`KAWZKGe`SN6MTY#fr$d3g3~3|Xz_nW(N|1K*)j8% z*iWv;4o*BQ7uM>mVbOD|3xgMhfb~Gnc)|Rwpd4y=jLdXyHPXT7IbmhE*`7Qt37)Y&dxQxDO9fyF;8DbB&cT~N@Blg`IJDn8 zOIz)iGV-g+e)qxudp<6qg~-oLRTgn|fvM7DYgvN1-erC7@3rrwzI&20HfESCf8zAE zJ3M00AYVnf&9X>d;+Vby(!~-CwE~;pf;5#f8KSMhd|8d_|1k9>;8?F)`wfapWlEAM36)TkAwx2RqRc~v%qfy7 zW0ak_j74OKLKH&gP=rcI5@nvkmW*ZQe?QLueBZaPYoC2i+u`-T&+l1l-RmAoWtn^x zlkadIW|-O}EPFQBF@OH%PmC(J&a*rl)!clt$4GaI}MuoR-}z?=nGvKHeN!Q1P`Mj&dt0T`^t=s(b|P1e0T zdLl4cv35i~QybV#v#`eA@`QCQ^d{yq=HQPTwoXR;P}0!QD9UR$u5k0*+07E0WF(%YiqIWV{I&H+U0)+MA@iTCq^iIqoUhznehPSIvoH?tgVVk$ zkgWWgp(v z`)9&U#ptEM)%m%lz(#I&nfJy8%L{JqC7qG?o=S<+nlK!=ac6_cu*SjNfdOgNZaa>i zE{UK1t2Cvl_=3TsI$TRj(r4wWM1KmDm3!Yx2m*+am8?Ko!FR2T(>WZ*jzHd z{7{g3zQamto7y{UznflliEpRa*D&j_akmo9(R(Z=G_Y)lP2Kmw-cMWL`qypA8|QAH z2cytx0QAl^`NqY9yS+&!6umiRvEh^EGYYjX;;htFuFAE~a9QUt1*evIF$+BU#36TP zxOLJ|n(D%?U21g2VCBy*JkUYhCbkr^3^ET66xey+s^c%c%eqtQ-G0#hu=9EM>5=9k z3pu^Y+;+m^7_5~pj~WB%qBX0NMfCbDpbGYfqx#)I&T=J(M<}LehJZ-GzYd+ZDdX9zy9w#PDa%%T2 zMOm%yH<)n6sLC+4Rs~W@yo-5VHaL{nH7H={HAlhW+6ZnsYhr2t`n`~_{_*paJTt1W z>yszn9mSkCPe;W=`d?n2yu}Mcl*z2v5EjlQ#j;fs)-b3@gs5 zi7fx`@Ds`|9p*gOZ0>0qhkIR6RFvusepqBA{X?;hvLf%Txi5~FKgzr~ZaHmu5zTZ6 z@795B+@Vq>j}n&#+=*Gv)@r)`etIZeq69Ab9SR2G_vE`N%&Pf}Uf=20&a4!s)|9Ui zb~<5O9$8vwV=^SW(|&ci{kzMhy9i2-1{3$|qwNa2@4f`4kCy?4?}NiS>YsLvIGqGM zhN;1oWVpQ04LdxrZLa)$DNYX)F2BUDPlF=F6-%m($XsuIhoI-N5l|x9uVQ zSZlN{^)N39_B@3@-|Wp0=@nNe$*lePqrLuQFLyXf02akZzexL}KYAY|2M9X44$B5y zPJ|ZxdfAvAZG;TXPJ6@A zWQ|8MuL>8;MtJw|@&s`%wMYNtr#rveJX=X`e)vzS(#5`_z^i|*D?_Bop}kHf?+47)|k=YgH~2rUj$a;T!TwFsWdbcG@HWRpEMA12n_7ppjZKQv)cGN`XooPXYcuEXY!##)t=#? zQ5&r;v`!PwTE%jE#USC>TE#mvmSlagIWdpu-g~Lmo=g4gLy0RRiNpw%WaGBwI-fn% zbEDJ{0`<{vnQMX-FcIF&F3-5Wk$4NzUVpKQUD(;ecDSZvH$?b7_&TyHEuSq5-Hr7j z02wTBL2e;(n!%rs?0%Ln{_c;xn$=5`ktL}Z{@3zIa6^9}?l*7$p(Gp%q*JEZx)4h> zB3&bX`mlZMxH;R|0pm5r2)Qz&p_;vR=J*%qR8-V5wNVfPl415u{D{$B9n2Id1_kCf ze%8#qz_n%h1tVrfS;*Axee1TV^r5ZsH*CUp?=%XyC@>8s*}~c77Y4-3s~_UpgL@iK8zh>f;-62++;-BZn)87<=LP?C^i z{4wQNQ1VOnKUetbOu+|hGu$gZ0e%mmGq_D#GVMhZ|7g{o{Op@Y$a8&IC+pmiIb1|2 zT5=Iwnpy~C#g|S*GP{9&eI8a1uSZnu`n*>fS3e+3q7GCb-EvP*)*N>r^$-qJ$W*Jp zY6MFC#Ac((ARVwWIVof=y|fF%Yk(oR7!D}JPQ5E$>rPzjP{h0MvyH#!h@IOu^3##> zaK1YFt%EeK!E{aDL1k=?(8Zy{JRXOY?OViE)ZoUvE*q_1%2;8ydP&^jWpq7PorBGm zVIt41yOskAh}X(=VxDPr(2U0-h_|pwdHu^m;ECp;rta>>FHI={0pJ{*HJYL$X3y|S z6>}Pzng<;rcp=61 zL|1-Hw$c3Y)9Y0w2F*!V3UUclmpNK;+(!HEe)TM?t>x?FB*pu>wNC$;LN|m#~B+z-^;|N zCJGv5-`z&Vr>du4 zpRC@U=F{A9E{!}MJ9GK=*Ew9_8Fn)|#J`3ZoTi}w$n`*ZlVK}Z%QLgJrgiCQ2|+_{ zST^Hd5Y7**EeF6ku|QQIdL7scQ|Lv-#KoCIRY-!e`gJmYBF5w$UIaJy7xyp{H!?pp zh)xYQqt1qqkouX>a63PC{~H{X3{x^VdMP}4OLR*R`C;hgc(Vl7?jY zU>E|J7o0fD#ac8syyEHIjfhoI}-Wy*eUg40Ck@pi<3l7}K4l*qgbEqT77t~N_t6_h|Ti8-xVs~zA>6(cxugo)8C_#C7c9iE^nu;{%}YU4fZrLP*iX&dA9Z*`3WuN@l(&vz|KHF(X( zkZB$iojwmXN}d`B9y&b6%ZN8We9=*5P1GIXWAm5&M2*p0<_WJZZu-1;Tc$^XNDGx0`mOVFfe2 z9T%I&_|34y)uo|0B#^u?u1Juc`$zf`vP*2b%UQc;=YO=8`y~juwzqfHU^nL4{HW_) zhSqbEo&kqrk|Tmre)Qzt_7}J;SZaF*1-!-VycgTaRiq2q2hp6bBV~=GkGMb|B)0e~ zZ_Pt)Z}*(STCQ6R5Yw|J^{}XmYHDq*C5kMhAy_52ag#!DWF1OG zCT~!s{o&Ala@StM8h)u;t{Lm?RwRbpS!HoQ;qb%?RGd6OM za)z-h;qqC;yNL{)1Bs2zV>h5phHZ`I=y4Q|jEvOU#|gv9@3+RLBKI4e%pH@54MU4w zvCb`Zf(|VHhy|hI=&U)NbPFmD2Qj!4%r1X@BoSSw#>dzk8*y5_>TnPd5~wl2$q`hN zZ;oh6L5u$_05F2lw7vPjwy39zyorr?kYjyW{r16@?n?fIgg|Bsmfi(s`;_R)$IaU9 zEj8|c6^^vnqoq~6G*yR4uC=m<1^DT#H>@rlZwU$ZS8kmv1krVa(Z&s#gpeE1LOBLl@@!{Jxs-;}pl?AVsvCE$ly}jt=C+(Yms!B5x zip+Yh+tJ;^w6rCu`_IC-gj-KFXVXVwUicpkEZ%S-#E1kN&2SPLHI|h-B_`JSnufR3 zW}84bj@e~=d1ZWh(a!rM2WR8|&7{h2I}S3=;BF(=ea7=2bwpQNhNcO5H?L9Z5qSZ) zVnAB{a64zC!Hw@0xH!93S7z4&4@vWH;L_=g8*Hf-%Z}ZFcAD8~?4M(T_(~YBNIF{48)APcz70E1$Rg_Ck-I{Y-=nmtGW37q+5l%_j2^DR}V^AAdw^**sg0 zSpupRm>xs3fPTf?($eokx)OiIk^{&__1p@l9~6rVA!0bpXq=I>SU^|;4J6rJ#D#i6yLkFQ_6F(MbvR|rujTB zx8HP%BfC4WC)>6UTFoTHTENSf^F!)Tq$3F5Ww?(R5y&cvD%Tr{3QX>tJBg$ z*(?8MuRTUYd=SX1ZVphbdG6 zFhVS3TB1jWxNoZUuaiZg1h4!v+M1Rx4g=Woaol|n%l8l^JIS(ZD|Hcw;7YRAkG0Ou z_;)&W^}i!H>W0opb+yNs;!NL)2@@ORs&}&JV)*a6p?@hGZ~XinYGrnk?o2{#xcyz% za{tj{S1$?W;^q+!?!WvPz<_T)eG<046N$X|UBO4cgoYk}C@3t7%$gA@Lr}0Bxjkmb z`!nrs9?tgY9GzR4t!V6Cs_$MCxL_>9-j6UNx!%qNP^mQcm8d723A=lUdy5UO-(bjs z&;y)Szb`@M^Yu!0=e6nNKLULlk+y>qAmG`%Ee=ij>wea8Md_<$M(4NPyc%1xvT?Dw z6^+xqVSO7)3q3{{p8#10qnM$;=?{Y35@uifW0HFM`+ad+LH)(9DMpOTkd~__y#uA# zXSeJ9;GFm_#9`SR!oj?Nbhap@Meyqt8}m)G$3wmLNTH;Ytg#ceUGE_HP)JelJs{w= zHCPdYm4;puh63=u!BsM%nN$WsT6e+bZ5`A%VSCD`H$!Q>xcO4 z$Yp`K6;f+}@E_xL{-G@=foRex}pQrbrOMa6Ec|Pq9e2(#X&-@$N(lFVJ+r z?Vv`sI&HOFFGB;vlRS$$MqH8~YVPfwe?uwt1|0iiE48b0>|`V)P83A51i8*x<0W-f zsd;nq-=Wdg%e}M(ygmYH63(~uO270^Kn??l663%~8Ox1cd+Cqy>hayio=2~0XDeuF zQOE?0Z7_^R?{BV0wozc14^6|*6+}9;(MWH@BtQQpy8PX*Up9v}H`Mb6N8HW)AEFmp zVyS80r+2M(Rt~&cei1dDFm}a^L9*=4u&2E9%Ib6?`--=sq8v>TsIg4Ne0svCnS28_ zoC;8UeK%}wHci~jPV4k*ev^g(QyH2sYBlFo73EmwCOiRj%uZ(Ll%vL8OS>RAFU`J4 zMKrjp{oYI7Mc1O^!iUZPmp$+Oukl==cPVT%hR<|3c6S5fGmoA4khXF5Oo)v z>d6H@^`q5TE=Tt%Ocz?e9q}B!*xg4YjToCfhcfJ@6ISQ>wb=`UX3Iu9<2?R~ktnMD z)Uq9)ho)Em+Ofnk4M~oBvv)836wxcWMC(0KP^k8)*0-*fhi7geB{~=SycOu9!oWz= zp9dMu@xy^=6!rOq)`}1lCh}_tp zVV%YI|E}Zn`tK#Jr`kzQjs>DyPzoRYEyoR*r(=Hn4MO=7~Vm ztsNSDFwdKKpYEv#u?FRg9C>_u9O?EGBnzkIvdar$vuCqq+J_!|70+(Ej6X-GX3%w4 zs6Y#}nJUFq8u~m9C!4*E)95+}d)mAh1z3<3QDPYVuTA_l5wUB#rU$0Qgbht>Jf(v; z=_%HW<)J3_etx^9L3GUNjRiE9mUX8!k88GYP;B*+F~-Q{6cZ~dm-hYLq=c&Eps-~u zEZ{b%oznO^H@8ZGYM!b6MEiLTWLK3>8}3`j&g@#e{5@0Hv4E#z=GZGt@@K#jRlBeG z9zwFoH*DQJcR6iZ-#Qxt+k;w%ev4!x2Nljo;E@Sr0#XY+{(j}@@f9pohZ^EvNO^5X z?WO{vTo*L9Vy^cy{0y53hDq2e0EP<-2iG_KUJn_WR75)8v6nB8kug&_CBm}JU+G>( z*9|=A|NTp%l-9klQ^qErzs|v6wq<8PGW>qFgcyt19i(yTxT$+CQ{=ka?^kAl#eZB6 zO-s=)o!IedWvso=YLDYTwP&t0I+t9F4xp0P25pdGtMAM@5>f~F9XwQ+y@rfpR&aZ} z&FBrr7m~5`IjJb%Mv6fa9Q3+x&e|`Z_pWTwF$N3rE}y|0pQcS93PD0fT2hRqSV|DR zE`*O`Y%(S7U&LS&bo?H{pcT(FWZA@}#pM zJ~kLO+j0l5k^c#A|AsBKiEnnV_6L&{_$koXL-c?k5&Q~&pNg`ekmqfQ_dGBlu{wq5zvxN;Xwu+;pA^HbnY2dXE@$=&J94@?d);6KAp zODG}ZJ^P7FQb;>t-E zPBp*!%Ky@(la>3%tN%ltY*hJK(~-Dya<*Fm9QJS<^N*s}N%|lCX*^cRt$;5`syI9M&&kEQhP zgR_M3^;t=m>CajI#eWL66UzZ;gMUQH!9(>45_mnYtdP8vuhK{VP5#*EfpC{jrspv1%~ ziDZCb0QlM7y>bGOu7OAA`T1GzRToeSYSEKKd=>wfEJ62**bZbJahYsRT)9uCE7I1( zIqV_If>h!e208NdAWe8p(t8s_e%3f0t;ujsekz}|WgcM{fYea3MHUqRcs`ocH*Zc|k>2gM4+~ViV=Y&9)!Os!I-9O&!n7G44I075dzqO{ z-%jw>Yez!jvUzc{SrT{7*w~Fy3{=7@Pg<_z4838iso5D`HwdIquGQ>fGqSS2+Z@wM z1i+6CyhR=XSvWb>OgHLa-+%c2$YF-h&dV>y5}-uRJ#y%Qa;!C1u)V_Prr|&d9Kdtu zRFPx$&jT5_$Z0=^CEUwh6f)u*Zwtus!}=BTz*o`fQWz$V-7sko!>(xutN1%}=ZGse z{j%DzPuu))BaJ*^)xrF9eTw>)x?C!QorY@RG`ht$2izo~83(EYl3}Z}_0z|8(kJxUVaRLK;{4`JDgiBfxX_aw0aIyN$>S^{3?4cU(e$8 zg*|DG!=Ggkg(y@w#Bd2z6o0ZOE4FHvgcTFaZ&X3X_2$Sr%W;;D zCsQ(9xw!J}Mza?Na8#&e59J8DBPW_w_jy9FHd~UoWh&2a*g4kmVY_tcZG!LCfVsZ6 zdxo2lUpF8|1pGBp7%oBUOqOqeQDK}koAM-MJ@R?N8M#GXiJrO-+JKzeoreqsBGmma z&E7S^e+@xFPOOq&mAA`^cl(YV`vN~-69I<32hL_HB;+nw1D-|E3oiK{@zIj5)B1 z?7@c53@}Cdu0az2l?tpYmkZ>YD2wXbHLCt1uY0#6Zzh7by3d)Zr-c91u_N=Mj#Byx zHQ5my0p5<1y)w!+e}4YW-yhL7y2<2Dzi1%Gr-~CN zKWzD-eX=Wo7l8WKlEBfEuP$QsM7MEJX37_&Cfuc4o`l_Y@Qu~lPj&O-Af2zldI74- z`bU1L#_jvv#+YioVMAX1=BTm9BjOo+%{f1qN|cK=?+8BC&Zd{&5Hi%{Y-ld;$Ls#3 z+mC|^u-`(^O~oLFcA64H0D!_Ociw3i^N5d>grr)dW*RkW*lGheDR*EmNv7X2dEAuA zkLp%TA(4+I$1NF!P`<+NL1J!`>SX7>(D%*E=3Ay6y1i0Q+l^2VF=Vqkvicma8OP5DqMV65K1z&R>pwH2Hs4las%=%N#J;-rJF)9zyf1 zMlp>rGV9o(PilC;c^RgA#DAa@xw*<_qv^q`stkZM<#guPn!`C2wOr`S8nFkzH z5^`#$H&Sm8ZKB?JuV;ZD`}=~E1JtZkG3na02YeMeD}|@2cia!ZrI2^qgo&W)e7Y}h zA$aB2FFv4)mopUFucYk>1i6l*XPzxDpIP=F*|uxZ+Rq=<7QNHPN4Z<5T`kT-tkM=n~t`?+%&H#AatGzYjZS; zu(hG;HS@{XJ13fOwn11;{=xSen=6GpTj-ken&kk@zoCjUg9lE+P;Uu8!Eomi{1d-A zzCj(pma9d~asMg*#!r{A~bca5p) zTv6>@!t7+TN2nSXjr?1aMvv?>Y7<36(?i>~Y*`aJ%SzM6Kin@~Ez)V9+8oC|oUP~- zL}i_Skzwk9g!HL!j~hnzpT)h7>@c7(xiz$DI99cA^vUS4eEomaQ{*1X4avzowaq_T z?>V(&^0L+X+o>&2FEdVVxn5$3Dk+2RR-K=r|BVwx!YJLA$IlLFx2pQE95U+ZZ8Jo< z*zis!#UKR2j#7Rxq0omEDVll;VO$JqmD@cvm85>PkLub^SxRm5vtpsIV1v5#m#c(i zlhP?roVnyWz#&hW0nt<{q?6MwwvCmtnQz^t_8ld(!IB}PkDqO2eXi2*$;{d2Q=C#> z%f7_MecL{(E}XXWQn@Fz>0^q+AvWs_h1S5Uw*!0vT5<#(EghWP?*>pcPJgCrZ(uvq zeHKGBO9gY&!L0pX^Carz(#-k;Bb?lmv4C)KG>C@kL%&fhguZo-*4!bsVeTszRvV+@ zBoKk&S<4mNcQn2f38CzpM*_qm++9jCAYg>gJt)wOR==DK5l1!b&JQ|GRy*31YQ0nQ z5}fvy#Z2yCmRv}m(css-eBiql)M42=M;(;KMIk7qt-G-<9J5F%+qNSnygpGy|2|6JKdS?TAGh3{M2mqdqw&8?w>m@yG3;C|Bv%*jX+&9 zZF~rW2Htn8oqZiWL*YM!>X*jcGRSgeHNAj$Ya}DfWDd_GiKZtUaskP=INoVHJF>$m z#w67A7DvtKNKyp$2rL<$sEpjy&L%5&>ghv7#3X-Iw##QK30lW6 zz3Gm*l%A%bu%w%z!n%b+DXxb%(dyCA-cF7=qnsPTa7zBv`h|MVt&jJqeN%Db;&MHA zozu%vUc=1e)SIvOp8BmDj)>T*x~|w@$NJB^9-QVB-EykyebT{0%BJHf%990ujdtHY z%`Ixdva!rdxKO%Y(u-wV1Xs$FVh@>D9O*iA?H*F))sp;Cw&pT&45uPfo@Q59m}o^D z{CPIOCl24&FDYeNSLe8#+*Zq@UY}wDP8HK^Q>~0E|NO{!-JukRrLN@rszSo7N;Lga zQ9{k-!i7py!$GmvJhH)`7pL;k9XJYMA9XWdh*`^r55x*q#^`p+-MI7 zo!@75sqkWYK%|gETi~SwTQ~T<;JA84Li`ln!GU05eU%GbA0KR0nAil9XLeMjh*HrGlk?SXKCQ7mS&%W5ONleF4gohMSy-in^kaVn zAR~;X)*k0|;44cAy_N0Yv>TV&)38RfQr3)oy-E&_M4>%<*aEML%I{0&Q_F0<;Jn>X z2$2K@qdorXWd`l5GC^bS4jBMzT!DEs9;v(6R^-sVlhCA%s## zuZk7~2i)O$&l{m!*v}EH9D9VDY6Bp5h-|GNVvd?pNO(=#zDgoC6mBCxW5^Bq$B@qI zFs+Zp8&Wg&EA)u*M_Kf`vjt^^8~oZhd9MhLF>lQ>o3F?+nJ|lq+io*#;kb);AujxE z=+)G4s{;M%of{ams;6&O@^Vh>XAon%1ZRK=2}&ipfSum^%6EUcqBr2c?Hd<3Iu}@$ z96DZVevDID{~rxx#paMBH*SiSxf%#;UTOd0wb!ky)k*x@n{k20 zoEx{gJpCJ2-?8!*v;-)Ou0YoW|A)Fs z#~-N>xBVTgLAFlVCfL#W?E^RqOZ%+hugvg*d2GNpJvQ#?k8Kd)W2ESscwUjHe?bUD z8>?iU%J{{aJH%e7t@kk?w74#qXjj-`%*gD`G zUtmG+Yj3H-;7f%OLYyH;z9x-lzD6XAi>q%=>-psq%%47;Yu3pKYjqTUbcBoI^_w#- znm(VOWb3AmI*o|)Mjv(G~tbVF2{Z>o9(J9%w7>#Sfo^=-uT>1NL=*X zKC3=TDeRs&sPIEUWwRlCK@Tj;|}*tnrMImugC-iV4bfu=OoQk`!%*;#)t zvffYd7wuZpRPl{UH=jrCvT_Gi0Ghbd8d^SC7!eB{23so{wo6xpb8OWsci04j;+XGx z!uTnNo(yz6lu%ZCA>ZlR--y*fMcttB@>Ya~o8fN)jx{B1y35L3RbY#)U*n$1;m>mB zBHK1uX5=x{(}Ae181ep6%1&9K>r4D5G@MGuVl6KmWE^SR5zg_L_fQ1a=gVL5O;>|0 z^U#rEuQK=_)3DHt42}rP{Qu?Lu-uzzSUmX6iEi~*+4VafScnZ(oN)QE)cRAp(RG34 z;@4yU(?a&^$O-Ts3F3q|%awc*A508S?OFW6HCf+YfCvCYGPGaYuS5qYx8p*D$&U7h z&|dZPYTkdQLXQeK^Na7t9Uw+Anv1{O< z;e=~q{lG0iwL!t(Pou5`12WPJ+$DN&RRDr6?7XoTc9H-razVTVtc4C87Ht6v&u?>3 zA@>NOp@HdJess2Z70*fm2@Tvh46T3ddS3ndnzSJ;|i1FPo!I8t# zCu&E{y3hC9m@h3=64O??;VH?u; z{tZn8*U{rAWdSeFMi04x{RBxmmQ?ajwqbk$W(qbWn95|Slx0}jcg{oRjj##`%10SC zM!YpZ+BTfsy^GmZnu(RBC`eI2j){LIl5iNXqhVn=xA=iR$F}_hE-fOB5nWxH#{0EK z>+KnkIoHgU*r>tBsBscOW$?J!6C+<0yiPaQbqg*HyH;zL_V>AQmVwKI#w!ON71#`} z*}&gQefRBt!Vq)>v&3@{%u?dO4Ax&5)jYB+<%9AR{tuNhp+aJ4jdA1Zc=5m8VvQYR z<8&C;!7@gmBm`yLycytqt22XqV4mDv79>BQEtX2O^7LeZLm*=?#;mWO7*0o6YujqW zL1Sk3BA6a<1*_S^lgTKbN2O#~n;@@yROhMg-HQN9qbv+s2Ycp3Pv_dM?JNG1{gP^p z-4XUF@LqJlUU32}^_?1+NaN)gMe^Ih0hUwaN(0{S97bLiB_$Dj5t+u5jT`w` zm`$<1O6}yPmId~j?N)zvj)?H;8eI2)ALYEhN)XgTmfG7-ziBn9XMVG}z{j}ep@U5- zWQm9Y)q<-tej8A%O_&hJ1ybz1rxat()?LJLZ$%RGIe+xV$Li^jPsq?9952jbPj{$o ztNWk(;6dq@xgUpOQ-n&xhJ8t2KW6^c9)6)Z z6endrRCc&^`N>~?RXe4dclO@q`ZnbDEl*Y}Q-7DX?`&z;tjd7|)1c%q<&?`mY_}F~ zAkh0hWHqcUg+ZJ=fQP31G#{UmVw{v?EN{@~&Y8%6@_ua zaLLN6{oCOn&K=ncF@gy&e~|gIO~$**`1tv2h8zz0!wILpzIxy4r@6L-I5DNpEZn9o z`hd#cTl6`m9B$jSb(3nGXgJj+n0l4671;JKN&(h~k>W97;dj`!gVdPgVrk`+FaX&s>9dKjAm2BWP_<+!;u&i6@Wna0CSKH$}wLLyY zvWl3iPDB;H_9e8cHB^U>4^Blf=ltk-R3{(@iA1fRr1YMk)srW)ihOn>zM4hKlL-$+blsXXl`uTLr+B<8 z4QD&5zkH#KvP2N>nGlxVvhkYw>7oKclD_>^z}(>s2m0UOGBfA2ZUwH;ut&g;0* ztkRJ7Yo+&YudHXOB^)-al$O>LEBjVM7goZ=#dc0vE^dBGLK?AMbNR-L$8U6(JvyCA z<5Bu-b4EtOgVl3dTFI)k-EV@{4|n zQ{VYekw_D8gtPh^wISC^c9VF0Y|K0@*;8Y^g@vs)dj z0BBww0Rd1yAKwu{35id{4whJMPm_C+D~`@9~pT^AG)sU0<%6<_HSSn*jL_a;_% zS!I9q$>$;k&eAk-e(YW>>n2R6?3>G@$lilQ4_3y5IU7SK>+p1v+oN|3OwSn2SSRuN zrrJ=#41^h7G3w$xj+paG9UiAw`Z=|TVu$zkc#?V+xG!b*2#Xlb_QsaiA16*~nGyPy z!D8~LuCDGS&z3_4#-3YBGQt~wx=yT6&JB4l@SL#ec#j9v=vXnI_w3u9Q&Twd&yB7h zxG2yOl)Cpt#3sUc4XHC=PRnM&MT_{H|!4!FSZBEE~o7~!8yTcK0n|+Ixj{SFwX!3%77cHx< zK(n5JXP_E1wj5os49Ke_uB?B@y&vwpIm`~TldDQ|>%m!u86zFT+>>}QAPW{zd2 zK9^D-(a4dT2g|{pA>(QNYh`1U3|rl}xp~lGynxrKo4-y45iP$Lq~q$|@6!H}9)!+T3BfBf^o9k)W@}Pq?jVLZKOg77@{2ncCah z3zSmSbuC|e;&y~=5bDN1AR^M<0xJo9(Y9^dP)w)DBSMOvc^=7sW@2Xt`xc$gnhs3` z#HCz5z(Oeea6lz29imqPwWl@95X8Fc|9(Bld{2Q+`9iZW`b#2f+~{k1EbQ{ofs@}c zw*Za-z>l_Aje+>lBKVlNaC`QiAbdfh6qxLoPm7g^Akh3#4ExqdKHM!e6e05%WmB)Lj*83As z0SwO|WgKrcs3JN|xNv*j$MZPVW)=#o|0ZWiH<>>-rQJ*nbai`jWJ8h2z)h^d*JKEI z`Cj9*UZEH5${tkf$IQ>WNi?37JKXC09g>d1Zm*NU!Dvf~>V#yY1}=(=HL7Uh=){3PuOq-&z>Cp1zt({PKx90rOj=w*4|xq>)h{P+4z$v*l< zKwE@r`Fgf&d@Ush+eG8}2gW9Ncrc>eKb`dEtnq?+jh~r`6IwK1-%U;j8^(muL>)Go z+}bs>AYu>QP`}$`PQ7K`-H?z7RpPx~IW0k5bqQMKmDAE@Rc6oPCn7ti-`KNwcEi5i zrv0~V#{afld*lL85!U{`1q?DRiPwE9M*6{yD@(`{Dq8#l;Zs^?mGg6f#8$vZ++;{99 z8#5k@R%f$NkrDrW{WyGdjpJ}2fSqDV)-(x3LMX#2dyWsQ&AZ~926s;9$X5x@v| zM|fi*dCqe;^VJ1S^N)xFqJ|E$-PWM>0M!P|-Bytul7^u=D$NKB2FI7=dbK-)%s{BD zMMZ=wr=Q84Tnn*Cs&z8&R>b+03Wl06Y3|i}t*XkI9cg^{QGG;yuKBS&5QKuI+Uks_ z@L`EknUU4yOT3J~#EnX`M!EZpL_{J-OLSAKD=RnHp4_o_H^usdp{C2iP#$1A_M}NM z#9k_zC-MXO>9H<>>yO0N*xGsQCAZYaA*orhpF>0Ip{4GF0-@J#<)_^onnjNt>{6fbEg{ju0b!8u z^%d|macC&}lW8{yQQhfQ=UZH8GmY8N=YX4AwOkb^=IH0WGUWY}WZRHFXw6rmug|oP z){<#+sx@cbF{o8+PGCH%5{&wDpQl!kI;;36Ga-7mA6TE1yTQhWj%ZgX(|>uTb(Xbf+$f_q#_ zu;HIM=v8^`Pp8Lq5#jF8DbEQ59%@jr5rdbBYBp$DtAOo&o zO@F8~2m?=>t)UJ54h1Wr=DRj}bX9qy^9Ozs-`)G~^K46->(q_YMF{M2gLtr(eYuG} zym1Ih^K#g`#R#$&{jFpka>c1!8>KeI=}A1J5j7FtGtzzHotIZH5O)+pwyJFp4= zR3D~V?Nf;n0d97g+89D$R}A6O9snL$L#fulrY`5}o1?aL4}1|5R)KyEu1!&cTxm_n zfpZ%OKLZO4lqiMG{>0-3&>CsIgdf#&=`>YVp5~lzn%3MTb1Rt_DQ81&&oY9>7ttk` zu}Uwtj5-t12|9Mqomap1-QegKRqea?gh-?wJ2@=y25orL_k|OrN?ae*)o*5R0;~~p z{7DT0h2K6b{JEm*j!H<}9cv3LJg4(mNJWVD3-S#bwyQK3t*$KYK}Mp9$-Z=HDj&I* zAIy)nmB$A-;hUlNK?*2{{_bTVTuPuWk%~d2)oXO=*^1R=DF2r>*|}EjeN2C?r0FtO zYZ!=Y;4Gp86WtNPg?BpU(IYSrc$N#hvK$eZhY8AU83{ke%Pq$C&~l^WmSggRW>!6& z0jv}bstB|0&d{>i$*$3V`b7bX;bni?6>SJQB~U8rDv(*=`9SHia?3e#lt|WzVGN09 z8gXLoM(RXJHDWRDa4FdsRM56MHwkY?5Dd=7Y?p^Xy5?3bEzpbZ?!e8b3COuh@UHn{ z>Ei3dQc_+^Yl0L!dGRcf*B+I&8vp+e4_pE7i0ZS#T@MO{K zX14*Sf4^krwO+DiQZPL?95p!YyxsM@R#fY(xkXCIs?)!sIwQ8{?MC7&3SWFJf1qo< z8fUkB*uH$!*n0lK$N6vF9un?n$Ct`dTvobr)fcrZM+ZW~#Ggzh{z_$ZqVDW0deu2U z{4P~;cjri5J71l{l=t|8``H<;*3iKhuT~pf*1p}53U#sWg~ZXi*@W@3LC;a??m>_C z=8M`H-UpJ|{;}0*8=<3ktGBv(d31DUeEQP}!;(T=vr?2rC2tpxGS7gXqSdY1Y2h^b zx~XpxdlH5qubJvld=r}V$yMtXqG1F*>B*9*@ zzpX`2|Et7?=U{W?PR|fFkGQHcp~r+A{r>Uak#v=Lf0eAnLYvpM`IXu4v!7)acMc+305mjxi&CVmfI0=I9+|Vq&{VG6SaVwA zwQ*t)5hju2Zqr_H4_7hQEUUl-G!_(x2gU#i0<<}LLRg{wD$nu)Qf`L6epMpF{rB%e zNO%Y#uy0Y(dte5|vt5fs#6)DJi1?cy)FG9u>B*V?eto-_cHKr{z;G3cUEAfK9yqov+ss$tosu&&vz;z$+-eVY_kf!_$E_AqPPW~clpl_*i4n3w^&&7~mH!UX zR^mpae>*bV^aMIs$p-pees}PYh4hyYMjgRvOJVQwAGO)pvuD91xV|uB9l|c%(AUT5 z=;(;P3LQoDS)(Po%|QnUT2A6%I%m7;J~XF2`?(-l>@+a;*5mVabp0{%yq7J{PYD22KREci}0up~G%n+73sC$sU(ec7@ zxMhZM?U7e=TbiVg6Sa@fNK>`{)uvV{_|)#PBGYDbjXjKv(GxdU`I6M42H!bOj3dNM z)N4tUgp@tDTgUZ2#o^AKK%J!j_FiGHeOD=@7$ zvR}!GE_Ah4J}t|{&h3ohHQtNYI(~nNFTBb!%KUb9zr}n>;)|Jv)kyxbv3a$#b~nkx z$?h^@XuS3qcT@fz9c>Jr9FW?-|5~FhqOeBI*E;?KANqf*-i=qF0Jjy&m{)` zcIb33QFxYhJwKw?E5-I#C;Qs>%AfhxdeMDo{7BjK>a&!>@~$r@z5oCB?*9LOJ{#3F z??HaVm66Qs!kLEu{`uP1<42ZlDXl17em{O^oky;-RU)=Y-d*|Bl<=BuH85A;ih8FKq?-UaJ zeEzu}a$ai{W4QM}dGfSbFL!=}tZWsN--k=y(#;ypfoGcGHRlR5kc19@O)X+tryKPk zZkA|6zuPb>MTC{^3_5?CO9@qvI3k>DzFW8#GW5CfisZ|etQjBZ`+A;eG!<(oJ}xEI z06H4hUmxh`>7S#6wu-PD#x?e;-Tr+wZxC4}poiEUlne#08UvKKCQO1y#Xca!jER}W z98Hu`gksD=gGQAG)?3;As$(B4k6}u~cnGt4=FPc$d60o) z8oQDvt*WXDd-^cW&rZ;!UpW2%1aT6Tz{aN90L!0RDAL1D7H2|-ElB+>%kV5l_Ag`Y z2Q3}rB-|oko6a4b;(h6s(+OFK2}~Uv|x%cTB+fl;Q!*CSSe=r0$XS# zp#CCv6p|L#HQz0p+aPkO%I}YERN&X*8VXotK6{^A+4Dwl(7{)bS}BDO*Tf!{Uzl#` zln~Y(Xmcny^{VK#xMnaX5$C0!nd>rcgxemebh6s!sEsPz(QobJ2KFcbM_wD-6%)4l z4$r)njlKA?arxV-Mr!NjX#=yAzbR&u}{Xex+w0l8ETuOSZ|2h9Q$60fyDxvg@SSnS+lAEI=fT)dlK8V~RDY4&9 zw$l$ctZKe^weldOTbWefBNcQ*7grr`&QD9O>SU~@b)qhJohg~k&+1{ISdSf5@#+WG zx0l7vw`8wRZZK?_FSgW2_p-rEhkNjwUxpgn~PU^k_Td6;R-h(;IJEvFC3I|p_|0sA=>ceSU zHrb{yhCNE!*2Qe~srSZ82J-eW?3vOFrVnM`Z_D&FcTcL-Pr)yHwpL43(&R>c=!nt7 zeKO(%d@`R~IVBBwUps_-k2x_7rO!5n$i{Z! ze{|=v6ANYM-Te$m#K!M@PxVP zO7=`>(&*+4=Ap+Q2i5ub>z`ym<3F@JSUgEtrTJ;t!Owqx5SKddg*0zdPtQ0aO9lE+ z)2BbQMD2ZNi2c8)*z_awyB0KH1!ZK^jPrRRat}SrOE-^3^~Y?Uyl#oc4O8896)xT5 zrK|B*#omTe?h}`+dhvDT_e@vgU~@DCATB=BjPX7ey}l)K@h{!f&RC!ZW}m#G!9lYW z)>Xdk^JU>Rv3%lZtCG!S?3)TB4a(bhm+mj#wBfBr^R7pg7F|B;(l!5m;nlwQX2JYs zNia>k7Yj;INh7B|Ph$AjNzT!q99Qyxy$hz~c=VBuuFqe_=G3^Wx>(3Rsn+J1ENpBc zqhVh(<`QI|w*Xf;*L<#lvN5XqeE4(kl&K;FStF+IyT_NvWY$ms# z1+6H5?2E@7lk<(9QNCdFfAL;vL}semRgplh3E7>7+SadPA8Iq#EBA^@l~~hllFGDX zHQZG9**;d~lT*-@OeJySH#<2`aZefKrc+OEn?|Wyhxs24kyRrDKbnM(Fdk>&p`DDF zcJQ6fDD}+O2dd_`=pS=2R`%ZkK?iZM_=KPvrS+Gr!@gJR`7QWSZ4o=}9HeFT{QBz? z8L7QRwmv=nY_las-$YoxA)wsqVfOedJ||a*Y$n8L4#76 z5~XAaAtXr>m7x%#RAk6J%bY3g24xmP=4}o|C^A=+-h{itqS!>TPBQuANpEl1hcZ8jwoar;G}fZj*H{xv+q~`ZoI0;1wZ5nPbln5M23x{3 z`;XyFXh_jv*Ex9`QP=w)_hRBV#%L8I1j@4;oRkS^PVDe(b!%V79)j|A&hJEPK7Yq4>7jN3$!u$vb<^9a7wiPRH$Ll zdvLu6T+v`GEzTEWFNC?4Ec-~ft((DN5s@%?IO@o`9RJUWJ$aipHm-c(W5uwO>Vq`C z0g<_dC(lY4V!K@TaxCO{QHFCpbc(WgZeM7sRnF`DtKfr@;q<=g^mmG}658w@ZHmVG zNXNwg;@ZB878SFfaEBK!g@x<+Ogt^0c9{$h8~kU)N|Sl{{>~daR;y!sN;_=|4DUo_ z#P!=XTTi!WUT+IMlGbr4DUhB22eWZ}hGguLt6-eUuEr|-bsxqtl}$-gB1VsQyVuQr zx>;5?ou5|l!Ru<5Z&~M$v5>|5x9kP>vS69 z^G3Jp1S_|QN(wPqq1h1WErCSX>5o!l;4gMd9ru*mB6VD`syugS zf^mUC5a;>oDT9R9&iI3Y>lhMM#0%J@Oy_nt!+JG5JX~1W57;G^i?5uH8)EAieU77$pass50i5h8R zP#tnnYjF*X2;Uw#twSHqNTew8J-x^v7iOok zt9!b63&5;kV^BWjTO2RwBz5J(pUd&>sSs=>n=RU($BKVU6sSY(0iFg+oD9AQJA$gdAb9K z-kjc@u>YsOuJn$s<}a#Mit{&9dJ9O`qga<+Is0i@TW`suaXIYAvU$O^eMN?YL? z%ii@lZ6A&^)jLb-P4C=YC?!^PUTFShm@XU@yZ?3Z|( zEI!sIAMhxUV-HJd&0x3WOASx>@%)6knW&c0fkS`(8j8s-%3{E-@7)2W27}cc(e>Po zc5wsx%x`_aO~GIkVv^=>YCr1TVnQ>rDJzu!-*SS5p zG`Xys1y0gp49HCmQ7|=VeB|b(o&VSE%J&NbIMCH&H3NAyHP1_!T2n;u^Tr7frV?Ji z?h`N0@*9225&dE81I1{ag2v;5&%8zVMl1JJLKrsU*t7Uy#LxM(V~8;exJ4$p0R)!YzQBJC(T@b zecoL{!T0tPzL&ela@Gb)EbKbs*w6(FX`2}anl*%5YG(TSTpf)v~ zvh#69Ajd9@U0J(o?6nJ?+;xeQd3sUb=<0SA+RZs#BfmDqLWU1hqhD;PpPv{ogO7k0 zt;R?M{EUCMb8BehR2oh14o=cI2qtZ>OP%%?;7oE&(J*D=rl3)JE#`$`aU7F$h`!-+52n!sf-BsT*B${Q(DR)&%B5?Tang}Mg ziKcBem-kCjB!Lb0+}h7R98&^;tU}sHSK>(Kxtei`T|# zn{N`X|aO zcs+h^6&MEu-=Yh*P(q0pjKJE(AnLp3{8YM`=uXg7`vR+*08T@E4+Bo?4xcrk3dzo0ZzqQkDIo}ZsQU*wN{SSp{U zTQoK88E5HMuxjsh`#po3%0@*FO8K+hee8SITb07Km;L%3`%1@44!^!{CU48{Lkgpd zE=N^QJO0qoEqA0~SS&d{En}+V$?VSWD3j!6zp(JzV|#27hXUo%wJ2_|%S6;;BBe3j zCqv#n?Do^{10AGoGHQ%(c*R1F#KIA_1qp*RYYRfl;nM(M9ip2W#>*K8P?CJPI5Rf| zGXl_N9%9qalH4)il&cQ&ZZ3mu={CF)64hpfF2O1(m$kKxT&_Oyde@^l^3#_oMScI+ zEA-b20UI{1=B->~TDoV~_lS(`Z|_y}Qr`6)${7gMH{-2&FVtZ8h&%TQhb3RV3F{}J z2$h4-vIE{X=d6$PI%JliC>E_ub@0 zG(s#3QAc5N&hd`r-mTGjBXySByjvZRwbO1TCdIA4kZ2agoob%@{5i@E))yGM@2tH8 zzQiF3k+a2J62l%dnn_12OrHOAm2?ZKp7=65r1^b8(=&FcL`dM~Y48+}Zp0BmSNuu>j}I_2kc^8w7}5;UzVsc$pQcl2SXfIX;0R=T#)iBr*ACqHZ&$H=I{h3@?y>V z(e>9)U$V^0?&mVfXx%wF+p_#N zD>gzuRRBK=kK{1zZ{Bj}VPvZAwFfV9AG`hB(rQ!vZDwx#M)Fh9e9JomgilRaHf133 zOuWdx8PJlZ3PJ#}bN$dOY@3?_VgOnRvOh)&7IFNAGYo<^xbVQQu5iFcEHPKW=u{!C z1#SQ6{A9nPth#{bcL&!u!e`&q&6WjyroR4ZiZ67ZPh|U{PUrdEKg-;Y5brgR`l$ZC z=vR^61yY0uA&bCvr5{EwI6}m$3LF#SjzMf9y0MFP1+gD%yWU)#2q+anidp$1koj+g zx!w*!Ps)p51TR0&uJ`TbnY!<4P65K+GF|^NOsQ_i>(J*p4#diZ_q))VGM8_t%RM`} zjZL%c%))Z9=fr9HShwZj*iZ9UY?maP`8dS=eo-%6koym&_&W)o3$N4hyG876Ognbq z{7Ej&^n&upJ)yz3owb@hy1u8#CT6*M;(^cc8=E566lGPvk5MR92vcaaW+1{PP(HN+LG3*@~;s2QXUMX#Bo5g%GeFzyAJKftBa&y-a;Y zdsOfiK@~*SEY}v^URxo~5EB7n!2GkTn2=ipBYKH3+e&~0Aq^5PTHO6V&^v?aRMLOo z+6q&ZFb-CPDIktxJHi!T2d+VSLG=5^#_5*y|$2F3a;TmZbU+Cml9#WQ>5( ztix=71o`I>Hd8nc&O~Bx*P<8%R~NFrR%;dwIAq%J8_d|C7E_f_m9(P^Gy24uc$`>d ztpuvijP>H1OXNic&7N;%8%9?S8*G2M;}@jkE1f(%q$@wrG{i*q7I&9Ms(Owu{|0mS z&Zz{dnwnhSj34l7x#98SeEfGfd>w%t;+n8@xwk$u&OW@=NxU~%rOj6INWHE<(jC*6 zrZ{Yl5TaG|LMg3DnRqvFfkDhb7QO|Y_M7!!LC^Je2gR&!}-U#RIxch8+`wPn*#LFC9B)UJy?|?B2LP5c)K>-F66C9+V z(h7@+0MqQiE)bixSinNS0#EW>YE4y+e#E@n^y<~Wux5kO7cDx^M#EqMUk=3-^=l6Q z)+*92NS;EeY8r0;c;(-JD~+_rV8s*|MEwO`C;ob3+y|Fc&<|T--HH7R+Q6jKK_sZf z!{9ZMM-V?)$VoC=dx#Bsnrb|};mAS{@JxEEL3WbR&DlB+cv}x(J&Px~$K~p_;_KZq zDe8tcIq!dUP1}xF?$@T6+BnC}&dNzrO!f7B+(!$8Zp`mVz4)}nD&KXX&CaH!F~tuYUd)YkhuFf#3nH8>$=`upA;nZj__c< z!RqIpJ~DX7Bu)IBQ*DSS0)BW!T-TAW2__=+5u@a; zC*g=>(uWi0W}%V?phqRu@c}$rbv`Jl@ZS|8VadmDY*zU9Mq-eoIIUt-SdGY{6#@eO zyW@JrUEI<2L-5!Rkf85 zUtgCK&^1T4Li*IR@)=(9HUb#II#QL@|G{~1_EwaYh=%x`wkCLi8~f= z`mCJi(u?d~U;eDV-wB+ZHvnQ!d?_Uafc>*8RNg$p9q<&cvc3y71eoF@*1 z)fp4NSVZQm(VZ@$j472*D&b}Co%iJUfu;@5&aYo?eAXi*I(?YBbxKU@g~fKyL3odg z@Mc6`d2*~gkrxX?MmZ21xsJwv9&zSaN}+))5QG5Ln{zD$v{sH58$+$$2f?J3F2_Kvqbrrg*Hj!iphqoIg;hT`w;DMa%(P-Q(j+r_)MvZ zq6;jtu^((=H_|kAe7vnr5e`&1!5pOf0RatfJrssMyG?WFmVsW(xdKka5fQ`8@}=SO2IA|B zr$r-U)q$~~vRL8A-TFL%%@n8fRtwY;$Us{l5tUba*k<_NoGf5q5GM<*98m~>(*}Pm zqos?P>t3z!lu=J`uoT6vb-o9;D8^7J+`OQ-6;Q$0@)Q#U3s5n8tq&};QHdE zgD}+bO}`DrJwfx0w$ao6axHusXhy>i)2^63Iu%#*+RrZKS?{X03o&-r{K8Y@ zk}@9MrzumFnXt9{ ztj2{;u67D4QOU5*XpizWH-=Hq>0(l8K`S-Mi|o@JzY&kagiVGWDY94$>ix=J;EUVQ)h+$_7l0uz!g9fuOlcRKnD>GYl!(&xE*meQ7x zVKH86X*(24WV-lN7ucv?Gu(ycC`>kSrh;E!F)-yEaBR(MhsgfKEDCk4tZF=fn@Cev zy|XFm3f+T$Oj|_buf8AN9#EVZlr^kMd$+hHe7Zt6ydZ&J#A!>is`Jxx8dCz_I`gg3 zhE2I0r^^UW;(fOJoM;I_fK5Z$y9s7-{_Zjb<{WK<$x;PiB5$QBZAB0QrUzk+QE+jebJ z<~eX>ZNcDC6Qt~Cn(!F0)H6hL0B#VZM&Q0qg)8pE@j!M()>~LXN1MDm( z9S}JJ5goc`t!M&CN z5JpI{!`8t6T;}A5*Lm?gTC3JcpFg8seJG>%Fyat^Tq&pzd3kwN+IH^F={P*;yyb|a zKeVgR8#vOuax?8KfJGL&_{Q6n!ks#vg>;dDk6RCCU9V`t=JH@&z!%@dt5GHf&jvs2 z)+p9)!z=H1H&$&#t@=vKAaT{a#>n*_^ zRm2{1nR~{0OXx{j9CY>a54F@E_Feuy(ssa;hfRTE)@JQ6dR^ki3x4kVvBKvNgvY!d zJOR4QaK9e)>W)z3qxy-wif`6Q+ldHipjMz-y0$DTE7a1;8g>STE9a zv1p1g*B55vmye=0-K;GBG9~y0#nBu{s2WwAsuY&sCQKBln zg-0RfY`*rhNJ7ouP(IbL%(hGHN$=<3tc2#r`YC6>Ol4h99OA>9&5v9lz1r!r>kLOa zXEtFrU)_c{qbdVBq!Np6E@a5cDsO5`TOMgynQz$U+p1^ClpOtKlKTr-v7o_We_w)z z{`>M=`I3Y1{j6;USH z!O>wLDLBD|?RL#`A{y4rv%93`nuEqcte#((>Rvcpts374Mm0N0*$3>{hg2i2Mmghj>HMv&W9OXx4^y+SeT@nM@JFnw9_; z{RZ2zFoz-chJ4?cNtPRc0EC*pe9WM$23l%5-~Y~Iqh0cB${&XN8OvNg4Nu*Vx1X6C zpyz*4e(c7qM_swlj`QKF!=CqK&!}7Z|5*?V&6MAmB)k-~F*5Ls48^RYy*pOpx%GxR z-{WlIyIxcXd)&Ts3OFkfoKa#tk;o&`fi26FONy@=A0Tgcl@-=sf0az`MZMu z$I|j*7|O4(et>qN2aS8XbTea)>bB|Hx_M-iqd-H{5UUb@Sns-)kk0618#bLU(?vbk zD7Nh*o4b~ED9WFZkpZMeK{`kxA$!nedq!Q!qqv`+UyB@k%rEFv#aV5KR*B*izn5`Z zH?+v<^zPU13A0BcbE|So$RhV#!B-D)$|7f?gYFs!2PL0N3TRSVE%Y@na(s7g7V2{P zbZynO#>FPh2)4HzR@cAE@m5cNJTuom`Q<$0x&nq@lw1LkA(}7?FJOG1^XpS0v=zH- zdX9}9uE`Z3Etp~z2R7HdzL<{uC$~8a4zh#9ftp8l*$=cz6}nGI!}df_NIm<=en$Jr zsV%=}dQWS~qabhA545+BFF||!z^!Z(HntF%7kU;`e4N!TZWj}VixYr$h z(bJ2t0b^9&7pvkh-lrhg<+mGOC|L*58UJZ>f=cL-m*`paR*7q`Gg3Jx`Q|k_Bx4HMI!R_Md z+db!DZEp0QQ8}$1eJ6D16~awU$LXjd+?$KC{613edI<pR@T*Gzqt z@v$~PT6a@u`HQ8g6vh5F+qP4VZud0P7r_Z0xU#eu$rNi7lxHtmUCQMxX#QMd#2&cf0tT4@L*F zXIqa|n4B7(7cPKR07J~2>>t4z^3|(Xnmrn&x^O(;{z6qp^6rR499xZNz0@hrC0me# zfS;V@4)uRY5rtU2=l&fPy_8;`7bT7Rec!+d7Kmo;i)8oRgSwnW@j8wBOGDF=-L0V@ zgDnbQSfaKd59M*#T-k|d^aDjVH#ZOQ$b6(u>DYbYgWP$?A5SHs%$qWfoRiqoTxAtp z^$u7OAU9;JK63l{G_uwRwTJf%&6WULgE_7{GBV7*C(oRrmBT_rQV7H=L3?`vduliFF#SI#MJefwI2{M@#U-z%JH1ivg+;!7Yck8)UkYP#VYzu^!;vO z2uJC>Q?xdu=yn3o;<6y?j-0PMsK~7d&2;xwBbNtyOZ|E#h}}>K5r#Wt%I%D|JuzLi zro8{X2;<5(HV@7EjnCiw&2#h9{D3Nx-MPl9LVsPy=bQ?@$4)cPF^+3;B+ir@yI)e| z*dJF~u5o}VNwN{I9j>kwUNTttIDY`2V}Of+nS`GJs^H-W?yTLO*Xf?Pi#}!kCZk;K zWLB{FPZzZ&>Q>6rAQc{p1pCxwldHeve%;L;$~P?Jb%M$T_IL4)g}Rk;a=)!o`QseD z;((74VeF@Dy`bSD;8~cJ^$F>mMDHNJ4bBz0HFWC=2EeNSMA^EU#IN-9jcpwnh)QL@emC>i;Y9cPRpNF%vh=aHB@Dau=(mlm)p;1S(2Hfzn{JQlZRMwPcKrQej47~4wY&EGOe2$8b<&62 zo(E1{+9%6Kd)G-7UIj3Ra=p!KgNS@K z*Lg$>T9Xv?*%B-s$?4dq9;4H7m%H-sy?NsI0%y)UXO7Q~)+K8HUzC|)=rryBVCE-Q zVLK$Vhb**o_8pB4WH>oJz2YH4!t99X8cP+|nQ!;o3$C$2baXqAQrq=mcY4RuO#X1J z3kMy)WW6r5-1}?igW@R!!z`p8nk#^PREJpBaL>G_mDP{;!{S@JMajR=GBz>cri&?37`Q(2xr$^$tTb29w*aL(iq+66lGlb7 zoHMl6wm0C&igf|$8zB`32QangDkzOhE1nM4##zWB8ksv*LH6JQE$ zI(JW>%itXIJ<**3c{HPHJlC}$v~RV5t;|u68DU7cF7U0Zqxx-AKKkMCj~2RT5qR)o z|6mnFns9#`n~AykeoYk&aYtZ}jr|YS$W#c8q_->j{6w9X(ahZ%Y-|(HdoU{icCr6> zS)4#?_@l^Zh00;KBuv)+TLV5=QS6RKxAro8+@+CQD9Vu3%Nj57_FVU>3eG!sgH;ue z)PDaUZWFzC^4wt-X>S?V45GTgG;v};%g8LpR3nAOPjSWd0DHh>e{q^vV@;BS25r`3 zft;7m&*WULq&83;Q+#Uwsmw(LF1HtYnjWLMIF)+hg)VZ}&bCyJ_WjJfm??1Jz`jO9 zh{bch9n>0Wn`r%US2^xVOxmX>Ny8H66+M%Y=5XrJ*T25=-~>io%Q*2nCdsmM zc<0#);XSVf71TR<90TfiSn1nOStu^vprP5yv0ubt^F(qUOE{k~{=X@OWB7>8Ra-M} z13jPG!`JvWB?-8c>IQH%@9YbS^_>Xz1=G+oX$5XvW z(o3KDa?xwFZ)>Yo=SI@#)8em8Ym9lDPd2sKN7I!Q-_iSQ6Ajyd-FseB?}osaM#f@f zCm^}_+}q|+A3wh^dgYJ&G6|02(fQ=_=9t_ed^OKW3;gIBE^;BjKumRT_Tdhu9IPVh zB?7c!d9fnGZ_?s%%n{6d|GrZ90>9U%wSz+8mjxQnXH&)`gYtJ=3*}NUdTPb-IZb^( zgc|7IDum4>tiMNm51se@OnqyIll@inM5l~>a=xkI7RPcLUfPg6Q=O;~=A~rTE_xg5 z>cN}Irl8g&&n)B(4lERS@JnajV*od8;-OO{uG{(cZ--8d<`GU9=hUi0ZC&a9yR5}? zx)WbE$mKO45W;#qO=P#@gC5}i(R9drc-Ha)TDU;9*kSon2efqDBLL0^HelTwP*)CJsp8Uy- zR+bK_N(1jZS#vYS+Z%7jr6{f|J!IjEDoeanV0D6k1}rHhXTIKn0-Ye}I0V)B$XAg% z4#ktAzbpyx1}J-CxbHdw9DVe;e)Jvm4)@xxUB+YRC&BDyeL8aZjLDR(1D(3aKgi6i~ z_{?J;_W6Y+DG^w{?A?fW3RXV2geFQV*U~BUpjF)_J;&Q`v0a*G_7k#_CnFEFW%;2I zn|`&n^Tuc?#G~|UHf-J9^XJ&8QI0luSgq|~4U%F(nE}~~!JBc=55L7oqU9o z2ki#){ex34ruQC|x@VKX!c-KwMsuIFaj=^C5$g+@GS#n~JUA5CLyQ(|RI>N}-D(z+ zul~sQyZ0<$h=PUyWEl#nZ2*kXC2Gr0z z=Vi;3n6hSipXvUS0E^OCO`}>e>%@!S+iW3wk?e3&ziyjxjlZ?1; z($7ZiovcYdz}GLQ*e+RUwUc6Q7%36p7FIPbDZARdLY5=B%i;v%)j__%t~WVXCU^LK z%rI6Dztv@vSS#YoQFZiLGVO*S`C_{Jn`w?)6)30ZvWKbmD$#{x>~$Ky8C7)4@|ft) z^+RXZ%LN5#ynELpzdCTVIp133;`+4R_ZcQ7h1ykhRNMN${P{l%)S02*EjwbV8aJUD zW4rXxVvJX>MYK2gajflrf||ix4IYSmVI&Zhyn5R1BQM~R652d=aL@7I%zMAh?TMc+ zXv{OOGtvFy8*!d@JA>-R`X=z#D~J!wXXj(ovB=w~H#tdk9m|Ja5D zh=by@dcj!8orNU4ku@5u0eBgSRTqAVpw%NC@sG?@_PPjyZ^d9}m~+{Gi+61EFsxMw zYAK&tyQ6GUPHG`|x&Bhw9}V&~F7IIIBqos(pU2gzlw` z3?R8mt@|(#sWApC8=K|tN`A^XWf5>nLX|s=+dB2K*5yh&*3UKqar=F58*BPRgzTEb zlyUG9iN85^LsB{eDMuY^-s8#$g)94FUeHipzX#0ixI@bSXNxCo{;+%bj`VEa z7wL7LqKS7R)qEVueFzr(bWlPPTMkgG>>x@e*%r{=lD~q8o##=Cut+Bf zwuCHtW8N8{3E3zUtesfq<2lqvAHjM6S_fQdWNvuZZ*MU3m9@49pdHEA*}}6=fga0u z1SQE3`<4rQ%6zeJ6*Yk$2L20VWaJfrbjDwF!@42MS7hwxp(5ki(&dBdG;QZ+sdJOrnOd(M3} z&qr{8gVzXA)sxEwH-=Aw1Evi$z<95SdD+XE9>2y-b^B-~4>_Ltu%_PR*qFp8r`_2M z_wp@&k0yef)i}3>cIo>u&za-CavS8-46d=IPb@Sodc^6QuLsdMr8g2b^(6Y{QEap^ z{8YLZySl5+MgrLAGoBrBYsMyXC;LK3%V3x1-a88jqL!q?wl{4#yNYU9-x5B$p zwGV{WngsVK zg{jyrR8o`7-V$fYRq`!|wHs5cJ0_>56Lv`)GBs&(+`{E^D!-AI}OHu`}=Hvu1JmoMo3>?jB#A5Wc7c?<&EQ3c{>C^wCz>^G>8VcLOcV30Ba<>k-1#RniA$bcC|}%ly{j~O zH%{2#y>(WHXNt=guV~eCmkms`X5R>~5>rZG-X;v=pckR>qJ}26i)`Pi82{TSHmcBX zwr1S76@H!cUSsahke9LiJGkGka=Co?Na*$0eu3q#W7e}bOIzG-Zf;=vHZ*ytVLbR* z*8?~3q7bEyZ?Fw4CETpvzUk{uWn4nN#P)!MpkgIv{~r$01o4y8n}4B47X4t|VAMQN zELW;o?bKy-*NVaqul~?lqeoG3YsC3)f~`kZY;Z=kU7VsgjJ%+X$1k>)lc;9P3E zeDf-Si&Y=_Iq{3=w~r(Yu51b%AwUAidHx)+Ware=PW;wRf$|l$HY>_BlG$Y(*MXuiMCB@hOH{%4>Vf2OwY*P&3e1+RLTrQ6?sAmA-0nD z^C6cp2wDeamq{LKW+0(05xqc+m8a9suhajJQ};gXM@sF<8|CKoVDp*&s_9 zyCHp0!k5+^d$(w<|@5l>s~&FeQoaA?QW8m7)^$ zqoCJTuh}82e8}@2rvd7~srsS7)|4;N9LLjr=YDQf!Z`IUqX1V^XUF0GlmRqJ+WzsiP9@5HOM z+pe-Ofk<{);8Uvw>v6Y-hShKF^OME&hLS!h$H+>W$(>S={p7 z!u||b))MmuoOSzVxgB=owE-}SbGWiKR7T}Q04)hyeR3r)e9Dq?(CLS$b7p;C2DM78 zc_pRdo#x?Qk zW!_Yb_O6TyZ_PUpePR4)ChK%@JICbOmW0{s|o`ODJI&`eG-xJcbg<>VZjX*LviUvFr!KHQu{zGh4g4QaTVKFZNFsO%Wu<1HsS3E#Uo^65fR+203dM=7U!lw zVqbyiA!58& zd*5)qz1U{6-=HYV`KXf5_T0=5Y7yDlK++pSS$Z-lG%ow97C@ob@EbH3ZR4K2`DJOng0k1@Gqqd*}&||F4v421*+>UyE=upL*9uCmk6v81- z>U0}^isqD5o|a2L4AV;<-px3Vm1S}LtveftkG@OuVTuSGm0%$O7A}?nQ*_ibvamsg!tO!OfK7?m83s#$TcsT%uCDGOH;EV^rw zS&Qk97~hZ%{7Cos4LH9n1&-cao~H>4Y@&1rF}x%M_wxB)-Mx+7^WCm9&8N{8m)-Id zX&0E^^QF;n!&ck*U&m(H;AQH9VEg*h(;Q}L5q~x+SOq^ia78T7u3jF#!?T&?vzAN4Y|96`i>FLf z;v}kI?rGQ4r?8b(q9#pxg5}FJWtpMci9kU0(#v(_wu^Oo@(1<$aHI(7r&o2UPZgoZ z^G9GeiR0j3nnQDkBy^k9??f&{6?nsKcvqkaQZ==^5kZ_jVKjUn?IoN+u>0}f8YptZ zBnr+E1f-U2qkh42{nU#!R-d2yV2lP;nB2(lgho3;3NVQMh*dZ%S&!3&G6j-u9-_eE zITt0{$;QSu02f4hywuRUIv_QY%qWf(S8Z$rHAi9aLwuo|sDte~MhYf=Tq@n9p>~cj zG8-Q5DCUG&m}+W@2EO;s&E;v?SATYQumZeV&e^@4Rh{Qo=CH1;Gd$S@>{ryPiicM7E17 zex9`;%%!Qsej&X5j2EAUZCCY__Qil~16$h%m{1m{yBopnRmL`f^m9GdJNB)A4fNq3s_mPQ&$TbG zOtt*N>~{3z_(L1Xq4EJEkkJ>joi4v5HXiL z(JO{T6*;ipTheCNtq8^ajp;Jll6DB`A5xk|uniLLd|6L{kW)Y|}K-*0WDlwF! z&xT*kulA%huY)$TOT-F^kZzkDFy(%XI(|wX(G>t=XZVYUK;5p#&WLx znR->(3-7XRO>DxGf}|U^U(#-fD|Mgk<5>>tUU=HwgZClmWVq{O^J#2720r)+^#|Vx z8e5t!kJnCk1p&fvr)|X+j~+d#20y(?4#5($T|2a2hP7ILvaJX>@_UX_8mBkorFYfr zDsA_)>(`|Xj~>Ms0db>J^R{7@bpE|6Dao+SQ*z^X08dpn=TysUYTB5wW=lB77hG#J zQy}EAS~I-xSC!C8Myk+__Bz1I9R8;+DxV$CW?QC0AF^UbOd4)zW~!!dh*^KWYu$kt z(((A#`&uNK;_}}UYuMxhkNc5xfD1R&^11)nQccy$#H<-uJ}UCi#BwRX#{-2i^1?kU zK_yx$=p$B4piv!>5f96Yhhd4>C~7icB0^7#lvriKhzchiToPmwM-9Zwj>PE@u3yE= zo@ienJGsDDg_jgKnf(0e_URg_vIP*H@t*qnXfV9Yujj<{fk0GrVKS$;PSjt!>9?c2 zuL`}!7}7N#X>%);LJ(BQ$LD1b%<~uVoay{V?=<<3q^YGomU4v|UH}>vW zE>qVtplW|<#|Z{acC|b$zwtp3BL;s97d!tlfb%Fg;#>*_ASkqm;?2ivmv7TWSUyNX z0UHr(OAxaOstHD}9n=E0Ln#qGB$tWvx_p0@X5PKbtfEbFYX5yJF{WUTgJ<@$vye1y zVCQ+n!FDAhA$t7|9Y;D6vH~Y5q62|t3zrha0jcJIi@*#g)J|L+1SBm3V{QVXOW>4@ zn*dD+)G3~7tIZ|P6cg8zAIvncdS?$+VW1;US96LtFrfbaE!=C^OKd<-No5H|*MV5j zj6i^CW)_weD@p^8nBZ^vSOh_#+eAGYf!z!nn<aM;ZXRm-A=KOByZH$ z+}#l^W*@~pzV0n{{%PcBMMIgM`7(XY#LUHehwjuV=rYb5G%pSYC)FUOrH`w4wDi7} zV0GsLYbFm*p-WxW^W)K|`-aE1<_b7`{d!U{h2HX0RzK2uzK`=~0p$O_gK4H!Z&BlO zet>|2tbXNE={zAquL5y|xYWUwIT$zD;8)>(i+y94f85*2I=JhVv(+2ntM&$P@9x`2 zton%0kS3u5UJS}k9eEzVSJbJzh2N+{7vx^wD@yl;X$ z$Xh8rzVXNx+qGUEx5l9g2xJ=|zSvL+>DOc8S{{!Gt{a$XP(CsSTJF83ZB2|}>RGa~ zKaKUWDby9TNuk61SSD`1*z!ED;mH5^3_oSORT}kVfBKj{vf1F0VP6)L8Rs3gMI-&= zwu!jM8Jl7sXz6g;PRr?rBy;tBvd0wi2=mcSdb$h&!ygj#bc-qUh83KTq5>^DX0nu| zqUFvrzIkQ(_szeo#G+l~5COzTHw0ojc*?kV1_#YPZeoQGbb{VKZ(hJ4iW>z# zn*uNclZ5=~(~+!^Tr?H33t%xKtxEaO*gdPZE!?Q_1IM~q{P;2ua+TaNc33SO8Evnp z4aY1!J>zgkLHQ2;UO&(qymP(cva%j?E%7UxW@1wxzciUfu!XE$_ucD^M9@|svsx ze%K%DGXrD=+Nz;3c7uqpL6$`l&|ab~F~JN+prmi#zV%5F%H%49GHMGRI(%48EV>Hd zS{yOSCQMt>nL4*>5rzQtF5si9=M!_XRp32V+cC2+EBx{)8?Dxb(=R@yxcNcJ^UfV# z4*YK%+w1b3HZk-o3(v;h=5A12(?j+CgJpcxcC1J~8Ylz3#9i(*YcfXIlNQv?U`>l? zWMq`V^6N#aSZ#az)BgE1YvI%&aXUPf7Y@TNHt@GwAsS8Yknp*FX1l-=bL?Lefp(%iv9=Oguz0oOe! z484c5UAL^*LX%W$Ld?#?CIA9VSP`S28u&7 zt)CjBMS`dbpih#`iggF708S)iga3HXPak(=0|0duwZiW5B@du%;y1pFV!Z{)d7&;h z@=n|HmEl@Sk`QncsaA@mGn~pl=6$=tOyR%x3-~+^-UWiMUD@UM8TU@E^_^Xl`SxuN z4TDZAE-2xRZkY@lsifC@a1Ed6r(P^AkgS!s88ZwmccUI^Xo&_?A24huNOa@Pr3oSm zmI*e>x9(E>te9gs@4S9~!^~uwRF$dpB1_pqm5_*}cxAY9G;g25wV$`jJOwIF`0kzb zID6nvANURcm?2!k8j&}C?qk}_ds8Wh5m%onIeR{x5%8yGxX*1`nHJ`q%vbCCEL0?y z#Pt@Yg`Qn++yfgVl;$*RhB>LI#GdCJydh^9sl=^!$%O#7H<|j;iotvgV8#7wH|IMD zN(H%JT3FEIUHLN&$42{r=p_e;g6ZGk%yA;tk7r`d2d7xP+-U163(BSrNX@r5j;E{M z@V#PF=#`SPkNkTmI!LKl?!CFNO=@|z5id#=8GyhsN1D`_FA}_xq`QMwY@3IR$nznx zVGl@#0bfp#$&&CHpa>ybOXBe#ciesEBc2r2TeavqDB2EN0(+l<)bz=>^fU6Drf*=e zT%`>H2!LN8$plszM^DTKJ&d-h1&_4(^o7?7tpJk?jc)K*-5?E6A^*0b^9FWwsBq;8>S9A zS?5`o>u;{*QdW%!H_#lOXcjQRCK_VK-V)o?s?R=7{(&?tW};`JnXnzwe%Zf9%&P5# zRBTGSMX=a`PuJQ`e04kXS4W^6^&w+5frmBQ$Is+asV%%05fwgzk6ev8H>1~!K>-jJ ziqN^`RncNL{sl52W=En3Mo<1ebUvDg4pN6y7PVRqvN7*9enb`nucjWwJi1y@6kt|# zPoPd=D~B?U$4-v;#%x~ymw|9u1e=}=zP+_&75& zoC_do?1_91oCn7>I81y;gn1M|yBrKWg2g{YP*O3|S+w$@kB4)^LJh3_(M({y+jxV%6kwrqtZOo6EbtlKhH69e1i(HWXi)Ym1-1M*h?smLn?QlH8V_>MMXCW>9 zl%ylyN5O3Et|k#Qe2~n#!;VHRt=NfN(%{kCM}i1ul?QA z5Rk7>{$akZ&y1U*T4fJsL|rhRQb7k48O3=Actt>OlNh+3^+2^G2o1v2jg2xQ>7p~M z!}U>dfsx(%#;&5Qnq$#Q)#6~vyoXhuyJylT^hsO{Fm@|SLaVP_KI;3~*1m-MY%75; z_w;`gSqzK?WF;pggliYQ7bw$v|>n|3k$pq5UKu&SiIFl(twME+u`_uYP>|$xp;jKSV59L&FC%G1Px6 zv8~@e!E_|474(D)8o(_@H8>4TdTeddMb!wYx+bkRCTv0psiVr9W+m}7Jh<;$cgX6) z2*JeM?%+3uvilxWnYrGA`&tC$gM`o&ZM`@lkLM2I# zod!cr?Sv#s5AbU^I{>E-Q%rCizbdB&J|E7Wy0wom&3-1|rtRVCNAVRwqg{Uj{qbZM z&_e*HxI>LZ6tWsmWj*-WtId5M^gT3z^70sYnQUd))_JL?%L2}{V+F+g?Aim+1kSwgr4+`D*6Prt3TG8^UHmmwe$WatSGg% zktb9UZvFX&0ILzFi8g@{7Jq6p${`8@2QA+k!Z40RZ6T!3fO&jWP>ojo+DBohJL(DE z>ARlqRCd&mIr;WuubNxo+3<(v>(QV-tCSzxT;fqpO5a zgQ-_{aKv_VO`6j9g>S!#c$`m%Im--t>BkhGBA?3bft|Hone!<}mOq@ou_;arp`v_Y zza_UdPP74j0F0(EzRg&K1>+v9ttn1u#$Y~+sEuf4$gYi(L7D;1Qrs0N(`;@+Z&Gi^ z%BAooR{(yuYMG#WZ=w(E1;~+pVZBTN@AvtpX1fkSFRRr$Eg_xqUC-dkgb&7^=i0dr zYt9w&mZmBMTr@K8{FE;2Jd6`ulnQqz0he04xRfwRWv~MUSqO^Y#(_`|g)Y;l4#9Qb zAxapt*mUe@ET#>hF@Ksk5Sl2GPD4+R)(cYqwkGC`3?NA@4lUpx>Xix66*maPu#c}5 zsC#3R|M*iTZ-_s4rs(h%$?B9=o4?;}Ds<#N+5ilcO0DKx_c;OP+12@79IbTy?_P+& zWdivOR~}V}o`p^@=SH-A64vLHt|dj)D&hUIMp|X6@eNDc|GV{H^ZF*Mc|xazs#47? zHA~|(?GAoxz3X%1vvF3DJki_s4sw}a)PR2(zWNiv?EWW4m@6-aTz?~siHu7@qHb-v z<)fh~#UBmJjx@Uv9I~m)^L4J;pupzn=Osc;^BsjiH;pxIf^3d=xq-4=bclNe5Chtb9S2piqB!a_MC7HtYoY`)nGD}lPb8xtE|(Xwn*%h&sIW!;j@w!KdF zY=6ExZc1L|vV#Rv6)A^S3SmVN!%8r$$!rN<)P8t-imwu+{AK|_9{}_Ags4BOv z{ofV^0Tl(5l8}^E8V*WGH-ZwuV5z0D~9Y1XZ&R43rmpZ#4g)RK#i)&Zhv&!Bm`Lw@-?%9nWE%YLG zi2L$u(RkQ;cob*qNq8t}@BV93@kWg9A1O}T(H*CHB!}XGY4)x^aMXM`KNEc7jf1VK z4w$llDn)%s)PUhnz^rI_TC_wY_2C_RUX!(u>K6UEQU9-ncR0>M3cfyG$3Y0 z^sXv*Ql|$X!c>at$ZhRLYOTiAX`7?kql<|e90i@qw%BpH=M?-*kwh*?f#M&GXKq44dj8-o; zr4n6q14Jm>bm*@n*O_EIXk1PmmnMgmU!9?4-%O_mxW;icz*^_XW>UXScQ&MiOl!`K zpXGy`ss6>dsIIBilTzpCS>2rsAQgOVRy6DvVnsA}aM(Lc5(C}2l4)wIvO5O8dikggFJeq0Te#Y)xdC1Tw4 zs(8N6|KNV|@-BS8Hzlz1{yF;Ad!6yIn_Y@X(g@xnd&7P2xY`YISBWGqdl!v}W3JrQ zcpjlRa6L{4t9cdabs~>KZ>BA;ajq)&HC*B>N>hDTI(6W% zvo&&v9i;cp-GUbjpZVB={Xx5olh3VRt|{OxATkdIOT$ONx4`9<>N>rektB=}@*cwe zL}JJaT?Dl%+%;kR0w!#jiXn|fVqz zaX{IU$p5xMR+JfJa&bII&Q0sRRApr^v_+~-J$>$sOvH*zZqrW6DXd{v$OnvGJxaqg zdls4sRfn1X?U%$Hv(G-93c%*Qw6PPPk{Y61we zxkt+*T?z2M^<*fSzk8=l?%#=)mbI|6M!sv%IX{L7D&axc{HZ+s+-)#jx4;qFPYCh{ z{>u)1RYdID#%cUT$!Lx7k6Sdodeu$2PqtIvO9z7DdXET6) zC3QFoJHc z5*qXXDH^nINcoRk=T!z8Gz%nQ5V$=8scUGsD#DwP zV=Xyx@Zf(-!6VFFt;YFF)hVBWI)Ufh_Ol_vik&M{fmDu(tiB+Vln904>M42JlHOkG;S`O& z-hl+i2g4}?IteK|KSV_fCWNaeDo6SpIEg|6oX(#ja=&z+0V6b8vs_#=w@7VhA1oriGu*+B_z!0w={Z*(_)+5VtiYOsi~vzzoM zsr{Z#@51srI)hEdH#(kYsE<_6Ki8Ei$iql;ja`DA{=-H?>f!H({j%ZRw33Dmp3(pL z1BQPSJmeqF8;`=T`uNWq*TXLoTpkf7HYyIM(iVRcd=73Is=^=Me-$)pX&pbKA5s}S zHWfy;Y>gDOK`)(4Yl(*d*v+zwzefeg?hqz;9TbX!5o zdt9`%CeS8A_JL&o!yOo0tpK+uT%c1J!1HpFX=lMI$H#a3EA%-*O%Hy*I_?r7`q&9! zkJR`&OZ_{XA?*V{^kW9}pBu&?m#u?_< z0#a!)6k*=Z|71si3fY0l z`PuucpYThw5Bp2El*OZQ1~cku{(PZDqho%DO}3?d?jNoWN`^`hOt+-8LnZvLQ7E1) zKlqwuSCMo&E;Ki(gKhZGj+P^hmGc7Ef}W69Lcnaj2k+kGT}JF|H`_(95m{c-f9?kp zw3(c*ju=`DOe=p_=casb%M1t5(@_g%l3ALD4 z*K&pzR}vo(u78ieB^zSWp7|&^Ao9(-qNEC7APVnwlnIY*PgDVNt`eH$h;9zKd@w0} z2kkjPPCnCs=Lafw(CI)!Zydr&CXl?nd-nqS-CkRmO^DZSnB1`&Y5~1rem;_jdz9yi z@Oz*OPv^G{v<@U(0f|7us!63>;nW2R30!T5YuwAAEd)y&K`HfZ8K7!nPn!Edh~3ry z@~In9Zf%S6|H#`j``12J%@Nf5RARBkvbg=IU!S|(DNe!lV+0csj|H*7KRv#X16LQI zyC=3PPJYovR4^xlsjB3?I-5C80wR|$82E1VBI6QE2Zv)S zS~D(>_9t1TS|!aukud7JwF=4KDM)5bUi@wCf?o92?GQ$Ygy1)4iG5_(@CqS_z~~c? zfk;xW>&9|2(6%{9vf(it%+cnMnA(0!btXmiH9S8&Hti6>8D_8{xq7v0Lk~3VdjWGh zil1>2g()6jr_fy0!`$G5V7NbBdFlT=j_%oJB^mOUb?eKKK_=$edOc_vwOl5~MF;Idg_Tc6c@g;z;*D^$2-{R*Y)%`=g=xG#p8+9g` zRE{t|YybKY0l!hvybc>BsSaU+*4mcupnVJZ0KsL@bT{6=eDLf2lgjJUP7n3YI~$X3 zI=;F`8FV8t$KnNTpDSnsG)rSnTSA5ME1Cd#*wW+$koY;u`p9 z+I&77WT=QA*{M?mr_K=zb{Uwg_{PU8X4jn{K=6|(KzDXvRfb~ZObSN1r%EK zl40LZLEDBD*S($wi8MPg?SqIdo1^D&_u0P>iC|Nat2FoashU6{e9HxY*f6^Hxt6`R z6}4}D@y#(T#~SDZT3krThJFzCYoYP~-zTg`#h~Yrl4A`Fr;0(AhmpP4rQUvVpvkpe zHrv4JH^igaAf=au^PVCPZ~x~SZ;R6+sUX0Upa0PX(N*F3`EUoN!NBLngObn@9vgJ? zr%V%IaG_hM_x0O18C|*(XfAIKSP1~E2SlG~dqX%R_O~3W+>TCf=+V5o3qhdAAsq|M zI$Ts`ANLKx6%N*;J9oYp2OEaHWy#(F;0NlWr^%b*d5_fBJ=ecPr&eT_IKoMaJb2Ko zSk9L{N!*O-`c)6dqHj1bv;xeIh*{8xHwArWvQry-vP+MGforc0Y_=iI=1U*Ice%U= z|HIYZc0yxzUKxkgq<95xvPYf(xm)_t!1BKjb`M{BHpPhJs@p%b9|Y$9sh}l2>s5bp z_$E=KK(yOUx`o47w~idr905)8m(AH<(_PXo?fs5ox!@xlevkYVXhTc&ApcBTzx4$D z=PG1ijagWSNzZ%KDYD$n1<=l{#Ubo!GbSblxmeqYJxQf_ZVO1GZAYu?+$R2hOgn_d z#Sm`b>g-p6spfTD2?*pBM%2e6(?)2E8R0YqA5dbrf>6M2ttCk<0Ar&APF@I}O|NTe*|> zgy2Mz<7CG^Xp04r$-u$?>__CM($k1UC0>(Kc!+k}zSbf*-&$~)Z%`qBX=Q1GbMrqt zW(wVX?598@Bl4GlOk2})7cq5A(K*)o(`zcaC% zRcmXU{j1v(GB1Z*F|=%`j@`>@S=wy;5+Vu(KV`HixSE(rnSwa(fszG^pBlHt41mJ{ zhzXJL|8a}2s`3QXi4heDazIb5-ztT1)RU}_iB^+LU`_Q1fYE4Y-CP2(LC_ zN?dl=%WrRRbgin(H@w7m@tJu2yrZ_P>?7XM{uf#Lc&=^nTxuUs3xlGNDMGRl8Nh+- zsy&_u_rXG*Pee?CD(F7yKOx057>_#7)Q4`Nhy)e1D^fVC2LuuY6li5zeKhPqjQ+dg z=Km?HnBg4k=;@q(6~AY%R5I9Si!AZ1l<|W;;((Qo);iC$RE2KnEV~#kH#{VNc`wGT zhlh*L`{Q1=!}jMv+i~HXNHz3Rxc@bO?lWpfrgjK}3Q@yXS)~OIeim6?aNe1DE!8O0 zu*y@r8;f%uyj9^eJ!`ds#&$8-KZI{@OE<3w_U;4i^Sa>@SkRV#fi?sZa1AR(eD&L9 zxK`|6SoR*>dzX}CHakP~z}WXT68+S6L1^d;QdFfK45T^247t?VRA^pR4})YLOCGL9 z;M2)`-yn&=uyvbf?~sD65V)j*D(3o~0a7&PzYV>_XQgto5>yG>5rh_m;w!mT)x`%J zgFlkU4jE&`t%fjZR>pRm@G_yYOfUm3Ry8+p-i`v0VNlg{)cJIzpQDk&;ytDN(A;CcZ7JH>iN$$GN~1piz4~R!ix7(vb1E1*=~HWbjYQ_uS*MpfUsV6> zVYpnUO2?2CC+DZj{?vrG%XYWd&V38oj1EyDt#HKvV5j1Bt+Tai9AhhaQ@( z)XO-c7ht_XD4!?Yjt~FxTMK=)gbI}bhiRku;{o16&<6#Kx6E-AvxW1)OHIvPuDDc^ zkh*tlb@5j=Qr@rMd3MJ*s*nxwNWh&PU{9VCvXuPLb@-L^Bu4}Lky;26MMAoWk zax6?{M4L5(g+st}2EX?GJ7!fP0yqQ~yW8~iQ(jv`x4X}knwC~pg96MAaCMx*<0(*( z$@#4g?J-NEY2DfYvyEJ=$&)8Uqg(n_P8OT`?bkg6`k`G28%oh(;Oc*m~$^H=ms2gBtnn>bBT)6K`CgQukRh8lep=Gt9E z^-^&bnWLwrJvOsh+ESP5*=dZR2M4!|9|E7CasXvAk~7)<{kx;h^*qYt{Jy7LwEAr` zEkuZ*fs3=1{H>X23Mtb`B|X;;4lXQn2#Q|d7(fqS&o>WCFGKko8orO(!M5pK<{%LTM6sGZ~g+Z9n0^YR6OfF=xk#0TGoH^0ldn^QZv8& zhrbd9JTB?TvAP5Wr3qnNc%mgB1k7i5{0Fema|45h`5B{7!9U zhYJd(&;T+NfCI{&X3najs>099`wwOj%3v@=2#Q_^*Y_j=?e75(jvc_KaTegHB5C4k z0+h?oS(jbPQC9u9G|iiufD=8YTLd#p+KRyZn|K*`$6!K)%3A1$i%U@bRQ=_*|XBV zp9C+mPYYvC?-vO^ga8?il6jU-j%p@9L;K6psGPJ z=1B)Hlu`xEETE(8Rs)ZjJ564f!k{a2#HRacMJY(;=*r^a8}_G?-Tqi31=PSZuB)L6 z*2Imt#Y*O8Zy{69Wx3_*z>oc_#e^NQPlf-s1U?Fh(6UrDlvC+T*4i%952@KY($IZ8 zYNqfT_XyY%_pyWo_!f?8Nu-eS!UiB>)Dv95!af@&+%K>HZaC&lZK}dwPLk;GH*6uB zyHRwPE}wks7V1@yU~4F}5_}^`>)3l;CB<^U0~PM8?E2RXTe`Vj-7VK3m+9yfbXYj{ z=3@%!i!{6EZ@kRtMo)G@13eta5>erBa>L$vbZb@IzU1P{Z1qph*2_HGS-+CrPI?W# zpn}rCkBa=bG|kvc$s2#=Z}{~m<)&}N6#X7yN=*7;ZdAnZhA4ojPvz!TxY^3Vq=0~w zaaWw0SfnPL1cUX%i{|r`2zg=7kn*?y-r2kE0e<5eU#r`n_QqFN%PFfK7L|{B-ZmI* zT9ptDR~cNo^i5;auckiUhE88ml2zkn1{*xg{QN!N4qyHL8`UdoUtz-L{r0|^6fPCV zXIyge_-9LEjGAPD9r>kicf6YQ_^mwib|iW)kN zBkt(pJji{@)|1t|{jfZcK5%ij1=4sGzd63Kn5$H!>~aQcS*^~uO;lk48__v&`kKM! zB^M2IYApeVaCSY<@sZM8+ln&$HQ7F*@i&&aq1Ru2LkHMRbr;dtk(-_+4awJ}rTRyR z6@wF9 zvRdZT&n}$Ty*RkarYF14a6;khM=l@6>huWKX11IVO0Or0cmIk`>cpZ<-Yd3RF_3;; z68?aeqxkkDB)s@q3!98|SeQ&JN6%BiRhoDS&n zdEdaqQMpuY{LvXh(fi{36#7-pQ{nPc_rGcKpV(>REY3E1=CpsVKqH3v*wo&Qx0Znh zos4roW^c4RLBQZcJJ#XjGjt796L45J zIvUWhCaeHVSz8ATFaRic6C4%!(J&rqq3G7o6CFp3Y8#C?QHuw>xSxJG-M4ogFK}Ho4d<+ z?T#yvIIlQRcO4O^kF{e>`W4=?Ur8}&c)7}2Ob9y?zsujvOv}dklNvLz)LbY z!jKM3Z6^9rN4{Ic3Q^YOV%i)h890B<-_CF5PqB;UUAaLLMM2F>%|o)9G3l(ElwfA_ z>>st>xs6Yug8 z*da!0^(5IbL$z>wzjw(=v(<`R3jO6uR7&@m1D!z+Ky)I5_ddF{kkPX9r7$B=09WNB zj!AXx3!D%BiMvwm79T$RitQ%RJs=n2eL1p^*LE}Ayt%&5+)Wio@%mGeEe%iOsyJMs zV>MH$f`wH#S^75EuAbKmlFsa9u%D7vJYx%Pj&G?FB;3?;*yuuZZEVxhpH}zhT+y79 z(Jv@_8Z%R5oII2d{T-KL|69wf47GWck55+A3@PQm$(#&(6lXDB;^RAJh?MN{4EtrPArd)CHZ#ltRpQ2g9$7TS%5-Hnp%`G&)1OV#t-K&tFQ zkjaNX8g_&V!Ig!uAEwZp+_}SxfI4&ZCDYeYzkTamDlm%jHH)?1rKXtRmJEHC&yM0& zY1J4C&X>lgU?&+T(7zaF_VGLj5`5mXlM3KW)$yAkpgAFX;-KZ>0K-ybedezR+0M|#<* zr3>Utuj%|M-BSWhsJc0^5ECN}+6Y%efPvyA5Tpup4!}8K1``$p1v~xJ`CWBFo!X9p zlW(?W>t>3tYhwMcmnQFQ)eQS!qxMQrf+>$Y)$7Xo(8Zgyx?tIEQOGsACJ1WU`4&~N zGP2w6{XX{K;Jtz-CN%MHBWOElH+l@u8vze)8Ye{%5gK;6T#Uoc{^{Q1B5I84ShV0| zG5pfK^eI}^LL?y?;~4GvL{KMFkL^VF=bHZT6pQZjU1{JSgx4ANq(0Z+H_+{b#-;L) zZ-BN>(RJfG-^APAcq>-1zdYqaC1f=KrQ*B19Q*Vd45NzY71-<9Ou?Sb7LLB;?8$i+ zCG*+9q!~R$62s|IWBBx~XkrGcPsK$+Iy`;g$z%7LPhzyw)3`h_TLYffYric(+7ok1 zY=ed~t_ap8sMs^_W!|ro7Jva2uv~u-!|fR6<6r{J#oh=vvl#@0J4jG%Yf?y#wN6EX zFeM$}hv1Yo8N73v2?IAT{Dvo_7(p1oVEKSZX*7(j!R$D@tqFPIy-!)b3h2l#{fIi< zJ>v$=KiKJD%zEv-$PeW+tf$=KikRE^rOCVPSoh-dAmy7jC_gYpDZ4N%eR&V$kx1XM zS=lxScxTY@$teMN4%~ugU3e+p0-HvogR|4IQQ68c=%v0*S(N7oJ%=VKL7XianFJ;ZCB)B zt6T9+Q_T$wQ7qEUglW?g!S1Q%cBb?HUO4u)0?0(q$%h{@7%MB6s!xKQ=({5g0uWgZ z>Z^O<+$r+9uYUS7GIXM*zCICBP^>7|5EuRYEK0cs`+dokt@HQvaW)+ekg#R;W?*OE zU%we=!6y>!QaM!RXr`%JxV0Y@V;=0XkQG=uqR0>C(&+c<1@oFEtW$@;Oq>^Oee=hN z)z{U5Tt~y&RrJ#zHOb<|YLEJObErQOp@vgXXS_~Jk7+gkJDlZWl&uGexe!{dksL;} zc_K}=5meH24gV6?#zvcm{0)~|C6(i99g_>7Zsf7*20+}4<6HnVebx-_U7 zK*nPclJP!&&Is=;Q|HMK<)Dmkc=V|xlF;N>bG^z`r5dbin|)MF+ZYh-GzK9Jb z=NapOTu34L%j3;=FJgJYY{nM;kv7RKH^t>ilG;*1Bi?GZC!0j~4BWXCHe0`)LRwu2 zw`^;~p9j(ZS1zF20b>cKnV{D}d@9jZJ1_*e3EZxZDL{OI{1H~khAw1G!nqA*lRj7I z`cWY{f*`<(*@hILWD zF}%DI6>%tk_*cp&Qf#?4I(QajW>BB-YbAXpPEBTdiupzg$#L*Cq|cG>_|(U%=pey~ z3`9VEQcQ1ju;6W9@5JE;Z~{vf?<@WkO(x&d0G(cC6mXYe@x!Tfy5Jug3|yO?*Vi(G zGu}W@b~-hp6JrC1Ck(ZrUx8MPac~e3|GnCxOQ{xj8YnM8} zO0B|(E&(dT`D&zR8pPSO&|epwH#^IP<%C*jiUUhBTpN|EnFr!Q@PY!3VFgU(6$Y+5 zR}Ap@XWJP6?eoZA9)#0iJSOZGG>k_`ZbLZnasxz}OyBky!-H+JDdM9m-m-pwi|~5u zaByoSsINZUy>8wxd1K9arwe+Khzk#@sWn#rHP&R`5l_UmvOi4O@EmLo@A_r4HOGEp z=q6z5Cb$gdDG|bL9R54;XbWY-b~9I`8)n$uVJh|kKk#dBRSY%0mP|Bf_}{hM)j?eU zpj4_Xui@P52eprvf)7bI*g`6I6AP-4kuPAatQz)hW`0PY;*}Kbd`*~;?^X$fBQI)l zsD)o>e^X9!9gH}?2Y7H975j8jb-PwFUsCfFB-bEifKY#M>u`*o5Jfn6)rGaG_(_s- zS7g}++ zw5}s*$-Kp-VNYC&&t_TL6iSJE?7)B9PgQ(JhP!^-6q!%I6&h(_mD*82LYXAj(j?mg zBgF64_2$f1`%DlHq(*j@+*g;OgDX7s3A;VeRdVBuwxO)%EVzG_M{L9@4-RX9lSDs{FEE1X)Lk(HAr%CamNp_q5UNb9OO z0!#up2*hc@Fn>diRsjVaIZmy)R|>N-qg&MZr0VQ(&~1gE7HXGCQ=_>AKyv_T0Ke9m zk6k_k_u=b6CD(4VSj&<9$xHn~TmKG!Q1-0LA6vJjfo9;*s8Y%A4{jZZaUUA{H@_-r zKtMC#s(-?EjsxW7VDZ!V2E$7TB?Il~CQ4ba3;(X@2S49!$jbvo{92R$!Hv~QpY!1`c;m2X-%zP|E2qQ>A$$X&# z37>eZf3?>%mVu~$cUyO}qZ5X&fN39VV}R~yD?UN?Kl5rx0nEjw>nZb}epGZ-DUQpe zoG9-wC?>v^oGoCnKJOvBttbzLhPt|%zd-ttq0)C2T zHHdih+gUv8D*?y%GSMR+;pLRe3)5NK{f|6;(;Tb-$Qc74)gT5tP`_Vugva8$BJ#6>i z;X%@0z?;e{xq8m7X7LV^3b6(uL6Pw0`K=brM^PlC5ynN!CF)Zm{PxZEySn^>?Dre& zoi@5k#>xQ?-L)=Qj^i(PN!I)v5Zh%^Lpr)QvPIMsi8yh5HVTX_0CM3oMr;g6+8U_b z;*Yd(U_oO+j#-a`dE>5FsX7$>!HoRTfd0t23P3m{nE?cU4KkSnEhAEcs-i7Mb3L79 z>&ul7GL{gSa<#F6eA$4^UB<9`@MO>nxkZmEoB*`OO07{>)u=}v_Lj`4^WDAdc~KBe z4a_({hXR7$i~3=#(~GJqxcY<4_M@#B`|y*&fwcgKtTkV$q=w>NyVbg7|A?XpJK3l! ze^Txk)`T*0QdNHi)fPhB8l)c$pJ$OiKelsQn;Klppyh`S@4Fm@(swaYp%YY1yfCK) z&w(}vWC-yBo&U~Wcz)mffSI6}?WVFC%qgLYGK8x0zY%IMVt{TpytTl?cwB7M#P39* zl+Enj;_7Jon(a-0nKR{!3C*;Ui)W4pW z1Ap%fw)3!^fH%N`IZzOGDs|?BY(NbZ(tu%>Smxeqj?_Yvqzz;t->hyKqs6FxXGyPIi7EFoQi?j58 zL>SirhwphvDFt6SqM~&i`eO!_VbmCl4<4@Z$cz9ns?Pf#a2l<)Jy=U5HoBK*V2SHY z=}+M%90t-){<@G0blK0k(VTvOocF;dZ3EDweZ7N(vUc-xSom9~Um>x{4n4ui9*v?- zJK|UY0R!)0R~rAubA=D<5^qJ=Sh{&7;yWecAh*%=Xb`<(v-X{l3C z>nQzxFe{O^9>b2swx8X)c52L_+m-3uuG|fz3*e6z@^1ontzNYY`@MVj^rx{0WJ6u< z^?2Ii>NyU$_+se&#di$o@`ThPah@P>a!uV@eztd6R^hj4MN-TraTB=;7DhU*^v>h0 z#soQn&Nq66HV+oe4`xoyxG8gqtP#JsLjt2&O>43;nv2z$amoqU9Uqrox^fv-$CP!7 zN4w7dhGP$5(5Kb!>L64iL;-=enAbhrfrmY4*N}o$6@#nWG32{~?B12D->+AJy%scj zAf5+aITE@CQsyZrqfbjgXx8?$dwp}hSs&VbmJPv6yAQdEO=$*yHBCr?x<8hOaFR8} zgAocPiRkEs)S;$03>^!KB`z!iJbP5vubIxS2uCb?BqIWh{TX?wad_ca;J!KjOG_vL zJ;sAU2LXb<>wYVN^a#lX?TNP>7q*0gQShOPpKDV|<<9h@s*TOTs<9wrXF$k-(utMU zA6kXxfGrSEl}h=UuJJuS9D!9evN4C7we4JrSoR4Z98~yY7KX%o6%(RZfPtaP^#aF& zXZWtkzj8m0C4)^w?N)qySUA^MVVP0qS~jO%y^Tmgxaj$I!BxYE(P784oN+u8SV;UEzIoH#Mck9W=k!J#jUoKSF+95l(-*yu)>!-s;C(FBFn-JjR^^xYWQx4u)Ie ze^|;e3Yu$#Q0J2WDnno5bbps!!2qj}s~y~?G%Qv_Xs*ltQ}e{jC<8(%nbm;{@&Bt6 zDO@{#dTL{ntQWKL{DG{yO*Kv6mTvBAm+D8 zIwV-9T)a9K{OL_e^l+i0x7caB0x&$jdK-2~U7b@nM6!2^vv?fzO%x?T{j#Vs zZdj}fhd=CHjcnteu&PZP;t&&e7=Wy&(IKD9e&jH7yu^IDc(8SHBryZ+i@A(!W`Mi1 zk`6QAem!3e-%pqzBRQ8Gp5T%)pMm&nOOI!vd!lT3d`t{L=5j z=qOP(?|Dh0FRs*W*@6%OvTtynNUG9bWNAX7b8N`7FXbp#88B;;3uRrMZEM-1va12g zxwyL(g8(tNDya9EvjL!rojwOfy&hOdt_4IEaDBn!=>@ffB5NG-c_Fx!q`jPI}>L7 z1?xn)nqfHInyeCbB3 z0xgSFff2~BY?+%g0Im!Tsx?G^%LC;n0xAS{M#I=MV}fYIcJBss7C}4+s%_-9dkE== z2}og@1O5+S(t&LfxM_b&F?41yJ36AOASu=es|g5=NK>x@Le6UsH%f41hkwbo+aA<6 z=1Oj`t4bZX#D#NP0G{P6y$`vLPTBDruy29|7(C$h%lJim|Lc9OI|yk3(WfJedmk647#HBEjx8c5tTr&|Yv7lJ1x4ylQ zqXeab4)k9a5)v8H+5@=+L_~r|IUD*fRKuMTDo8j(wI55SQ8v*3s~;r&nIWC~iRSy^ zAJYsY+Fh$Zq!ovKc$SI;zUy6zE2<>#~2DU?y3c|U1n?&aV>-shT-oSdau-2LP> z=)O4)0d*09fMfUj2)o82aDiK`%{uuJ;*FXv2qAoB{P410Jo1hLA;uB1{y?w;)(Uva zNAblW&8EMDq2w&_nd01;Rr6N_9v`F@6&g24atc9^DCkO{rw4*x^gmG+`ru=wkX1O_ zoGJmR3y^!gRXtf%kpy+d#h}*v#)f_Hl})~u+P{HZ38X;&wzC@HkHE7*WUTL54WgL! z1;Ny1I3|Kj)QfK7mAi@0|C)CN%ngvdj)FEpb2stlS;-V)6##C>c|^@zYy`G`Kzo7q z?&N!I4vWhXV&V5H2D7ylr^{dt*Md$CyAy5(n*Y2$h{a7jtp-XO6f=daiHC>qIrSa7 zd{hj-;%Ia{KUPqK;TY^}!6&+?m)q>tnK9%ZJWpL;9!^8{c;69Kof>(+%#>}QkR!Fb0;ftIFDu>)xlIPY6my&TxYg3>&v z6JU0O994W6ntN11$W9E3w(Hl;oSgX2-MtMg-7gSGRrUpvMh~7MDk~R592_<{XvcTf z52|3lIVLY`k1V=DyBAt02zEyjpV0Nu@4yQw+~KD_(5+u`h0WZlD(jTNf78N2B-f69 zmz*qvVn$T$bw)TOYnio3j6I?K3Bt}{k5z7~F+P(J?fXKpB}Ha598BY!$F^k?O7rsys(SN);?z)*&?d2QKzd zHPS+aST2X<%&lR#Lc$t2BXzp9wCG{c(v?Y)Ci21ZvW3G>`_5i8Lw$!e{f{~Sl`D)f z1gHi$upv)qW`+$hIEBxvbH^PAyYVxD3_4s{b9dq8L^$r{W!3k?Ho$a|VKKhRXzJyLqz{N= zScp!X;m|?aOjc5R`jY!&WcaUSevuKr3ugO+{(JV+^z^JVDlVkzdD^_UFT-Yn48q}{ zf1=30{36G?(4h2$VIMW!wQg1Y zUJf$(O}(XA&a1_~=%7XM3VFp~;UKT978}J;eGJFGEZm+wV1>6q>(uUd+NXNLSSn?e z0XH#28~O1qheZWQms&U8C1SW=%TVa)bMMvR#i~f_ukVVla zIWJ(cS4D&1IV2-MMECe^A2s#0n-E}z8QU8rNLGw^aU%CA#IA`D@-$+&uFGpsfQr*3 zx#L+3DFq*iS&2=Fpn$$K0C<3E5JxM>FN``ZGdc1cCHN)gA!sSBQe2TnWAXvuOwgoljCS_0(&F*K8q> z%lfQ%{_e8x3N57qs{2YT#$EEO@79nKcf6Uohp>oWE=zi5R#-N(!a%uNS69Y}ZI1P3 zN%6ljycI#3O{3<+r~tK!b38U+3i}(QrHWvDCZO2M`u$w8VrLENK+{?ADPu}teH%fj~-|pdGCmRM@|K# zl7U&*!=iCvRoyvKusK>}pSdCnjVl+Orc&F78)wKQwizl$Tm($WdG$D)JATSz3wvij z4ZaIewDn6J$g_q(-|CDW8_*e14wk12J*^$E0SPwXaJ-nDpe+p@GL!$H@(_S0MD*t#w-OJ|sa0m0(#YcmV#|Fq@eVN&jPBKqPTZs4 zC0D}4b9=<#(w6tS+Y4(zQsDuJSkJ-1k?#n)7Df!rZx{`_l=vwv&PlDv?(mJhv6}gA zM6DE(;jkovTT9)leT|gTum>S6DL^~7z$`{SPmgiw;BvT+^=vsW34j=zcme2XS8=2Q zMf}@H8Hym(441tqkK(z3)gMWF7TX0ycS~K^r!O1APn#9nnlA+)Vn z8=iAtvIbPGE!DhK>0~d~FFPaA0Z@s02K}fg#a7+mdh-x+7rynTR9p0DfBg7qCYBw^f)b@;^$vox5ng$4^Ty zQH2f@)Ip3#oTKc0231@pta|fFc*Yw|J~fgo9?rKM+?!n1m;jvTeN;^BHP7!oHHw!2 zUML8%I;1k4u(z8;6e48e+sBZu3iL?g(_F7wCA*0NP8ASdH{F(}mul`nFf%u@!cL6h zK9^KI7f_W*F|GfCESiI)CCJ+SE+<#D@q17hH-#(p>Q$^xxe1c9i4yxMCi+8OGbT{e zkX8))P)?XB4SXDvIdi6sCDhYJ}FlZDKs}5Yh1SI*+Q>O`DG@AuMssXl48RRKCKp3R&_Q8DNC zf4oih{H-6xLn|M5rQCKj2>aP{fZ9r(BMzAB&~FrGr}x+tfgzWl`{3gK?t_N$Vz9}= zwq^EF^h?snwp_ST6$%E%PRqkv$XapeXp6O}ea2LsgN8O%O#4(}*chO;OT0D;;Behv z3Ks(i9O9JbqBU;&SrBdl$`?3LoPoa#5=B+GHebP|VAxsH0wzSCTW-Uk9l(VTG1~PXfYh zy}&H8HRMbN>kLXNf>iX^n*{}zY9~ky1}$;L zh8{-$lp0KLPpJLVtFu&tGYXe{%7Q60iuJV=@4BZOz4jfXN^tp(OantNGG4xqLU4)d zg0TQ7pPqi2?6*6+U8js*7e`|i6uYSFY;eM=CwONiK~?BSz6PAe2(}=Wrh9PPvykSv zfAg4XgvJ3TViJ9-<&+GH^y&fA6a ztz7g4t5n&lht2A@>3kRX{MIt*f7-cM&O=@zw41kuU>+aLn$n{FxeZB=bnV+uPO_^{ zNV40%upt+C_eK|BDT3^nzZMvs`wAxk{VBHm^F1T-V%E~$d}(|Cb$~QFvxpUiM~_}~ zvagO4_Hx>d+vYSj_K2U{jqvqIJ9tXWvo%`2QhhkM%R~Hn>Eqdy9b7b z<|%EiP5<+BS*n~?)Cl8lulO65Egp$bW%ge%BLwMnN3m!9%Csd=Z`t?4i0VWG}JDgLp=l6Dx9epc}JY8hb zg`N%i!*qhx8d{+99}Nl{%=hlMh1m7?k|)oWwh$YN&gn1xXugVZ z>|xLT;Nhpawi+hT`ChY_L5V*nD$Kl3=RzvxJ)9F@TpqeUYzzsCNZFd|c@rU3ztee! zz`1>9slXR!Iu#N-lc-33=05u6khk-m*ag{c1>sBE(x|WTeh;kkgXozDxB&ef_BO&1 z>J%;Z2mhSVM@40u8?T(C|4hktNjjHFY4^wUA5v*@bBie_UDR={Jmwc^=`2cM{46x6 zLd9}EaO&X0K=c~oeuSQzCCuPxTz_eRCJN$-fy56>gk||Hb18(9nnj4kO-(3K)6p~v zsz5K?d5${QdAi-`drOaFNGyNpsr;N9t^9D-hjHl#yp-wXy7f!?8z!)p=o+l~?qRDwwznY1-q$1^S7%j~q|mzCp|m+=yYch=9~#_tWCK zsjq8@S&H4@bcBiA#h|Z7X*og~dq$ z?c3DvqEZQ)^in0C-$>-sU8pLNusv6Xs-W;5!`{vWP# z@?FBH`F+6B&k&!~Iq17pdx4SBw6VKuZkC7k`pppOr2}fWDipoiVt0)0vB5tg%w|~vj+x)S8^rU^}E5OBfT{wOI*u%xE#q+3k7U_&nCkwH&k?oWpCC+LIMI93fx4xYi)7Dz6Lz^3F zJ>_tcOs6;a4G|4xJFhx3wW))I6gv+#$$Sva1xz4V5vLi5P@Sn+TE7@bqHR%b%0bea zmu$x<7h(Dbg5o#hZx})Vwh*Ia(a0tfccoFdV}z{Bkv+bxrEW)qL}GTxnbvAq=ieJQJLYk&m{0FMhvxpT9gl=7uKoE4K5|T}t-#?_6d-2&M0Uq8P%}y&Fg^ z=?5X73!2zW3le9DHwOYQ%4+BuwUvWe7xI8=zhK(p9VN*6ltgcY)_JyM2rG)tct_m$ zNsvf1@_UM@W)OdnJmRiP!J6IDbI^&8UdDpwGF`o8nw7Ybd5`e|4W}l*kZM6MTstkL zGhK`a_dNErrQiARfYv9&`k`AP{Jp4xR%7gi{EN!;SzyCKSEb;mxq}1m4qfz@4$5www-3v{!gLZ{I0teMFgm}w`+}DQ z)RJ&ZK?n+4%;&;CCnh|k!54=B0Ad$7uZB{^1rHLN-${h#7N!IDX8$=V0k`D6 zJPt=YAUL7jsyki|0B<27`Jp7|d{8rrCwH9GDd_EXd8JwW50(5Wld#3JB(Z0o>`CV2 zj8z*N)W}xpF>Lx?Tf7>SkUPKgPM5zS=Bjjo5mQ`rm-4l3Yu7)JSqVwf(H#XNwVIGJ zFDUqu{uNc?SDT`*sJUad6_9B!Ge8r>hs0|+Mw_L5a<{JIT|bvWg=PEuny)%>L9KW& zNlQWLjCA%^lvjc#d%_zcYUT?n0xPwXwO!q=qQl&}?632m{kn#jEqcvOdA?{0a_O_I zNv!QKs<1LgqQZ=@f~(#vCRSEfNDhK}3&L-WTRVnya3Jpm+YJi!ti!j3TnhCVM1glT`>Ar%KnU-~GpZUDthgah%6_9Pjt*{dzs0k0;dQ{hIh)Tv1W4-RB)I*N755NJ;6a zq+-qE5gKf%q_W!F#B5Zp(Z2WFG`dYK)>B@Fo9Mxpg^g4u*}Q|2AQRa=n}JT|bIt{! zBvLh+hMj#}K(iL&s4cp6E*#+MRiK`pZoc*RVFY-wjxksQ>*MI~Je9_Chb;To;&NEx zy4uvFpLu(i*2LetheELp(&EK%s$jJCfs6tV{~^oV=3;Woz=j%q=J@Zsl*F=E<1MFz zp=s^7h}jrP%S3F)x$^TINcXx=;^~9!S#W$=%6(^rnAM5&G)Ne5*_>9_45)JREg5sX zp3IPXlWk+Y8H}*j5KzmdPb}E_%Hdp_))4R2=x-!fJqF_PYM`lsV)kCv-AZ+tVbo=} zm}aYMUBBbbuQ;*Y_o4~_!H>M(oE@j4Kv=YjgO(Qb4+i;m4{VfnvCy04$BzW2b~zs8nm;DhO8d0_3V0qpsuzA&6O0wt-_hQf^fEph{Ky?Mj8Z{E|3&H7- zK}r!?gD(dl3kYJw3E;KkCD2x6xj zQ#9;f^rAgBAImp1D}Zz8!aGufYVvWu=&S1G(&VyxlCSlK#~wKif{rV|VEh4L@t1}V2d>%Ybc%FJ_nUE|)2FbkrAEMrXs?|aF^sElmNI+yY zM}i{b_lRhFPD`P>4>^Fm1frt;SO?#=+NyBc+ssx2ZfpDE^XdVD1fBB`ebTW(ZlK;? z<)tUEL=K>-GaM2yw7}@0@I9d6ucEwAGW`Xmc+(FCFNzX54Jj@`D8VH_DL$Zh!z1__ z(??TBG9)^T&z~GoGg(};k<2mDykxv;=CGVe`Os8h}^}#dpBUtwAh#O>HmoOC8$!B zR_6@%#wZLeb?hAaKbqb18x0E*77K0$(M$cy(z#?0lti!>o;t}*er8tr8n8`(w`6WU ziL%?flI~KnFtdL%ebA(P4%>#DN$@fn?Z* z1O)&;Wqd-f+J#OkFzR|9V4r*E zkdQFxDMkmCrP_$NE;ZIion*LzHe@>S078a7>@`*XB$*-OC)fJ#romPGe_$g5j^L#eG{VR9Jj zS=qMx!NBhAW4)ax-p+ZYmOiDkHEumv&Y7#|&G+J*jG@l}x59gzo(egb@SpX804z}I zfCK<^KP2w+8-O{8R}_&ZY&Tx!#O8Fy<-JF0Cdek*yQWIQbb$iqK(M0&ry9%*Daw4S zojk>bUU9>rYfZQJB|@;XugHg}X;CR_V0(BhM(KX;T8cBOWCIJ!SmlyqG@B|=Wk~|g z6cVut@_lVia8A4|7KCd>bE)`;?E*H45FPcH?N8N4R|{I7WS zhsd3kW$Kt8KKrAUBg>0E4OqH6wnBY7KH}dO+H-v#b5tp3$iMambDWEMlO3Rk4E%qHgnRU3~EoDcL_vuuc2|WTnjsBxoMXm zbP_-qE3FpGz95JkcazlqdC1@}RWHbX-SId2=hAad=i}1dr7F5{_(g}cfHsmM4xBc~ zIV5yGRakIT5V0ZL@dhM}cCBjg>;!Mc9}gD1R_hYRu_|CP>C`kZhk#}Rd6^|1|GU@0 z)fXUFuNZ^CJJ2P7Klu7aP#YvvGHO*4JExooN$TdpK;|H>QRbJZHem7U!&nQY1ILFm zf)4ODAlLQn5X@+OwLWo$vHkgGQ+-IS@_+A7F!G?OBpp|_Z<(KLRnz_FakGsv6-r?O z|1ictPZ8U}TKS`20pD)V=M}Nqm72Uc-4-P)mA_)}W=0T(Z2wgZXN|26EIA^F;An^` zuzgMS6KOZzg6v=#Y|u*M>g6@Rxo}6)l?AueHseq-ISxh(HF`FsUh?jwirS5Ry)Nk- zGHT!)MsLE5j|Lm&o%+LYia%>@!WSXV*gFQ>oXbbg;C0vARPk-bwd7#c{ zDjzdL7Y|T(P7Bi4ch~by_T9@w1|}!B_IHO1R|E;{IAdsgFkNd$RdD^Zfg~AO!DU!4 zf!^J5%NiT@sO?kqI=s0wR^W4d;8(u%B0QjLXL8|gHs~spRaIe_7~QYI+-J?-&$r4F z$&lvx8DKxzhwE}Dk+iykDB!U>N>=^n zZ{tDf?(SIAX}RrUy1{zEpX`J+|8h3ip5d96eB=0SuO*_*5zD9X2Qd7|Rs)6*CZNOk zj7&MkAdQ-?H|v_|J*szHcoo+7VagO1=IEoRbb1i7vxWS5pF((gI8XJeHA!Ul8Uc_t z+BoP#`FOAC&lWrk@R1&fon;@R)5Vb1Ac*49VNiP(R#CF7y6o}b|2~kUSHVXC=L}Qk zkaBcy9p5tT>PHW81TFaQWs#blc4E@Kv^~nrfMSeA>u({z2&Sq&F`520*eP zOAO#MS1?B@RhFJrJ=+ya{@?tR2Ah)4Z@=wP>Wducsw9vHUFE$J( znMp0xPMp5}-(u9F_=>r7cZR`NC1@1))*=b#BE7i;)sP+M2nh#s8Ci!NX)KX{LUurs z%2Kn9(PMI3HF{ipy|dwRdaAmAsDmdA-{Ik9;r_=Q_=4=+&hFv}rNZmPpKc6y{#cpN5%-2y z(t{=YrTM)pJQoMOY|GXY+HJ@PRs#O+do)rmjWLaPkXD_HX&mYfw;N0$}YaGa+0^P(UL7Fqj-e;fVom2OuR`CSywW{l7O4Te!hBICIdbO1ogaWw@!d z>G0I&>m*vek40bAGAdJ}seR%JEMD?{5w<%;7Mez=xEt5x0_uF;Y5L%JN#YE zm}zH=$H?Q;jjD`=pvBs+CHlmW3n_~3x@5Dm+z8)GJK;~91`LNQ4D-MEl%=$^vL{|1 z+G3^|)6i4AGhVtB;Fc+&)n#R68ClC8Idry&pgpGWwbA z3lXbuo&qS+;7(|1;rLFySfIm#{q^6akF2`&Caf4#xFWXF<`Mt-Ss=|VFY={49NjUF@J zl*cMsS|l<+T!eQK5t<`WD%6J~9eWUS;{gX~zadFbX&ICPJ)msZWOZ3o=HDObpO6oi zr{G5ipS4+MB#W*p<-eEMV-u47N-!dd9-t(Ew$#$v4{h4Rg|==2uZRdPphkc)i5TAp z@>GSWuP(6sdlPB?=wYV$OI4H-q-{O#FQhx`>heZ)&12Z=3*g?BkCq0Kz?+Ed>zKA<=NK^|T_D2GBd!c13M`06_S^ANOa#b_5ER3MtI=voFZC3JRy5 zrC9VlcqGo!c;UPNR%po1(;tQ)KR5*BTLO8y{OH@Dtep+8GyeHe=U@EHaN7c+?2Mqe zAuQbgVa@{1lsSZ8kFV0 zEifWlxcmS!l70eu0h%7jK@O3+L8M@s3Kr+zSJ|)Xqs$>vlcf86$bqzaNnPSh_WK0bC45c9;< zd`8qriBLTQ9uvMr2+Kon9;E-@=QiOoY;^y*OE?Nx9l6cGF$#+=V%ps;erQs{-2u{yKqL61lf3`8l2^Y4GP<+-$H{G%ied(eFJR8o z6E3E}#3fVk{iCFM*uVdJ0L&QCM{GMi`cI1FSsuvUxEMT^e{>6QRR+n@eM?ui69$)` z|L14QCUfBdBU(#qxAvVS#JoIM{{}{J44qCfIEaIq7xn|hfR2RV0PBGxfe!J67A!kP zncBIt{QJFfYGpry{S|)GGy}Yu(CUsig@Se12?AfZ@F1UtPZ`7{aEPYChEh;}xZ)k7 z@}R9gD4xK{4?M-?TFCfhd;Nbft1XI=0G`f1>CQi4Jo`Pixgb1;;$bSLs0g5PzypK2 z117`MhvHB=W1cHnhNYnP9sC`r9)K&v#IUUy2QG}uHRt0^gEy)Fy$h~5#F9+YUtV_` zaUegr#o=xgYKDvU?*&*|LTEiF&7Hj?R>!J0sZUycrcx%47zblw*a``tg!lwHDv6pE zCQvG#QXpC@s&!W^7}Y>)+AX}8XxHIf^xtRjy^ngsyr`&~lL^2uHB z+@-%d4)%B(cQUCO5@TI)R(p4qV}Jky*?}5PIlM?6Emm6_tGXr#R2ROs>`Gvt2PU0b zthGV-)Os4UkLy& zNWLX_9s#jaa7=Ydpp`tY$$>~cz)S{)CbM3IL{hU4R36zuzIXT1tq>hKbWG3khO$^# zVQQ;CyfmUeBlwvsY|fRl3pi;d%gnX+r`$8?^Nzq>Y@gcAbWP@*(OeEQQJ@h9cgZri zB=hc3NZ?uC5t*}Rc*P*f`H9cz^*tKe5ce^b2v8*vxYUZN>S5$*T@wl0uE zkq5%O7NwOlYHenU6;q>zcG)wJ(rwHK z^f0PQ7wyoswMn?S>m(x+Z`#7OZ~dhl)by?2oM^HqEX~jv=}gSYDnDeE(F=26qO! zaxcu@suXHBZ>G&rst)5wWq69KUcpREmT%C4m>^B90!`Md9UBIv&D%hk>gknxV2hez<9;GlEiueal-4exq;~OdC@sFY+a5Jl2A=*7iYn|oa z8pr+Cv%6Ly;entx3z`TAmI)ls1`O`q0c2q`uJ-<$_v}hH=~i=Jc}UWtH(vq%MpH-W z;3IIcisTpM-Q6}`^fhacn3i@7I*Bd6a?R2iQHyrYOw6x&^ob?HWFr8VRr#eJ^a}W^x ERz@@SGydLdz1-FlY)9x-&tVG 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

      B^ky5`x_eQ8G&BJ``5fYg)3xY-T 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

      w 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/docs/assets/sceenshots_ws-tester/url_port.png b/docs/docs/assets/sceenshots_ws-tester/url_port.png new file mode 100644 index 0000000000000000000000000000000000000000..7fce331f0eeedec8dae60ebf7afe9c663e5ac8c9 GIT binary patch literal 231701 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%0A`>oGS;-Hd?S#=55sR66HEyyt_ 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 m|7ZJBQsf)>|Mk5{wqQT}aMn=Nu7?WX Date: Mon, 9 Mar 2026 16:09:10 +0000 Subject: [PATCH 55/59] docs: Clarify Linky meter serial number description in API reference --- docs/docs/en/usage/api.md | 2 +- docs/docs/fr/usage/api.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/en/usage/api.md b/docs/docs/en/usage/api.md index d8fa59b..9d1d9c1 100644 --- a/docs/docs/en/usage/api.md +++ b/docs/docs/en/usage/api.md @@ -19,7 +19,7 @@ This request returns the list of available TIC streams. The response contains a list of TIC information objects: -- serial number; +- Linky meter serial number; - serial port name; - physical USB port identifier. diff --git a/docs/docs/fr/usage/api.md b/docs/docs/fr/usage/api.md index 9360b4d..6316e85 100644 --- a/docs/docs/fr/usage/api.md +++ b/docs/docs/fr/usage/api.md @@ -19,7 +19,7 @@ Cette requête permet de récupérer la liste des flux TIC disponibles. La réponse contient une liste de structures avec les informations TIC : -- numéro de série ; +- numéro de série du compteur Linky ; - nom du port série ; - identifiant du port USB physique. From 04cddb04958f85e474afe8397774f15bfb537061 Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:09:38 +0000 Subject: [PATCH 56/59] docs: Clarify modem type availability in API response documentation --- docs/docs/en/usage/api.md | 2 +- docs/docs/fr/usage/api.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/en/usage/api.md b/docs/docs/en/usage/api.md index 9d1d9c1..6ccab70 100644 --- a/docs/docs/en/usage/api.md +++ b/docs/docs/en/usage/api.md @@ -71,7 +71,7 @@ Supported modem types: The response contains a list of modem information objects: - serial port name; -- modem type; +- modem type, if available (only the USB modems described above); - product identifier (USB PID), if available; - vendor identifier (USB VID), if available; - product name, if available; diff --git a/docs/docs/fr/usage/api.md b/docs/docs/fr/usage/api.md index 6316e85..f8c6b5f 100644 --- a/docs/docs/fr/usage/api.md +++ b/docs/docs/fr/usage/api.md @@ -71,7 +71,7 @@ Types de modems pris en charge : La réponse contient une liste de structures avec les informations des modems : - nom du port série ; -- type de modem ; +- type de modem, si disponible (uniquement les modem USB décrit ci-dessus); - identifiant produit (USB PID), si disponible ; - identifiant vendeur (USB VID), si disponible ; - nom du produit, si disponible ; From b21365ca142414f463b0361fc81278a7ccb2b8ad Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:10:27 +0000 Subject: [PATCH 57/59] docs: Update value format to string in API documentation --- docs/docs/en/usage/api.md | 4 ++-- docs/docs/fr/usage/api.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docs/en/usage/api.md b/docs/docs/en/usage/api.md index 6ccab70..4d2dee0 100644 --- a/docs/docs/en/usage/api.md +++ b/docs/docs/en/usage/api.md @@ -136,7 +136,7 @@ The `data` field must contain a TIC identifier defined in [TIC identifier format }, "content": { "ADSC": "010203040506", - "URMS1": 230 + "URMS1": "230" } } } @@ -234,7 +234,7 @@ This event is generated for each new TIC frame and sent to all clients subscribe }, "content": { "ADSC": "010203040506", - "URMS1": 230 + "URMS1": "230" } } } diff --git a/docs/docs/fr/usage/api.md b/docs/docs/fr/usage/api.md index f8c6b5f..3ea3bc4 100644 --- a/docs/docs/fr/usage/api.md +++ b/docs/docs/fr/usage/api.md @@ -136,7 +136,7 @@ Le champ `data` doit contenir un identifiant TIC défini dans le [format d'ident }, "content": { "ADSC": "010203040506", - "URMS1": 230 + "URMS1": "230" } } } @@ -234,7 +234,7 @@ Cet évènement est généré à chaque nouvelle trame TIC et envoyé à tous le }, "content": { "ADSC": "010203040506", - "URMS1": 230 + "URMS1": "230" } } } From c8b00df4693b3d786efd53c3dc9890a02d8bc0bd Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:10:57 +0000 Subject: [PATCH 58/59] docs: Enhance TIC identifier descriptions for clarity in API documentation --- docs/docs/en/usage/api.md | 6 +++--- docs/docs/fr/usage/api.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/docs/en/usage/api.md b/docs/docs/en/usage/api.md index 4d2dee0..44a2a9d 100644 --- a/docs/docs/en/usage/api.md +++ b/docs/docs/en/usage/api.md @@ -313,9 +313,9 @@ For targeted unsubscription, the `data` field must contain one TIC identifier (o A TIC identifier is an object containing one of the following fields: -- `serialNumber` -- `portId` -- `portName` +- `serialNumber` : Linky meter serial number; +- `portId` : physical serial port identifier where the modem is connected; +- `portName` : serial port name where the modem is connected; Depending on the request, you can provide a single identifier or a list of identifiers. diff --git a/docs/docs/fr/usage/api.md b/docs/docs/fr/usage/api.md index 3ea3bc4..2260b1a 100644 --- a/docs/docs/fr/usage/api.md +++ b/docs/docs/fr/usage/api.md @@ -313,9 +313,9 @@ Dans le cas d'un désabonnement ciblé, le champ `data` doit contenir un identif Un identifiant TIC est un objet contenant un des champs suivants : -- `serialNumber` -- `portId` -- `portName` +- `serialNumber` : numéro de série du compteur Linky ; +- `portId` : identifiant physique du port série sur lequel le modem est connecté ; +- `portName` : nom du port série sur lequel le modem est connecté ; Selon la requête, vous pouvez fournir un identifiant unique ou une liste d'identifiants. From 8112aa715c60428c435e8cf9816c00d18e2ff0cd Mon Sep 17 00:00:00 2001 From: Mathieu Sabarthes <91611760+MathieuSabarthes@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:22:57 +0000 Subject: [PATCH 59/59] docs: Translate development documentation to French --- docs/docs/fr/development/index.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/docs/fr/development/index.md b/docs/docs/fr/development/index.md index 71ebee2..06900ca 100644 --- a/docs/docs/fr/development/index.md +++ b/docs/docs/fr/development/index.md @@ -1,18 +1,20 @@ --- sidebar_position: 1 -title: Development +title: Développement --- -# Development +# Développement -This section documents how to work on **TIC2WebSocket** locally, run checks, and contribute safely. +Cette section explique comment travailler sur **TIC2WebSocket** en local, exécuter les vérifications et contribuer au projet dans de bonnes conditions. ## Architecture -See the dedicated page for an overview of the architecture and design decisions: +Consultez la page dédiée pour une vue d’ensemble de l’architecture et des choix de conception : - [Architecture](architecture.md) -## Contributing -See the dedicated page for guidelines on contributing to the project: -- [Contributing](contributing.md) \ No newline at end of file +## Contribuer + +Consultez la page dédiée pour les bonnes pratiques de contribution au projet : + +- [Contribuer](contributing.md) \ No newline at end of file

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® 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�Fao6_EWc=j0qlpVBT!j$kZ9 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 z + +SPDX-FileContributor: Jehan BOUSCH +SPDX-FileContributor: Mathieu SABARTHES + +SPDX-License-Identifier: Apache-2.0 diff --git a/src/uml/TICCore.vpp b/src/uml/TICCore.vpp new file mode 100644 index 0000000000000000000000000000000000000000..09cb169fd7e57f03ed27079f5913d5255af18170 GIT binary patch literal 2255872 zcmeEv2S8KT`uH7eMR0+N15gx{y+KgJ4iHAzI4~pu0)aphK)`haZtLD#>#B8IchzcZ z9aZaUU3JvGM_sL>{=aiG?***wdwsvY_cyek_kL%8=iD>CbyAY!B}%bPhGn_q_+Xgn91MeC{SVhaHueO0xZN}{hG3F>f+DphUz0D$ zC**zdDtWcBR64wyv576m4#y-VBJm(anG|Z8BPdtO==YomE+?AL;pKQ~9!zR6!qm1~ zH+)2Ir65--<|Rf%#^*%FNAe;QQW-B~I6sn;8fl-(35$=kX9U?hb0h491p>LVm%odB zLSm|YLRx&ht9@EZBtHj&Dg<&m)Zf=dhs;Uh@SzA@GFMHOsaZ*ph6xO^Q>Q=~YRhu9 zPvGz(?Umv|N>}@c$S7_CH1V4o|M^an{w> z#hxD-6$#A^k4#}w4kdC4B28?0-5TpL2BmYn7_Vq#8L4ErsjZU}KEi?S!6bfSY-D(9 z4mTkx(eRmI^q*l!W8Fv7R@-;petw2s8xfY19?4HZ#HKqV&@fP&`5(tf;-rShAn^apU#F$jjTXu+&HnFC&p3XP+yRNyUN^gXE2m2!3KB-J|-ko|0n_TM2Zv zXb-@+EduDtvKas~GXSQfB|sdH#o)h2e&iK5hYlU^4rbJykSV<>L3(OjPP(6eG^Z>q zEi*YIK9w7uB9>RcC%)B67jxk3#4*VY#DrLrb+H~A&GZ~+so72-`msM)6?I}GYcNPJmHUBVz8s8c96(1xKu7H6bqI1HI^3UN$Teai6CDt zC{8R@N@OL9oCtBApj@gIi9#tZ5sFi*O2wg`jT1yeNRC1w z6G{Y#xRE8IbcsTeD}hh?Z5~V~S&B)`Fh~zSfC>71-%0#(DT%x4&dVlh? zk`k!0h>I4DB!N;XmY0M&*3c754hk8PQzN8Xs(yP z`oqAH%JScnHd-zyEszKmoD$R)i9jJK$q!9!oMLFqrOE{*3TV5mBvmfShdKNG4XsaP zlu49aR?LU#!+_d!u~L~(Ak_?e^*`|a??P*{5qTS+NdWnZd`3PbZK{}D0$hKrl(vmbKapE=coOndsA+8Y@i8I6r;yYqLv4_}BY$DbWD~QF!Tw*#g zkr+eN5<`eeqKqga#6%8}MWhfsBAVb3JqbU;gK!}_6CDU!qB+roFvj2Ezu`~u2ly@g zXZ!;G1O7dJ2>%-2g>S_-;Gg5m@P+tnd@BA4J_@Js8oV5r;SyYk_s28vWIPU!#CzjG zxDW1zcf%cUJG?bLw*fWI)$stHt&0J8rY-{Dk9B{^p0FT#s0Q|nL z2f$->-2fh~>kRNnT_=Fw)!6|&TxSdLP+bdv2kX%E`)!>$!2NY-+55V77{GnCH2}Y= ztpvEIRt|7iZ3)1gwGx2aYx4kZs}%y=S~~#Xmf9?Un`=`6eo>nUa8oUs#MjqG0$f|$ z2jH68o&Z0u^#{1B)(hZgwXOhH)S`MWuXO~tthNKdPixx%TwL1{;G$Yo!-ch|hVyGt z4dGh>{ajh?3(~ z;Q+^}dI21(3IaGrg=jchg=jcZ)g54+$_b!KWe<>2A?giR*#I1-Y7TIyswu$1DpP