<commit_before>/**
 * Copyright (c) 2010-2016 by the respective copyright holders.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.binding.modbus.internal;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.collections.ResettableIterator;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.pool2.KeyedObjectPool;
import org.apache.commons.pool2.SwallowedExceptionListener;
import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
import org.openhab.binding.modbus.ModbusBindingProvider;
import org.openhab.binding.modbus.internal.ModbusGenericBindingProvider.ModbusBindingConfig;
import org.openhab.binding.modbus.internal.pooling.EndpointPoolConfiguration;
import org.openhab.binding.modbus.internal.pooling.ModbusSlaveConnectionFactoryImpl;
import org.openhab.binding.modbus.internal.pooling.ModbusSlaveEndpoint;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.binding.BindingProvider;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.wimpi.modbus.Modbus;
import net.wimpi.modbus.net.ModbusSlaveConnection;
import net.wimpi.modbus.procimg.InputRegister;
import net.wimpi.modbus.util.BitVector;
import net.wimpi.modbus.util.SerialParameters;

/**
 * Modbus binding allows to connect to multiple Modbus slaves as TCP master.
 *
 * @author <NAME>
 * @since 1.1.0
 */
public class ModbusBinding extends AbstractActiveBinding<ModbusBindingProvider>implements ManagedService {

    private static final long DEFAULT_POLL_INTERVAL = 200;

    /**
     * Time to wait between connection passive+borrow, i.e. time to wait between
     * transactions
     * Default 60ms for TCP slaves, Siemens S7 1212 PLC couldn't handle faster
     * requests with default settings.
     */
    private static final long DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS = 60;

    /**
     * Time to wait between connection passive+borrow, i.e. time to wait between
     * transactions
     * Default 35ms for Serial slaves, motivation discussed
     * here https://community.openhab.org/t/connection-pooling-in-modbus-binding/5246/111?u=ssalonen
     */
    private static final long DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS = 35;

    private static final Logger logger = LoggerFactory.getLogger(ModbusBinding.class);

    private static final String UDP_PREFIX = "udp";
    private static final String TCP_PREFIX = "tcp";
    private static final String SERIAL_PREFIX = "serial";

    private static final String VALID_CONFIG_KEYS = "connection|id|start|length|type|valuetype|rawdatamultiplier|writemultipleregisters|updateunchangeditems";
    private static final Pattern EXTRACT_MODBUS_CONFIG_PATTERN = Pattern.compile(
            "^(" + TCP_PREFIX + "|" + UDP_PREFIX + "|" + SERIAL_PREFIX + "|)\\.(.*?)\\.(" + VALID_CONFIG_KEYS + ")$");

    /** Stores instances of all the slaves defined in cfg file */
    private static Map<String, ModbusSlave> modbusSlaves = new ConcurrentHashMap<String, ModbusSlave>();

    private static GenericKeyedObjectPoolConfig poolConfig = new GenericKeyedObjectPoolConfig();

    static {
        // When the pool is exhausted, multiple calling threads may be simultaneously blocked waiting for instances to
        // become available. As of pool 1.5, a "fairness" algorithm has been implemented to ensure that threads receive
        // available instances in request arrival order.
        poolConfig.setFairness(true);
        // Limit one connection per endpoint (i.e. same ip:port pair or same serial device).
        // If there are multiple read/write requests to process at the same time, block until previous one finishes
        poolConfig.setBlockWhenExhausted(true);
        poolConfig.setMaxTotalPerKey(1);

        // block infinitely when exhausted
        poolConfig.setMaxWaitMillis(-1);

        // make sure we return connected connections from/to connection pool
        poolConfig.setTestOnBorrow(true);
        poolConfig.setTestOnReturn(true);

        // disable JMX
        poolConfig.setJmxEnabled(false);
    }

    /**
     * We use connection pool to ensure that only single transaction is ongoing per each endpoint. This is especially
     * important with serial slaves but practice has shown that even many tcp slaves have limited
     * capability to handle many connections at the same time
     *
     * Relevant discussion at the time of implementation:
     * - https://community.openhab.org/t/modbus-connection-problem/6108/
     * - https://community.openhab.org/t/connection-pooling-in-modbus-binding/5246/
     */
    private static KeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> connectionPool;
    private static ModbusSlaveConnectionFactoryImpl connectionFactory;

    private static void reconstructConnectionPool() {
        connectionFactory = new ModbusSlaveConnectionFactoryImpl();
        GenericKeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> genericKeyedObjectPool = new GenericKeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection>(
                connectionFactory, poolConfig);
        genericKeyedObjectPool.setSwallowedExceptionListener(new SwallowedExceptionListener() {

            @Override
            public void onSwallowException(Exception e) {
                logger.error("Connection pool swallowed unexpected exception: {}", e.getMessage());

            }
        });
        connectionPool = genericKeyedObjectPool;
    }

    /**
     * For testing
     */
    static KeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> getReconstructedConnectionPoolForTesting() {
        reconstructConnectionPool();
        return connectionPool;
    }

    /** slaves update interval in milliseconds */
    public static long pollInterval = DEFAULT_POLL_INTERVAL;

    @Override
    public void activate() {
    }

    @Override
    public void deactivate() {
        clear();
    }

    @Override
    protected long getRefreshInterval() {
        return pollInterval;
    }

    @Override
    protected String getName() {
        return "Modbus Polling Service";
    }

    /**
     * Parses configuration creating Modbus slave instances defined in cfg file
     * {@inheritDoc}
     */
    @Override
    protected void internalReceiveCommand(String itemName, Command command) {
        for (ModbusBindingProvider provider : providers) {
            if (provider.providesBindingFor(itemName)) {
                ModbusBindingConfig config = provider.getConfig(itemName);
                ModbusSlave slave = modbusSlaves.get(config.slaveName);
                slave.executeCommand(command, config);
            }
        }
    }

