gt:         if (lastOperation == null) {
context: /**
   * Copyright (c) 2010-2020 Contributors to the openHAB project
   *
   * See the NOTICE file(s) distributed with this work for additional
   * information.
   *
   * This program and the accompanying materials are made available under the
   * terms of the Eclipse Public License 2.0 which is available at
   * http://www.eclipse.org/legal/epl-2.0
   *
   * SPDX-License-Identifier: EPL-2.0
   */
  package org.openhab.binding.modbus.internal.pooling;
  
  import java.net.InetAddress;
  import java.net.UnknownHostException;
  import java.util.Map;
  import java.util.concurrent.ConcurrentHashMap;
  
  import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
  import org.apache.commons.pool2.PooledObject;
  import org.apache.commons.pool2.impl.DefaultPooledObject;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import net.wimpi.modbus.net.ModbusSlaveConnection;
  import net.wimpi.modbus.net.SerialConnection;
  import net.wimpi.modbus.net.TCPMasterConnection;
  import net.wimpi.modbus.net.UDPMasterConnection;
  
  /**
   * ModbusSlaveConnectionFactoryImpl responsible of the lifecycle of modbus slave connections
   *
   * The actual pool uses instance of this class to create and destroy connections as-needed.
   *
   * The overall functionality goes as follow
   * - create: create connection object but do not connect it yet
   * - destroyObject: close connection and free all resources. Called by the pool when the pool is being closed or the
   * object is invalidated.
   * - activateObject: prepare connection to be used. In practice, connect if disconnected
   * - passivateObject: passivate connection before returning it back to the pool. Currently, passivateObject closes all
   * IP-based connections every now and then (reconnectAfterMillis). Serial connections we keep open.
   * - wrap: wrap created connection to pooled object wrapper class. It tracks usage statistics and last connection time.
   *
   * Note that the implementation must be thread safe.
   *
   */
  public class ModbusSlaveConnectionFactoryImpl
          extends BaseKeyedPooledObjectFactory<ModbusSlaveEndpoint, ModbusSlaveConnection> {
  
      private static class PooledConnection extends DefaultPooledObject<ModbusSlaveConnection> {
  
          private long lastConnected;
  
          public PooledConnection(ModbusSlaveConnection object) {
              super(object);
          }
  
          public long getLastConnected() {
              return lastConnected;
          }
  
          public void setLastConnected(long lastConnected) {
              this.lastConnected = lastConnected;
          }
  
      }
  
      private static final Logger logger = LoggerFactory.getLogger(ModbusSlaveConnectionFactoryImpl.class);
      private volatile Map<ModbusSlaveEndpoint, EndpointPoolConfiguration> endpointPoolConfigs = new ConcurrentHashMap<>();
      private volatile Map<ModbusSlaveEndpoint, Long> lastPassivateMillis = new ConcurrentHashMap<>();
      private volatile Map<ModbusSlaveEndpoint, Long> lastConnectMillis = new ConcurrentHashMap<>();
  
      private InetAddress getInetAddress(ModbusIPSlaveEndpoint key) {
          try {
              return InetAddress.getByName(key.getAddress());
          } catch (UnknownHostException e) {
              logger.error("KeyedPooledModbusSlaveConnectionFactory: Unknown host: {}. Connection creation failed.",
                      e.getMessage());
              return null;
          }
      }
  
      @Override
      public ModbusSlaveConnection create(ModbusSlaveEndpoint endpoint) throws Exception {
          return endpoint.accept(new ModbusSlaveEndpointVisitor<ModbusSlaveConnection>() {
              @Override
              public ModbusSlaveConnection visit(ModbusSerialSlaveEndpoint modbusSerialSlavePoolingKey) {
                  SerialConnection connection = new SerialConnection(modbusSerialSlavePoolingKey.getSerialParameters());
                  logger.trace("Created connection {} for endpoint {}", connection, modbusSerialSlavePoolingKey);
                  return connection;
              }
  
              @Override
              public ModbusSlaveConnection visit(ModbusTCPSlaveEndpoint key) {
                  InetAddress address = getInetAddress(key);
                  if (address == null) {
                      return null;
                  }
                  EndpointPoolConfiguration config = endpointPoolConfigs.get(key);
                  int connectTimeoutMillis = 0;
                  if (config != null) {
                      connectTimeoutMillis = config.getConnectTimeoutMillis();
                  }
                  TCPMasterConnection connection = new TCPMasterConnection(address, key.getPort(), connectTimeoutMillis);
                  logger.trace("Created connection {} for endpoint {}", connection, key);
                  return connection;
              }
  
              @Override
              public ModbusSlaveConnection visit(ModbusUDPSlaveEndpoint key) {
                  InetAddress address = getInetAddress(key);
                  if (address == null) {
                      return null;
                  }
                  UDPMasterConnection connection = new UDPMasterConnection(address, key.getPort());
                  logger.trace("Created connection {} for endpoint {}", connection, key);
                  return connection;
              }
          });
      }
  
      @Override
      public PooledObject<ModbusSlaveConnection> wrap(ModbusSlaveConnection connection) {
          return new PooledConnection(connection);
      }
  
      @Override
      public void destroyObject(ModbusSlaveEndpoint endpoint, final PooledObject<ModbusSlaveConnection> obj) {
          logger.trace("destroyObject for connection {} and endpoint {} -> closing the connection", obj.getObject(),
                  endpoint);
          obj.getObject().resetConnection();
      }
  
      @Override
      public void activateObject(ModbusSlaveEndpoint endpoint, PooledObject<ModbusSlaveConnection> obj) throws Exception {
          if (obj.getObject() == null) {
              return;
          }
          try {
              ModbusSlaveConnection connection = obj.getObject();
              EndpointPoolConfiguration config = endpointPoolConfigs.get(endpoint);
  
              if (connection.isConnected()) {
                  if (config != null) {
                      long waited = waitAtleast(lastPassivateMillis.get(endpoint), config.getPassivateBorrowMinMillis());
                      logger.trace(
                              "Waited {}ms (passivateBorrowMinMillis {}ms) before giving returning connection {} for endpoint {}, to ensure delay between transactions.",
                              waited, config.getPassivateBorrowMinMillis(), obj.getObject(), endpoint);
                  }
              } else {
                  // invariant: !connection.isConnected()
                  tryConnect(endpoint, obj, connection, config);
              }
          } catch (Exception e) {
              logger.error("Error connecting connection {} for endpoint {}: {}", obj.getObject(), endpoint,
                      e.getMessage());
          }
      }
  
      @Override
      public void passivateObject(ModbusSlaveEndpoint endpoint, PooledObject<ModbusSlaveConnection> obj) {
          ModbusSlaveConnection connection = obj.getObject();
          if (connection == null) {
              return;
          }
          logger.trace("Passivating connection {} for endpoint {}...", connection, endpoint);
          lastPassivateMillis.put(endpoint, System.currentTimeMillis());
          EndpointPoolConfiguration configuration = endpointPoolConfigs.get(endpoint);
          long reconnectAfterMillis = configuration == null ? 0 : configuration.getReconnectAfterMillis();
          long connectionAgeMillis = System.currentTimeMillis() - ((PooledConnection) obj).getLastConnected();
          if (reconnectAfterMillis == 0 || (reconnectAfterMillis > 0 && connectionAgeMillis > reconnectAfterMillis)) {
              logger.trace(
                      "(passivate) Connection {} (endpoint {}) age {}ms is over the reconnectAfterMillis={}ms limit -> disconnecting.",
                      connection, endpoint, connectionAgeMillis, reconnectAfterMillis);
              connection.resetConnection();
          } else {
              logger.trace(
                      "(passivate) Connection {} (endpoint {}) age ({}ms) is below the reconnectAfterMillis ({}ms) limit. Keep the connection open.",
                      connection, endpoint, connectionAgeMillis, reconnectAfterMillis);
          }
          logger.trace("...Passivated connection {} for endpoint {}", obj.getObject(), endpoint);
      }
  
      @Override
      public boolean validateObject(ModbusSlaveEndpoint key, PooledObject<ModbusSlaveConnection> p) {
          boolean valid = p.getObject() != null && p.getObject().isConnected();
          logger.trace("Validating endpoint {} connection {} -> {}", key, p.getObject(), valid);
          return valid;
      }
  
      public Map<ModbusSlaveEndpoint, EndpointPoolConfiguration> getEndpointPoolConfigs() {
          return endpointPoolConfigs;
      }
  
      public void applyEndpointPoolConfigs(Map<ModbusSlaveEndpoint, EndpointPoolConfiguration> endpointPoolConfigs) {
          this.endpointPoolConfigs = new ConcurrentHashMap<>(endpointPoolConfigs);
      }
  
      private void tryConnect(ModbusSlaveEndpoint endpoint, PooledObject<ModbusSlaveConnection> obj,
              ModbusSlaveConnection connection, EndpointPoolConfiguration config) throws Exception {
          if (connection.isConnected()) {
              return;
          }
          int tryIndex = 0;
          Long lastConnect = lastConnectMillis.get(endpoint);
          int maxTries = config == null ? 1 : config.getConnectMaxTries();
          do {
              try {
                  if (config != null) {
                      long waited = waitAtleast(lastConnect,
                              Math.max(config.getInterConnectDelayMillis(), config.getPassivateBorrowMinMillis()));
                      if (waited > 0) {
                          logger.trace(
                                  "Waited {}ms (interConnectDelayMillis {}ms, passivateBorrowMinMillis {}ms) before "
                                          + "connecting disconnected connection {} for endpoint {}, to allow delay "
                                          + "between connections re-connects",
                                  waited, config.getInterConnectDelayMillis(), config.getPassivateBorrowMinMillis(),
                                  obj.getObject(), endpoint);
                      }
  
                  }
                  connection.connect();
                  long curTime = System.currentTimeMillis();
                  ((PooledConnection) obj).setLastConnected(curTime);
                  lastConnectMillis.put(endpoint, curTime);
                  break;
              } catch (Exception e) {
                  tryIndex++;
                  logger.error("connect try {}/{} error: {}. Connection {}. Endpoint {}", tryIndex, maxTries,
                          e.getMessage(), connection, endpoint);
                  if (tryIndex >= maxTries) {
                      logger.error("re-connect reached max tries {}, throwing last error: {}. Connection {}. Endpoint {}",
                              maxTries, e.getMessage(), connection, endpoint);
                      throw e;
                  }
                  lastConnect = System.currentTimeMillis();
              }
          } while (true);
      }
  
      private long waitAtleast(Long lastOperation, long waitMillis) {
  