    /**
     * Posts update event to OpenHAB bus for "holding" and "input register" type slaves
     *
     * @param binding ModbusBinding to get item configuration from BindingProviding
     * @param registers data received from slave device in the last pollInterval
     * @param itemName item to update
     */
    protected void internalUpdateItem(String slaveName, InputRegister[] registers, String itemName) {
        for (ModbusBindingProvider provider : providers) {
            if (!provider.providesBindingFor(itemName)) {
                continue;
            }
            ModbusBindingConfig config = provider.getConfig(itemName);
            if (!config.slaveName.equals(slaveName)) {
                continue;
            }

            ModbusSlave slave = modbusSlaves.get(slaveName);
            String slaveValueType = slave.getValueType();
            double rawDataMultiplier = slave.getRawDataMultiplier();

            /* receive data manipulation */
            State newState = extractStateFromRegisters(registers, config.readIndex, slaveValueType);
            // Convert newState (DecimalType) to on/off kind of state if we have "boolean item" (Switch, Contact etc)
            // In other cases (such as Number items) newStateBoolean will be UNDEF
            State newStateBoolean = provider.getConfig(itemName)
                    .translateBoolean2State(!newState.equals(DecimalType.ZERO));
            // If we have boolean item (newStateBoolean is not UNDEF)
            if (!UnDefType.UNDEF.equals(newStateBoolean)) {
                newState = newStateBoolean;
            } else if ((rawDataMultiplier != 1) && (config.getItemClass().isAssignableFrom(NumberItem.class))) {
                double tmpValue = ((DecimalType) newState).doubleValue() * rawDataMultiplier;
                newState = new DecimalType(String.valueOf(tmpValue));
            }

            if (slave.isUpdateUnchangedItems() || !newState.equals(config.getState())) {
                eventPublisher.postUpdate(itemName, newState);
                config.setState(newState);
            }
        }
    }

    /**
     * Read data from registers and convert the result to DecimalType
     * Interpretation of <tt>index</tt> goes as follows depending on type
     *
     * BIT:
     * - a single bit is read from the registers
     * - indices between 0...15 (inclusive) represent bits of the first register
     * - indices between 16...31 (inclusive) represent bits of the second register, etc.
     * - index 0 refers to the least significant bit of the first register
     * - index 1 refers to the second least significant bit of the first register, etc.
     * INT8:
     * - a byte (8 bits) from the registers is interpreted as signed integer
     * - index 0 refers to low byte of the first register, 1 high byte of first register
     * - index 2 refers to low byte of the second register, 3 high byte of second register, etc.
     * - it is assumed that each high and low byte is encoded in most significant bit first order
     * UINT8:
     * - same as INT8 except values are interpreted as unsigned integers
     * INT16:
     * - register with index (counting from zero) is interpreted as 16 bit signed integer.
     * - it is assumed that each register is encoded in most significant bit first order
     * UINT16:
     * - same as INT16 except values are interpreted as unsigned integers
     * INT32:
     * - registers (2 * index) and ( 2 *index + 1) are interpreted as signed 32bit integer.
     * - it assumed that the first register contains the most significant 16 bits
     * - it is assumed that each register is encoded in most significant bit first order
     * UINT32:
     * - same as UINT32 except values are interpreted as unsigned integers
     * FLOAT32:
     * - registers (2 * index) and ( 2 *index + 1) are interpreted as signed 32bit floating point number.
     * - it assumed that the first register contains the most significant 16 bits
     * - it is assumed that each register is encoded in most significant bit first order
     *
     * @param registers
     *            list of registers, each register represent 16bit of data
     * @param index
     *            zero based item index. Interpretation of this depends on type
     * @param type
     *            item type, e.g. unsigned 16bit integer (<tt>ModbusBindingProvider.VALUE_TYPE_UINT16</tt>)
     * @return number representation queried value
     * @throws IllegalArgumentException when <tt>type</tt> does not match a known type
     * @throws IndexOutOfBoundsException when <tt>index</tt> is out of bounds of registers
     *
     */
    private DecimalType extractStateFromRegisters(InputRegister[] registers, int index, String type) {
        if (type.equals(ModbusBindingProvider.VALUE_TYPE_BIT)) {
            return new DecimalType((registers[index / 16].toUnsignedShort() >> (index % 16)) & 1);
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_INT8)) {
            return new DecimalType(registers[index / 2].toBytes()[1 - (index % 2)]);
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_UINT8)) {
            return new DecimalType((registers[index / 2].toUnsignedShort() >> (8 * (index % 2))) & 0xff);
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_INT16)) {
            ByteBuffer buff = ByteBuffer.allocate(2);
            buff.put(registers[index].toBytes());
            return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getShort(0));
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_UINT16)) {
            return new DecimalType(registers[index].toUnsignedShort());
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_INT32)) {
            ByteBuffer buff = ByteBuffer.allocate(4);
            buff.put(registers[index * 2 + 0].toBytes());
            buff.put(registers[index * 2 + 1].toBytes());
            return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getInt(0));
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_UINT32)) {
            ByteBuffer buff = ByteBuffer.allocate(8);
            buff.position(4);
            buff.put(registers[index * 2 + 0].toBytes());
            buff.put(registers[index * 2 + 1].toBytes());
            return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getLong(0));
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_FLOAT32)) {
            ByteBuffer buff = ByteBuffer.allocate(4);
            buff.put(registers[index * 2 + 0].toBytes());
            buff.put(registers[index * 2 + 1].toBytes());
            return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getFloat(0));
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Posts update event to OpenHAB bus for "coil" and "discrete input" type slaves
     *
     * @param binding ModbusBinding to get item configuration from BindingProviding
     * @param registers data received from slave device in the last pollInterval
     * @param item item to update
     */
    protected void internalUpdateItem(String slaveName, BitVector coils, String itemName) {
        for (ModbusBindingProvider provider : providers) {
            if (provider.providesBindingFor(itemName)) {
                ModbusBindingConfig config = provider.getConfig(itemName);
                if (config.slaveName.equals(slaveName)) {
                    boolean state = coils.getBit(config.readIndex);
                    State newState = config.translateBoolean2State(state);
                    ModbusSlave slave = modbusSlaves.get(slaveName);
                    if (slave.isUpdateUnchangedItems() || !newState.equals(config.getState())) {
                        eventPublisher.postUpdate(itemName, newState);
                        config.setState(newState);
                    }
                }
            }
        }
    }

    /**
     * Returns names of all the items, registered with this binding
     *
     * @return list of item names
     */
    public Collection<String> getItemNames() {
        Collection<String> items = null;
        for (BindingProvider provider : providers) {
            if (items == null) {
                items = provider.getItemNames();
            } else {
                items.addAll(provider.getItemNames());
            }
        }
        return items;
    }

    /**
     * updates all slaves from the modbusSlaves
     */
    @Override
    protected void execute() {
        Collection<ModbusSlave> slaves = new HashSet<ModbusSlave>();
        synchronized (slaves) {
            slaves.addAll(modbusSlaves.values());
        }
        for (ModbusSlave slave : slaves) {
            slave.update(this);
        }
    }

    /**
     * Clear all configuration and close all connections
     */
    private void clear() {
        try {
            // Closes all connections by calling destroyObject method in the ObjectFactory implementation
            if (connectionPool != null) {
                connectionPool.clear();
            }
        } catch (Exception e) {
            // Should not happen
            logger.error("Error clearing connections", e);
        }
        modbusSlaves.clear();
    }

    @Override
    public void updated(Dictionary<String, ?> config) throws ConfigurationException {
        setProperlyConfigured(false);
        // remove all known items if configuration changed
        clear();
        reconstructConnectionPool();
        if (config == null) {
            logger.warn("Got null config!");
            return;
        }
        Enumeration<String> keys = config.keys();
        Map<String, EndpointPoolConfiguration> slavePoolConfigs = new HashMap<String, EndpointPoolConfiguration>();
        Map<ModbusSlaveEndpoint, EndpointPoolConfiguration> endpointPoolConfigs = new HashMap<ModbusSlaveEndpoint, EndpointPoolConfiguration>();
        while (keys.hasMoreElements()) {
            final String key = keys.nextElement();
            final String value = (String) config.get(key);
            try {
                // the config-key enumeration contains additional keys that we
                // don't want to process here ...
                if ("service.pid".equals(key)) {
                    continue;
                }

                Matcher matcher = EXTRACT_MODBUS_CONFIG_PATTERN.matcher(key);
                if (!matcher.matches()) {
                    if ("poll".equals(key)) {
                        if (StringUtils.isNotBlank((String) config.get(key))) {
                            pollInterval = Integer.valueOf((String) config.get(key));
                        }
                    } else if ("writemultipleregisters".equals(key)) {
                        // XXX: ugly to touch base class but kept here for backwards compat
                        // FIXME: should this be deprecated as introduced as slave specific parameter?
                        ModbusSlave.setWriteMultipleRegisters(Boolean.valueOf(config.get(key).toString()));
                    } else {
                        logger.debug(
                                "given modbus-slave-config-key '{}' does not follow the expected pattern or 'serial.<slaveId>.<{}>'",
                                key, VALID_CONFIG_KEYS);
                    }
                    continue;
                }

                matcher.reset();
                matcher.find();

                String slave = matcher.group(2);

                ModbusSlave modbusSlave = modbusSlaves.get(slave);
                EndpointPoolConfiguration endpointPoolConfig = slavePoolConfigs.get(slave);
                if (modbusSlave == null) {
                    if (matcher.group(1).equals(TCP_PREFIX)) {
                        modbusSlave = new ModbusTcpSlave(slave, connectionPool);
                    } else if (matcher.group(1).equals(UDP_PREFIX)) {
                        modbusSlave = new ModbusUdpSlave(slave, connectionPool);
                    } else if (matcher.group(1).equals(SERIAL_PREFIX)) {
                        modbusSlave = new ModbusSerialSlave(slave, connectionPool);
                    } else {
                        throw new ConfigurationException(slave, "the given slave type '" + slave + "' is unknown");
                    }
                    endpointPoolConfig = new EndpointPoolConfiguration();
                    // Do not give up if the connection attempt fails on the first time...
                    endpointPoolConfig.setConnectMaxTries(Modbus.DEFAULT_RETRIES);
                    logger.debug("modbusSlave '{}' instanciated", slave);
                    modbusSlaves.put(slave, modbusSlave);
                }

                String configKey = matcher.group(3);

                if ("connection".equals(configKey)) {
                    String[] chunks = value.split(":");
                    Iterator<String> settingIterator = stringArrayIterator(chunks);
                    if (modbusSlave instanceof ModbusIPSlave) {
                        ((ModbusIPSlave) modbusSlave).setHost(settingIterator.next());
                        //
                        // Defaults for endpoint and slave
                        //
                        modbusSlave.setRetryDelayMillis(DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS);
                        endpointPoolConfig.setPassivateBorrowMinMillis(DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS);

                        //
                        // Optional parameters
                        //
                        try {
                            ((ModbusIPSlave) modbusSlave).setPort(Integer.valueOf(settingIterator.next()));

                            long passivateBorrowMinMillis = Long.parseLong(settingIterator.next());
                            modbusSlave.setRetryDelayMillis(passivateBorrowMinMillis);
                            endpointPoolConfig.setPassivateBorrowMinMillis(passivateBorrowMinMillis);

                            endpointPoolConfig.setReconnectAfterMillis(Integer.parseInt(settingIterator.next()));

                            // time to wait before trying connect closed connection. Note that
                            // ModbusSlaveConnectionFactoryImpl makes sure that max{passivateBorrowMinMillis, this
                            // parameter} is waited between connection attempts
                            endpointPoolConfig.setInterConnectDelayMillis(Long.parseLong(settingIterator.next()));

                            endpointPoolConfig.setConnectMaxTries(Integer.parseInt(settingIterator.next()));
                        } catch (NoSuchElementException e) {
                            // Some of the optional parameters are missing -- it's ok!
                        }
                        if (settingIterator.hasNext()) {
                            String errMsg = String
                                    .format("%s Has too many colon (:) separated connection settings for a tcp/udp modbus slave. "
                                            + "Expecting at most 6 parameters: hostname (mandatory) and "
                                            + "optionally (in this order) port number, "
                                            + "interTransactionDelayMillis, reconnectAfterMillis,"
                                            + "interConnectDelayMillis, connectMaxTries.", key);
                            throw new ConfigurationException(key, errMsg);
                        }
                    } else if (modbusSlave instanceof ModbusSerialSlave) {
                        SerialParameters serialParameters = new SerialParameters();
                        serialParameters.setPortName(settingIterator.next());
                        //
                        // Defaults for endpoint and slave
                        //
                        endpointPoolConfig.setReconnectAfterMillis(-1); // never "disconnect" (close/open serial port)
                                                                        // serial connection between borrows
                        modbusSlave.setRetryDelayMillis(DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS);
                        endpointPoolConfig.setPassivateBorrowMinMillis(DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS);

                        //
                        // Optional parameters
                        //
                        try {
                            serialParameters.setBaudRate(settingIterator.next());
                            serialParameters.setDatabits(settingIterator.next());
                            serialParameters.setParity(settingIterator.next());
                            serialParameters.setStopbits(settingIterator.next());
                            serialParameters.setEncoding(settingIterator.next());

                            // time to wait between connection passive+borrow, i.e. time to wait between
                            // transactions
                            long passivateBorrowMinMillis = Long.parseLong(settingIterator.next());
                            modbusSlave.setRetryDelayMillis(passivateBorrowMinMillis);
                            endpointPoolConfig.setPassivateBorrowMinMillis(passivateBorrowMinMillis);

                            serialParameters.setReceiveTimeoutMillis(settingIterator.next());
                            serialParameters.setFlowControlIn(settingIterator.next());
                            serialParameters.setFlowControlOut(settingIterator.next());
                        } catch (NoSuchElementException e) {
                            // Some of the optional parameters are missing -- it's ok!
                        }
                        if (settingIterator.hasNext()) {
                            String errMsg = String.format(
                                    "%s Has too many colon (:) separated connection settings for a serial modbus slave. "
                                            + "Expecting at most 9 parameters (got %d): devicePort (mandatory), "
                                            + "and 0 or more optional parameters (in this order): "
                                            + "baudRate, dataBits, parity, stopBits, "
                                            + "encoding, interTransactionWaitMillis, "
                                            + "receiveTimeoutMillis, flowControlIn, flowControlOut",
                                    key, chunks.length);
                            throw new ConfigurationException(key, errMsg);
                        }

                        ((ModbusSerialSlave) modbusSlave).setSerialParameters(serialParameters);
                    }
                } else if ("start".equals(configKey)) {
                    modbusSlave.setStart(Integer.valueOf(value));
                } else if ("length".equals(configKey)) {
                    modbusSlave.setLength(Integer.valueOf(value));
                } else if ("id".equals(configKey)) {
                    modbusSlave.setId(Integer.valueOf(value));
                } else if ("type".equals(configKey)) {
                    if (ArrayUtils.contains(ModbusBindingProvider.SLAVE_DATA_TYPES, value)) {
                        modbusSlave.setType(value);
                    } else {
                        throw new ConfigurationException(configKey, "the given slave type '" + value + "' is invalid");
                    }
                } else if ("valuetype".equals(configKey)) {
                    if (ArrayUtils.contains(ModbusBindingProvider.VALUE_TYPES, value)) {
                        modbusSlave.setValueType(value);
                    } else {
                        throw new ConfigurationException(configKey, "the given value type '" + value + "' is invalid");
                    }
                } else if ("rawdatamultiplier".equals(configKey)) {
                    modbusSlave.setRawDataMultiplier(Double.valueOf(value.toString()));
                } else if ("updateunchangeditems".equals(configKey)) {
                    modbusSlave.setUpdateUnchangedItems(Boolean.valueOf(value.toString()));
                } else {
                    throw new ConfigurationException(configKey, "the given configKey '" + configKey + "' is unknown");
                }
                modbusSlaves.put(slave, modbusSlave);
                slavePoolConfigs.put(slave, endpointPoolConfig);
            } catch (Exception e) {
                String errMsg = String.format("Exception when parsing configuration parameter %s = %s  --  %s %s", key,
                        value, e.getClass().getName(), e.getMessage());
                logger.error(errMsg);
                throw new ConfigurationException(key, errMsg);
            }
        }
        // Finally, go through each slave definition, and combine the slave pool configurations
        for (Entry<String, EndpointPoolConfiguration> slaveEntry : slavePoolConfigs.entrySet()) {
            String slave = slaveEntry.getKey();
            EndpointPoolConfiguration poolConfiguration = slaveEntry.getValue();
            ModbusSlaveEndpoint endpoint = modbusSlaves.get(slave).getEndpoint();
            EndpointPoolConfiguration existingPoolConfiguration = endpointPoolConfigs.get(endpoint);
            // Do we have two slaves with same endpoint, but different pool configuration parameters? Warn if we do.
            if (existingPoolConfiguration != null && !existingPoolConfiguration.equals(poolConfiguration)) {
                logger.warn(
                        "Slave {} (endpoint {}) has different retry/connection delay "
                                + "(EndpointPoolConfiguration) etc. settings. Replacing {} with {}",
                        slave, endpoint, existingPoolConfiguration, poolConfiguration);
            }
            endpointPoolConfigs.put(endpoint, poolConfiguration);
        }
        connectionFactory.applyEndpointPoolConfigs(endpointPoolConfigs);
        logger.debug("Parsed the following slave->endpoint configurations: {}. If the endpoint is same, "
                + "connections are shared between the instances.", slavePoolConfigs);
        logger.debug("Parsed the following pool configurations: {}", endpointPoolConfigs);
        logger.debug("config looked good");
        setProperlyConfigured(true);

    }

    private static Iterator<String> stringArrayIterator(final String[] chunks) {
        Iterator<String> settingIterator = new Iterator<String>() {

            private ResettableIterator inner = IteratorUtils.arrayIterator(chunks);

            @Override
            public String next() {
                return (String) inner.next();
            }

            @Override
            public boolean hasNext() {
                return inner.hasNext();
            }

        };
        return settingIterator;
    }

}<commit_msg>made modbus binding compilable with Java 7 (#4395)<commit_after>/**
 * Copyright (c) 2010-2016 by the respective copyright holders.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.binding.modbus.internal;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.collections.ResettableIterator;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.pool2.KeyedObjectPool;
import org.apache.commons.pool2.SwallowedExceptionListener;
import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
import org.openhab.binding.modbus.ModbusBindingProvider;
import org.openhab.binding.modbus.internal.ModbusGenericBindingProvider.ModbusBindingConfig;
import org.openhab.binding.modbus.internal.pooling.EndpointPoolConfiguration;
import org.openhab.binding.modbus.internal.pooling.ModbusSlaveConnectionFactoryImpl;
import org.openhab.binding.modbus.internal.pooling.ModbusSlaveEndpoint;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.binding.BindingProvider;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.wimpi.modbus.Modbus;
import net.wimpi.modbus.net.ModbusSlaveConnection;
import net.wimpi.modbus.procimg.InputRegister;
import net.wimpi.modbus.util.BitVector;
import net.wimpi.modbus.util.SerialParameters;

/**
 * Modbus binding allows to connect to multiple Modbus slaves as TCP master.
 *
 * @author <NAME>rasnov
 * @since 1.1.0
 */
public class ModbusBinding extends AbstractActiveBinding<ModbusBindingProvider>implements ManagedService {

    private static final long DEFAULT_POLL_INTERVAL = 200;

    /**
     * Time to wait between connection passive+borrow, i.e. time to wait between
     * transactions
     * Default 60ms for TCP slaves, Siemens S7 1212 PLC couldn't handle faster
     * requests with default settings.
     */
    private static final long DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS = 60;

    /**
     * Time to wait between connection passive+borrow, i.e. time to wait between
     * transactions
     * Default 35ms for Serial slaves, motivation discussed
     * here https://community.openhab.org/t/connection-pooling-in-modbus-binding/5246/111?u=ssalonen
     */
    private static final long DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS = 35;

    private static final Logger logger = LoggerFactory.getLogger(ModbusBinding.class);

    private static final String UDP_PREFIX = "udp";
    private static final String TCP_PREFIX = "tcp";
    private static final String SERIAL_PREFIX = "serial";

    private static final String VALID_CONFIG_KEYS = "connection|id|start|length|type|valuetype|rawdatamultiplier|writemultipleregisters|updateunchangeditems";
    private static final Pattern EXTRACT_MODBUS_CONFIG_PATTERN = Pattern.compile(
            "^(" + TCP_PREFIX + "|" + UDP_PREFIX + "|" + SERIAL_PREFIX + "|)\\.(.*?)\\.(" + VALID_CONFIG_KEYS + ")$");

    /** Stores instances of all the slaves defined in cfg file */
    private static Map<String, ModbusSlave> modbusSlaves = new ConcurrentHashMap<String, ModbusSlave>();

    private static GenericKeyedObjectPoolConfig poolConfig = new GenericKeyedObjectPoolConfig();

    static {
        // When the pool is exhausted, multiple calling threads may be simultaneously blocked waiting for instances to
        // become available. As of pool 1.5, a "fairness" algorithm has been implemented to ensure that threads receive
        // available instances in request arrival order.
        poolConfig.setFairness(true);
        // Limit one connection per endpoint (i.e. same ip:port pair or same serial device).
        // If there are multiple read/write requests to process at the same time, block until previous one finishes
        poolConfig.setBlockWhenExhausted(true);
        poolConfig.setMaxTotalPerKey(1);

        // block infinitely when exhausted
        poolConfig.setMaxWaitMillis(-1);

        // make sure we return connected connections from/to connection pool
        poolConfig.setTestOnBorrow(true);
        poolConfig.setTestOnReturn(true);

        // disable JMX
        poolConfig.setJmxEnabled(false);
    }

    /**
     * We use connection pool to ensure that only single transaction is ongoing per each endpoint. This is especially
     * important with serial slaves but practice has shown that even many tcp slaves have limited
     * capability to handle many connections at the same time
     *
     * Relevant discussion at the time of implementation:
     * - https://community.openhab.org/t/modbus-connection-problem/6108/
     * - https://community.openhab.org/t/connection-pooling-in-modbus-binding/5246/
     */
    private static KeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> connectionPool;
    private static ModbusSlaveConnectionFactoryImpl connectionFactory;

    private static void reconstructConnectionPool() {
        connectionFactory = new ModbusSlaveConnectionFactoryImpl();
        GenericKeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> genericKeyedObjectPool = new GenericKeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection>(
                connectionFactory, poolConfig);
        genericKeyedObjectPool.setSwallowedExceptionListener(new SwallowedExceptionListener() {

            @Override
            public void onSwallowException(Exception e) {
                logger.error("Connection pool swallowed unexpected exception: {}", e.getMessage());

            }
        });
        connectionPool = genericKeyedObjectPool;
    }

    /**
     * For testing
     */
    static KeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> getReconstructedConnectionPoolForTesting() {
        reconstructConnectionPool();
        return connectionPool;
    }

    /** slaves update interval in milliseconds */
    public static long pollInterval = DEFAULT_POLL_INTERVAL;

    @Override
    public void activate() {
    }

    @Override
    public void deactivate() {
        clear();
    }

    @Override
    protected long getRefreshInterval() {
        return pollInterval;
    }

    @Override
    protected String getName() {
        return "Modbus Polling Service";
    }

    /**
     * Parses configuration creating Modbus slave instances defined in cfg file
     * {@inheritDoc}
     */
    @Override
    protected void internalReceiveCommand(String itemName, Command command) {
        for (ModbusBindingProvider provider : providers) {
            if (provider.providesBindingFor(itemName)) {
                ModbusBindingConfig config = provider.getConfig(itemName);
                ModbusSlave slave = modbusSlaves.get(config.slaveName);
                slave.executeCommand(command, config);
            }
        }
    }

    /**
     * Posts update event to OpenHAB bus for "holding" and "input register" type slaves
     *
     * @param binding ModbusBinding to get item configuration from BindingProviding
     * @param registers data received from slave device in the last pollInterval
     * @param itemName item to update
     */
    protected void internalUpdateItem(String slaveName, InputRegister[] registers, String itemName) {
        for (ModbusBindingProvider provider : providers) {
            if (!provider.providesBindingFor(itemName)) {
                continue;
            }
            ModbusBindingConfig config = provider.getConfig(itemName);
            if (!config.slaveName.equals(slaveName)) {
                continue;
            }

            ModbusSlave slave = modbusSlaves.get(slaveName);
            String slaveValueType = slave.getValueType();
            double rawDataMultiplier = slave.getRawDataMultiplier();

            /* receive data manipulation */
            State newState = extractStateFromRegisters(registers, config.readIndex, slaveValueType);
            // Convert newState (DecimalType) to on/off kind of state if we have "boolean item" (Switch, Contact etc)
            // In other cases (such as Number items) newStateBoolean will be UNDEF
            State newStateBoolean = provider.getConfig(itemName)
                    .translateBoolean2State(!newState.equals(DecimalType.ZERO));
            // If we have boolean item (newStateBoolean is not UNDEF)
            if (!UnDefType.UNDEF.equals(newStateBoolean)) {
                newState = newStateBoolean;
            } else if ((rawDataMultiplier != 1) && (config.getItemClass().isAssignableFrom(NumberItem.class))) {
                double tmpValue = ((DecimalType) newState).doubleValue() * rawDataMultiplier;
                newState = new DecimalType(String.valueOf(tmpValue));
            }

            if (slave.isUpdateUnchangedItems() || !newState.equals(config.getState())) {
                eventPublisher.postUpdate(itemName, newState);
                config.setState(newState);
            }
        }
    }

    /**
     * Read data from registers and convert the result to DecimalType
     * Interpretation of <tt>index</tt> goes as follows depending on type
     *
     * BIT:
     * - a single bit is read from the registers
     * - indices between 0...15 (inclusive) represent bits of the first register
     * - indices between 16...31 (inclusive) represent bits of the second register, etc.
     * - index 0 refers to the least significant bit of the first register
     * - index 1 refers to the second least significant bit of the first register, etc.
     * INT8:
     * - a byte (8 bits) from the registers is interpreted as signed integer
     * - index 0 refers to low byte of the first register, 1 high byte of first register
     * - index 2 refers to low byte of the second register, 3 high byte of second register, etc.
     * - it is assumed that each high and low byte is encoded in most significant bit first order
     * UINT8:
     * - same as INT8 except values are interpreted as unsigned integers
     * INT16:
     * - register with index (counting from zero) is interpreted as 16 bit signed integer.
     * - it is assumed that each register is encoded in most significant bit first order
     * UINT16:
     * - same as INT16 except values are interpreted as unsigned integers
     * INT32:
     * - registers (2 * index) and ( 2 *index + 1) are interpreted as signed 32bit integer.
     * - it assumed that the first register contains the most significant 16 bits
     * - it is assumed that each register is encoded in most significant bit first order
     * UINT32:
     * - same as UINT32 except values are interpreted as unsigned integers
     * FLOAT32:
     * - registers (2 * index) and ( 2 *index + 1) are interpreted as signed 32bit floating point number.
     * - it assumed that the first register contains the most significant 16 bits
     * - it is assumed that each register is encoded in most significant bit first order
     *
     * @param registers
     *            list of registers, each register represent 16bit of data
     * @param index
     *            zero based item index. Interpretation of this depends on type
     * @param type
     *            item type, e.g. unsigned 16bit integer (<tt>ModbusBindingProvider.VALUE_TYPE_UINT16</tt>)
     * @return number representation queried value
     * @throws IllegalArgumentException when <tt>type</tt> does not match a known type
     * @throws IndexOutOfBoundsException when <tt>index</tt> is out of bounds of registers
     *
     */
    private DecimalType extractStateFromRegisters(InputRegister[] registers, int index, String type) {
        if (type.equals(ModbusBindingProvider.VALUE_TYPE_BIT)) {
            return new DecimalType((registers[index / 16].toUnsignedShort() >> (index % 16)) & 1);
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_INT8)) {
            return new DecimalType(registers[index / 2].toBytes()[1 - (index % 2)]);
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_UINT8)) {
            return new DecimalType((registers[index / 2].toUnsignedShort() >> (8 * (index % 2))) & 0xff);
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_INT16)) {
            ByteBuffer buff = ByteBuffer.allocate(2);
            buff.put(registers[index].toBytes());
            return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getShort(0));
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_UINT16)) {
            return new DecimalType(registers[index].toUnsignedShort());
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_INT32)) {
            ByteBuffer buff = ByteBuffer.allocate(4);
            buff.put(registers[index * 2 + 0].toBytes());
            buff.put(registers[index * 2 + 1].toBytes());
            return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getInt(0));
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_UINT32)) {
            ByteBuffer buff = ByteBuffer.allocate(8);
            buff.position(4);
            buff.put(registers[index * 2 + 0].toBytes());
            buff.put(registers[index * 2 + 1].toBytes());
            return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getLong(0));
        } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_FLOAT32)) {
            ByteBuffer buff = ByteBuffer.allocate(4);
            buff.put(registers[index * 2 + 0].toBytes());
            buff.put(registers[index * 2 + 1].toBytes());
            return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getFloat(0));
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Posts update event to OpenHAB bus for "coil" and "discrete input" type slaves
     *
     * @param binding ModbusBinding to get item configuration from BindingProviding
     * @param registers data received from slave device in the last pollInterval
     * @param item item to update
     */
    protected void internalUpdateItem(String slaveName, BitVector coils, String itemName) {
        for (ModbusBindingProvider provider : providers) {
            if (provider.providesBindingFor(itemName)) {
                ModbusBindingConfig config = provider.getConfig(itemName);
                if (config.slaveName.equals(slaveName)) {
                    boolean state = coils.getBit(config.readIndex);
                    State newState = config.translateBoolean2State(state);
                    ModbusSlave slave = modbusSlaves.get(slaveName);
                    if (slave.isUpdateUnchangedItems() || !newState.equals(config.getState())) {
                        eventPublisher.postUpdate(itemName, newState);
                        config.setState(newState);
                    }
                }
            }
        }
    }

    /**
     * Returns names of all the items, registered with this binding
     *
     * @return list of item names
     */
    public Collection<String> getItemNames() {
        Collection<String> items = null;
        for (BindingProvider provider : providers) {
            if (items == null) {
                items = provider.getItemNames();
            } else {
                items.addAll(provider.getItemNames());
            }
        }
        return items;
    }

    /**
     * updates all slaves from the modbusSlaves
     */
    @Override
    protected void execute() {
        Collection<ModbusSlave> slaves = new HashSet<ModbusSlave>();
        synchronized (slaves) {
            slaves.addAll(modbusSlaves.values());
        }
        for (ModbusSlave slave : slaves) {
            slave.update(this);
        }
    }

    /**
     * Clear all configuration and close all connections
     */
    private void clear() {
        try {
            // Closes all connections by calling destroyObject method in the ObjectFactory implementation
            if (connectionPool != null) {
                connectionPool.clear();
            }
        } catch (Exception e) {
            // Should not happen
            logger.error("Error clearing connections", e);
        }
        modbusSlaves.clear();
    }

    @Override
    public void updated(Dictionary<String, ?> config) throws ConfigurationException {
        setProperlyConfigured(false);
        // remove all known items if configuration changed
        clear();
        reconstructConnectionPool();
        if (config == null) {
            logger.warn("Got null config!");
            return;
        }
        Enumeration<String> keys = config.keys();
        Map<String, EndpointPoolConfiguration> slavePoolConfigs = new HashMap<String, EndpointPoolConfiguration>();
        Map<ModbusSlaveEndpoint, EndpointPoolConfiguration> endpointPoolConfigs = new HashMap<ModbusSlaveEndpoint, EndpointPoolConfiguration>();
        while (keys.hasMoreElements()) {
            final String key = keys.nextElement();
            final String value = (String) config.get(key);
            try {
                // the config-key enumeration contains additional keys that we
                // don't want to process here ...
                if ("service.pid".equals(key)) {
                    continue;
                }

                Matcher matcher = EXTRACT_MODBUS_CONFIG_PATTERN.matcher(key);
                if (!matcher.matches()) {
                    if ("poll".equals(key)) {
                        if (StringUtils.isNotBlank((String) config.get(key))) {
                            pollInterval = Integer.valueOf((String) config.get(key));
                        }
                    } else if ("writemultipleregisters".equals(key)) {
                        // XXX: ugly to touch base class but kept here for backwards compat
                        // FIXME: should this be deprecated as introduced as slave specific parameter?
                        ModbusSlave.setWriteMultipleRegisters(Boolean.valueOf(config.get(key).toString()));
                    } else {
                        logger.debug(
                                "given modbus-slave-config-key '{}' does not follow the expected pattern or 'serial.<slaveId>.<{}>'",
                                key, VALID_CONFIG_KEYS);
                    }
                    continue;
                }

                matcher.reset();
                matcher.find();

                String slave = matcher.group(2);

                ModbusSlave modbusSlave = modbusSlaves.get(slave);
                EndpointPoolConfiguration endpointPoolConfig = slavePoolConfigs.get(slave);
                if (modbusSlave == null) {
                    if (matcher.group(1).equals(TCP_PREFIX)) {
                        modbusSlave = new ModbusTcpSlave(slave, connectionPool);
                    } else if (matcher.group(1).equals(UDP_PREFIX)) {
                        modbusSlave = new ModbusUdpSlave(slave, connectionPool);
                    } else if (matcher.group(1).equals(SERIAL_PREFIX)) {
                        modbusSlave = new ModbusSerialSlave(slave, connectionPool);
                    } else {
                        throw new ConfigurationException(slave, "the given slave type '" + slave + "' is unknown");
                    }
                    endpointPoolConfig = new EndpointPoolConfiguration();
                    // Do not give up if the connection attempt fails on the first time...
                    endpointPoolConfig.setConnectMaxTries(Modbus.DEFAULT_RETRIES);
                    logger.debug("modbusSlave '{}' instanciated", slave);
                    modbusSlaves.put(slave, modbusSlave);
                }

                String configKey = matcher.group(3);

                if ("connection".equals(configKey)) {
                    String[] chunks = value.split(":");
                    Iterator<String> settingIterator = stringArrayIterator(chunks);
                    if (modbusSlave instanceof ModbusIPSlave) {
                        ((ModbusIPSlave) modbusSlave).setHost(settingIterator.next());
                        //
                        // Defaults for endpoint and slave
                        //
                        modbusSlave.setRetryDelayMillis(DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS);
                        endpointPoolConfig.setPassivateBorrowMinMillis(DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS);

                        //
                        // Optional parameters
                        //
                        try {
                            ((ModbusIPSlave) modbusSlave).setPort(Integer.valueOf(settingIterator.next()));

                            long passivateBorrowMinMillis = Long.parseLong(settingIterator.next());
                            modbusSlave.setRetryDelayMillis(passivateBorrowMinMillis);
                            endpointPoolConfig.setPassivateBorrowMinMillis(passivateBorrowMinMillis);

                            endpointPoolConfig.setReconnectAfterMillis(Integer.parseInt(settingIterator.next()));

                            // time to wait before trying connect closed connection. Note that
                            // ModbusSlaveConnectionFactoryImpl makes sure that max{passivateBorrowMinMillis, this
                            // parameter} is waited between connection attempts
                            endpointPoolConfig.setInterConnectDelayMillis(Long.parseLong(settingIterator.next()));

                            endpointPoolConfig.setConnectMaxTries(Integer.parseInt(settingIterator.next()));
                        } catch (NoSuchElementException e) {
                            // Some of the optional parameters are missing -- it's ok!
                        }
                        if (settingIterator.hasNext()) {
                            String errMsg = String
                                    .format("%s Has too many colon (:) separated connection settings for a tcp/udp modbus slave. "
                                            + "Expecting at most 6 parameters: hostname (mandatory) and "
                                            + "optionally (in this order) port number, "
                                            + "interTransactionDelayMillis, reconnectAfterMillis,"
                                            + "interConnectDelayMillis, connectMaxTries.", key);
                            throw new ConfigurationException(key, errMsg);
                        }
                    } else if (modbusSlave instanceof ModbusSerialSlave) {
                        SerialParameters serialParameters = new SerialParameters();
                        serialParameters.setPortName(settingIterator.next());
                        //
                        // Defaults for endpoint and slave
                        //
                        endpointPoolConfig.setReconnectAfterMillis(-1); // never "disconnect" (close/open serial port)
                                                                        // serial connection between borrows
                        modbusSlave.setRetryDelayMillis(DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS);
                        endpointPoolConfig.setPassivateBorrowMinMillis(DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS);

                        //
                        // Optional parameters
                        //
                        try {
                            serialParameters.setBaudRate(settingIterator.next());
                            serialParameters.setDatabits(settingIterator.next());
                            serialParameters.setParity(settingIterator.next());
                            serialParameters.setStopbits(settingIterator.next());
                            serialParameters.setEncoding(settingIterator.next());

                            // time to wait between connection passive+borrow, i.e. time to wait between
                            // transactions
                            long passivateBorrowMinMillis = Long.parseLong(settingIterator.next());
                            modbusSlave.setRetryDelayMillis(passivateBorrowMinMillis);
                            endpointPoolConfig.setPassivateBorrowMinMillis(passivateBorrowMinMillis);

                            serialParameters.setReceiveTimeoutMillis(settingIterator.next());
                            serialParameters.setFlowControlIn(settingIterator.next());
                            serialParameters.setFlowControlOut(settingIterator.next());
                        } catch (NoSuchElementException e) {
                            // Some of the optional parameters are missing -- it's ok!
                        }
                        if (settingIterator.hasNext()) {
                            String errMsg = String.format(
                                    "%s Has too many colon (:) separated connection settings for a serial modbus slave. "
                                            + "Expecting at most 9 parameters (got %d): devicePort (mandatory), "
                                            + "and 0 or more optional parameters (in this order): "
                                            + "baudRate, dataBits, parity, stopBits, "
                                            + "encoding, interTransactionWaitMillis, "
                                            + "receiveTimeoutMillis, flowControlIn, flowControlOut",
                                    key, chunks.length);
                            throw new ConfigurationException(key, errMsg);
                        }

                        ((ModbusSerialSlave) modbusSlave).setSerialParameters(serialParameters);
                    }
                } else if ("start".equals(configKey)) {
                    modbusSlave.setStart(Integer.valueOf(value));
                } else if ("length".equals(configKey)) {
                    modbusSlave.setLength(Integer.valueOf(value));
                } else if ("id".equals(configKey)) {
                    modbusSlave.setId(Integer.valueOf(value));
                } else if ("type".equals(configKey)) {
                    if (ArrayUtils.contains(ModbusBindingProvider.SLAVE_DATA_TYPES, value)) {
                        modbusSlave.setType(value);
                    } else {
                        throw new ConfigurationException(configKey, "the given slave type '" + value + "' is invalid");
                    }
                } else if ("valuetype".equals(configKey)) {
                    if (ArrayUtils.contains(ModbusBindingProvider.VALUE_TYPES, value)) {
                        modbusSlave.setValueType(value);
                    } else {
                        throw new ConfigurationException(configKey, "the given value type '" + value + "' is invalid");
                    }
                } else if ("rawdatamultiplier".equals(configKey)) {
                    modbusSlave.setRawDataMultiplier(Double.valueOf(value.toString()));
                } else if ("updateunchangeditems".equals(configKey)) {
                    modbusSlave.setUpdateUnchangedItems(Boolean.valueOf(value.toString()));
                } else {
                    throw new ConfigurationException(configKey, "the given configKey '" + configKey + "' is unknown");
                }
                modbusSlaves.put(slave, modbusSlave);
                slavePoolConfigs.put(slave, endpointPoolConfig);
            } catch (Exception e) {
                String errMsg = String.format("Exception when parsing configuration parameter %s = %s  --  %s %s", key,
                        value, e.getClass().getName(), e.getMessage());
                logger.error(errMsg);
                throw new ConfigurationException(key, errMsg);
            }
        }
        // Finally, go through each slave definition, and combine the slave pool configurations
        for (Entry<String, EndpointPoolConfiguration> slaveEntry : slavePoolConfigs.entrySet()) {
            String slave = slaveEntry.getKey();
            EndpointPoolConfiguration poolConfiguration = slaveEntry.getValue();
            ModbusSlaveEndpoint endpoint = modbusSlaves.get(slave).getEndpoint();
            EndpointPoolConfiguration existingPoolConfiguration = endpointPoolConfigs.get(endpoint);
            // Do we have two slaves with same endpoint, but different pool configuration parameters? Warn if we do.
            if (existingPoolConfiguration != null && !existingPoolConfiguration.equals(poolConfiguration)) {
                logger.warn(
                        "Slave {} (endpoint {}) has different retry/connection delay "
                                + "(EndpointPoolConfiguration) etc. settings. Replacing {} with {}",
                        slave, endpoint, existingPoolConfiguration, poolConfiguration);
            }
            endpointPoolConfigs.put(endpoint, poolConfiguration);
        }
        connectionFactory.applyEndpointPoolConfigs(endpointPoolConfigs);
        logger.debug("Parsed the following slave->endpoint configurations: {}. If the endpoint is same, "
                + "connections are shared between the instances.", slavePoolConfigs);
        logger.debug("Parsed the following pool configurations: {}", endpointPoolConfigs);
        logger.debug("config looked good");
        setProperlyConfigured(true);

    }

    private static Iterator<String> stringArrayIterator(final String[] chunks) {
        Iterator<String> settingIterator = new Iterator<String>() {

            private ResettableIterator inner = IteratorUtils.arrayIterator(chunks);

            @Override
            public String next() {
                return (String) inner.next();
            }

            @Override
            public boolean hasNext() {
                return inner.hasNext();
            }

            @Override
            public void remove() {
                inner.remove();
            }

        };
        return settingIterator;
    }

}
{'fraction_non_alphanumeric': 0.08447857061345593, 'fraction_numerical': 0.005493878482580982, 'mean_word_length': 2.1098605830164767, 'pattern_counts': {'":': 2, '<': 69, '<?xml version=': 0, '>': 79, 'https://': 6, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 52, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '382731', 'n_tokens_mistral': 15188, 'n_tokens_neox': 14547, 'n_words': 3852}