org.jumpmind.symmetric.service.impl.NodeService.java Source code

Java tutorial

Introduction

Here is the source code for org.jumpmind.symmetric.service.impl.NodeService.java

Source

/**
 * Licensed to JumpMind Inc under one or more contributor
 * license agreements.  See the NOTICE file distributed
 * with this work for additional information regarding
 * copyright ownership.  JumpMind Inc licenses this file
 * to you under the GNU General Public License, version 3.0 (GPLv3)
 * (the "License"); you may not use this file except in compliance
 * with the License.
 *
 * You should have received a copy of the GNU General Public License,
 * version 3.0 (GPLv3) along with this library; if not, see
 * <http://www.gnu.org/licenses/>.
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.jumpmind.symmetric.service.impl;

import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.jumpmind.db.sql.ISqlRowMapper;
import org.jumpmind.db.sql.ISqlTransaction;
import org.jumpmind.db.sql.Row;
import org.jumpmind.db.sql.SqlException;
import org.jumpmind.db.sql.UniqueKeyException;
import org.jumpmind.db.sql.mapper.StringMapper;
import org.jumpmind.security.ISecurityService;
import org.jumpmind.symmetric.common.ParameterConstants;
import org.jumpmind.symmetric.config.INodeIdCreator;
import org.jumpmind.symmetric.db.ISymmetricDialect;
import org.jumpmind.symmetric.ext.IOfflineServerListener;
import org.jumpmind.symmetric.model.NetworkedNode;
import org.jumpmind.symmetric.model.Node;
import org.jumpmind.symmetric.model.NodeGroupLinkAction;
import org.jumpmind.symmetric.model.NodeHost;
import org.jumpmind.symmetric.model.NodeSecurity;
import org.jumpmind.symmetric.model.NodeStatus;
import org.jumpmind.symmetric.security.INodePasswordFilter;
import org.jumpmind.symmetric.service.IExtensionService;
import org.jumpmind.symmetric.service.INodeService;
import org.jumpmind.symmetric.service.IParameterService;
import org.jumpmind.symmetric.util.DefaultNodeIdCreator;
import org.jumpmind.util.AppUtils;

/**
 * @see INodeService
 */
public class NodeService extends AbstractService implements INodeService {

    private IExtensionService extensionService;

    private Node cachedNodeIdentity;

    private Map<String, NodeSecurity> securityCache;

    private long securityCacheTime;

    private INodePasswordFilter nodePasswordFilter;

    private NodeHost nodeHostForCurrentNode = null;

    public NodeService(IParameterService parameterService, ISymmetricDialect dialect,
            ISecurityService securityService, IExtensionService extensionService) {
        super(parameterService, dialect);
        this.extensionService = extensionService;
        extensionService.addExtensionPoint(new DefaultNodeIdCreator(parameterService, this, securityService));
        setSqlMap(new NodeServiceSqlMap(symmetricDialect.getPlatform(), createSqlReplacementTokens()));
    }

    public String findSymmetricVersion() {
        return (String) sqlTemplate.queryForObject(getSql("findSymmetricVersionSql"), String.class);
    }

    public String findIdentityNodeId() {
        Node node = findIdentity();
        return node != null ? node.getNodeId() : null;
    }

    public Collection<Node> findEnabledNodesFromNodeGroup(String nodeGroupId) {
        return sqlTemplate.query(getSql("selectNodePrefixSql", "findEnabledNodesFromNodeGroupSql"),
                new NodeRowMapper(), new Object[] { nodeGroupId });
    }

    public Set<Node> findNodesThatOriginatedFromNodeId(String originalNodeId) {
        return findNodesThatOriginatedFromNodeId(originalNodeId, true);
    }

    public Collection<Node> findNodesWithOpenRegistration() {
        return sqlTemplate.query(getSql("selectNodePrefixSql", "findNodesWithOpenRegistrationSql"),
                new NodeRowMapper());
    }

    public Set<Node> findNodesThatOriginatedFromNodeId(String originalNodeId, boolean recursive) {
        Set<Node> all = new HashSet<Node>();
        List<Node> list = sqlTemplate.query(getSql("selectNodePrefixSql", "findNodesCreatedByMeSql"),
                new NodeRowMapper(), originalNodeId);
        if (list.size() > 0) {
            all.addAll(list);
            if (recursive) {
                for (Node node : list) {
                    all.addAll(findNodesThatOriginatedFromNodeId(node.getNodeId()));
                }
            }
        }
        return all;
    }

    /**
     * Lookup a node in the database, which contains information for syncing
     * with it.
     */
    public Node findNode(String id) {
        List<Node> list = sqlTemplate.query(getSql("selectNodePrefixSql", "findNodeSql"), new NodeRowMapper(), id);
        return (Node) getFirstEntry(list);
    }

    public Node findNodeByExternalId(String nodeGroupId, String externalId) {
        List<Node> list = sqlTemplate.query(getSql("selectNodePrefixSql", "findNodeByExternalIdSql"),
                new NodeRowMapper(), nodeGroupId, externalId);
        return (Node) getFirstEntry(list);
    }

    public void ignoreNodeChannelForExternalId(boolean enabled, String channelId, String nodeGroupId,
            String externalId) {
        Node node = findNodeByExternalId(nodeGroupId, externalId);
        if (sqlTemplate.update(getSql("nodeChannelControlIgnoreSql"),
                new Object[] { enabled ? 1 : 0, node.getNodeId(), channelId }) == 0) {
            sqlTemplate.update(getSql("insertNodeChannelControlSql"),
                    new Object[] { node.getNodeId(), channelId, enabled ? 1 : 0, 0 });
        }
    }

    public boolean isRegistrationEnabled(String nodeId) {
        NodeSecurity nodeSecurity = findNodeSecurity(nodeId);
        if (nodeSecurity != null) {
            return nodeSecurity.isRegistrationEnabled();
        }
        return false;
    }

    /**
     * Lookup a node_security in the database, which contains private
     * information used to authenticate.
     */
    public NodeSecurity findNodeSecurity(String id) {
        return findNodeSecurity(id, false);
    }

    public List<NodeHost> findNodeHosts(String nodeId) {
        return sqlTemplate.query(getSql("selectNodeHostPrefixSql", "selectNodeHostByNodeIdSql"),
                new NodeHostRowMapper(), nodeId);
    }

    public void deleteNodeHost(String nodeId) {
        platform.getSqlTemplate().update(getSql("deleteNodeHostSql"), new Object[] { nodeId });
    }

    public void updateNodeHost(NodeHost nodeHost) {

        Object[] params = new Object[] { nodeHost.getIpAddress(), nodeHost.getOsUser(), nodeHost.getOsName(),
                nodeHost.getOsArch(), nodeHost.getOsVersion(), nodeHost.getAvailableProcessors(),
                nodeHost.getFreeMemoryBytes(), nodeHost.getTotalMemoryBytes(), nodeHost.getMaxMemoryBytes(),
                nodeHost.getJavaVersion(), nodeHost.getJavaVendor(), nodeHost.getJdbcVersion(),
                nodeHost.getSymmetricVersion(), nodeHost.getTimezoneOffset(), nodeHost.getHeartbeatTime(),
                nodeHost.getLastRestartTime(), nodeHost.getNodeId(), nodeHost.getHostName() };

        if (sqlTemplate.update(getSql("updateNodeHostSql"), params) == 0) {
            sqlTemplate.update(getSql("insertNodeHostSql"), params);
        }

    }

    public void updateNodeHostForCurrentNode() {
        if (nodeHostForCurrentNode == null) {
            nodeHostForCurrentNode = new NodeHost(findIdentityNodeId());
        }
        nodeHostForCurrentNode.refresh(platform);
        updateNodeHost(nodeHostForCurrentNode);
    }

    public NodeSecurity findNodeSecurity(String nodeId, boolean createIfNotFound) {
        try {
            if (nodeId != null) {
                List<NodeSecurity> list = sqlTemplate.query(getSql("findNodeSecuritySql"),
                        new NodeSecurityRowMapper(), new Object[] { nodeId }, new int[] { Types.VARCHAR });
                NodeSecurity security = (NodeSecurity) getFirstEntry(list);
                if (security == null && createIfNotFound) {
                    insertNodeSecurity(nodeId);
                    security = findNodeSecurity(nodeId, false);
                }
                return security;
            } else {
                log.debug("A 'null' node id was passed into findNodeSecurity");
                return null;
            }
        } catch (UniqueKeyException ex) {
            log.error("Could not find a node security row for '{}'", nodeId);
            throw ex;
        }
    }

    public void deleteNodeSecurity(String nodeId) {
        sqlTemplate.update(getSql("deleteNodeSecuritySql"), new Object[] { nodeId });
    }

    public void deleteNode(String nodeId, boolean syncChange) {
        ISqlTransaction transaction = null;
        try {
            transaction = sqlTemplate.startSqlTransaction();
            if (!syncChange) {
                symmetricDialect.disableSyncTriggers(transaction, nodeId);
            }
            if (StringUtils.isNotBlank(nodeId)) {
                if (nodeId.equals(findIdentityNodeId())) {
                    transaction.prepareAndExecute(getSql("deleteNodeIdentitySql"));
                }
                transaction.prepareAndExecute(getSql("deleteNodeSecuritySql"), new Object[] { nodeId });
                transaction.prepareAndExecute(getSql("deleteNodeHostSql"), new Object[] { nodeId });
                transaction.prepareAndExecute(getSql("deleteNodeSql"), new Object[] { nodeId });
                transaction.prepareAndExecute(getSql("deleteNodeChannelCtlSql"), new Object[] { nodeId });
                transaction.prepareAndExecute(getSql("deleteIncomingErrorSql"), new Object[] { nodeId });
                transaction.prepareAndExecute(getSql("deleteExtractRequestSql"), new Object[] { nodeId });
                transaction.prepareAndExecute(getSql("deleteNodeCommunicationSql"), new Object[] { nodeId });
                transaction.prepareAndExecute(getSql("deleteTableReloadRequestSql"),
                        new Object[] { nodeId, nodeId });
                transaction.prepareAndExecute(getSql("setOutgoingBatchOkSql"), new Object[] { nodeId });
                transaction.prepareAndExecute(getSql("deleteIncomingBatchSql"), new Object[] { nodeId });
            }
        } catch (Error ex) {
            if (transaction != null) {
                transaction.rollback();
            }
            throw ex;
        } catch (RuntimeException ex) {
            if (transaction != null) {
                transaction.rollback();
            }
            throw ex;
        } finally {
            if (!syncChange) {
                symmetricDialect.enableSyncTriggers(transaction);
            }

            close(transaction);
        }
    }

    public void insertNodeSecurity(String id) {
        flushNodeAuthorizedCache();
        String password = extensionService.getExtensionPoint(INodeIdCreator.class)
                .generatePassword(new Node(id, null, null));
        password = filterPasswordOnSaveIfNeeded(password);
        sqlTemplate.update(getSql("insertNodeSecuritySql"), new Object[] { id, password, null });
    }

    public void insertNodeIdentity(String nodeId) {
        sqlTemplate.update(getSql("insertNodeIdentitySql"), nodeId);
    }

    public boolean deleteIdentity() {
        boolean successful = false;
        try {
            sqlTemplate.update(getSql("deleteNodeIdentitySql"));
            successful = true;
        } catch (SqlException ex) {
            log.debug(ex.getMessage());
        } finally {
            cachedNodeIdentity = null;
        }
        return successful;
    }

    public void insertNodeGroup(String groupId, String description) {
        if (sqlTemplate.queryForInt(getSql("doesNodeGroupExistSql"), groupId) == 0) {
            sqlTemplate.update(getSql("insertNodeGroupSql"), description, groupId);
        }
    }

    public void save(Node node) {
        if (!updateNode(node)) {
            sqlTemplate.update(getSql("insertNodeSql"),
                    new Object[] { node.getNodeGroupId(), node.getExternalId(), node.getDatabaseType(),
                            node.getDatabaseVersion(), node.getSchemaVersion(), node.getSymmetricVersion(),
                            node.getSyncUrl(), new Date(), node.isSyncEnabled() ? 1 : 0,
                            AppUtils.getTimezoneOffset(), node.getBatchToSendCount(), node.getBatchInErrorCount(),
                            node.getCreatedAtNodeId(), node.getDeploymentType(), node.getNodeId() },
                    new int[] { Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR,
                            Types.VARCHAR, Types.VARCHAR, Types.TIMESTAMP, Types.INTEGER, Types.VARCHAR,
                            Types.INTEGER, Types.INTEGER, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR });
        }
    }

    public boolean updateNode(Node node) {
        boolean updated = sqlTemplate.update(getSql("updateNodeSql"),
                new Object[] { node.getNodeGroupId(), node.getExternalId(), node.getDatabaseType(),
                        node.getDatabaseVersion(), node.getSchemaVersion(), node.getSymmetricVersion(),
                        node.getSyncUrl(), new Date(), node.isSyncEnabled() ? 1 : 0, AppUtils.getTimezoneOffset(),
                        node.getBatchToSendCount(), node.getBatchInErrorCount(), node.getCreatedAtNodeId(),
                        node.getDeploymentType(), node.getNodeId() },
                new int[] { Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR,
                        Types.VARCHAR, Types.VARCHAR, Types.TIMESTAMP, Types.INTEGER, Types.VARCHAR, Types.INTEGER,
                        Types.INTEGER, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR }) == 1;
        return updated;
    }

    protected <T> T getFirstEntry(List<T> list) {
        if (list != null && list.size() > 0) {
            return list.get(0);
        }
        return null;
    }

    public List<NodeSecurity> findNodeSecurityWithLoadEnabled() {
        return sqlTemplate.query(getSql("findNodeSecurityWithLoadEnabledSql"), new NodeSecurityRowMapper());
    }

    public Map<String, NodeSecurity> findAllNodeSecurity(boolean useCache) {
        long maxSecurityCacheTime = parameterService.getLong(ParameterConstants.CACHE_TIMEOUT_NODE_SECURITY_IN_MS);
        Map<String, NodeSecurity> all = securityCache;
        if (all == null || System.currentTimeMillis() - securityCacheTime >= maxSecurityCacheTime
                || securityCacheTime == 0 || !useCache) {
            all = (Map<String, NodeSecurity>) sqlTemplate.queryForMap(getSql("findAllNodeSecuritySql"),
                    new NodeSecurityRowMapper(), "node_id");
            securityCache = all;
            securityCacheTime = System.currentTimeMillis();
        }
        return all;
    }

    /**
     * Check that the given node and password match in the node_security table.
     * A node must authenticate before it's allowed to sync data.
     */
    public boolean isNodeAuthorized(String nodeId, String password) {
        Map<String, NodeSecurity> nodeSecurities = findAllNodeSecurity(true);
        NodeSecurity nodeSecurity = nodeSecurities.get(nodeId);
        if (nodeSecurity != null && !nodeId.equals(findIdentityNodeId())
                && ((nodeSecurity.getNodePassword() != null && !nodeSecurity.getNodePassword().equals("")
                        && nodeSecurity.getNodePassword().equals(password))
                        || nodeSecurity.isRegistrationEnabled())) {
            return true;
        }
        return false;
    }

    public void flushNodeAuthorizedCache() {
        securityCacheTime = 0;
    }

    public Node getCachedIdentity() {
        return cachedNodeIdentity;
    }

    public Node findIdentity() {
        return findIdentity(true);
    }

    public Node findIdentity(boolean useCache) {
        return findIdentity(useCache, true);
    }

    public Node findIdentity(boolean useCache, boolean logSqlError) {
        if (cachedNodeIdentity == null || useCache == false) {
            try {
                List<Node> list = sqlTemplate.query(getSql("selectNodePrefixSql", "findNodeIdentitySql"),
                        new NodeRowMapper());
                cachedNodeIdentity = (Node) getFirstEntry(list);
            } catch (SqlException ex) {
                if (logSqlError) {
                    log.info("Failed to load the node identity because: {}.  Returning {}", ex.getMessage(),
                            cachedNodeIdentity);
                }
            }
        }
        return cachedNodeIdentity;
    }

    public List<Node> findNodesToPull() {
        return findSourceNodesFor(NodeGroupLinkAction.W);
    }

    public List<Node> findNodesToPushTo() {
        return findTargetNodesFor(NodeGroupLinkAction.P);
    }

    public List<Node> findSourceNodesFor(NodeGroupLinkAction eventAction) {
        Node node = findIdentity();
        if (node != null) {
            return sqlTemplate.query(getSql("selectNodePrefixSql", "findNodesWhoTargetMeSql"), new NodeRowMapper(),
                    node.getNodeGroupId(), eventAction.name());
        } else {
            return Collections.emptyList();
        }
    }

    public List<Node> findTargetNodesFor(NodeGroupLinkAction eventAction) {
        Node node = findIdentity();
        if (node != null) {
            return sqlTemplate.query(getSql("selectNodePrefixSql", "findNodesWhoITargetSql"), new NodeRowMapper(),
                    node.getNodeGroupId(), eventAction.name());
        } else {
            return Collections.emptyList();
        }
    }

    public List<String> findAllExternalIds() {
        return sqlTemplate.query(getSql("selectExternalIdsSql"), new StringMapper());
    }

    public List<Node> findAllNodes() {
        return sqlTemplate.query(getSql("selectNodePrefixSql"), new NodeRowMapper());
    }

    public Map<String, Node> findAllNodesAsMap() {
        List<Node> nodes = findAllNodes();
        Map<String, Node> nodeMap = new HashMap<String, Node>(nodes.size());
        for (Node node : nodes) {
            nodeMap.put(node.getNodeId(), node);
        }
        return nodeMap;
    }

    public NetworkedNode getRootNetworkedNode() {
        Map<String, Node> nodes = findAllNodesAsMap();
        Map<String, NetworkedNode> leaves = new HashMap<String, NetworkedNode>(nodes.size());
        NetworkedNode nodeLeaf = null;
        for (Node node : nodes.values()) {
            nodeLeaf = leaves.get(node.getNodeId());
            if (nodeLeaf == null) {
                nodeLeaf = new NetworkedNode(node);
                nodeLeaf.addParents(nodes, leaves);
                leaves.put(node.getNodeId(), nodeLeaf);
            }
        }

        nodeLeaf = leaves.get(findIdentityNodeId());
        if (nodeLeaf != null) {
            NetworkedNode root = nodeLeaf.getRoot();
            root.setAllNetworkedNodes(leaves);
            return root;
        } else {
            return null;
        }
    }

    public boolean updateNodeSecurity(NodeSecurity security) {
        ISqlTransaction transaction = null;
        try {
            transaction = sqlTemplate.startSqlTransaction();
            boolean updated = updateNodeSecurity(transaction, security);
            transaction.commit();
            return updated;
        } catch (Error ex) {
            if (transaction != null) {
                transaction.rollback();
            }
            throw ex;
        } catch (RuntimeException ex) {
            if (transaction != null) {
                transaction.rollback();
            }
            throw ex;
        } finally {
            close(transaction);
        }
    }

    public boolean updateNodeSecurity(ISqlTransaction transaction, NodeSecurity security) {
        security.setNodePassword(filterPasswordOnSaveIfNeeded(security.getNodePassword()));
        boolean updated = transaction.prepareAndExecute(getSql("updateNodeSecuritySql"),
                new Object[] { security.getNodePassword(), security.isRegistrationEnabled() ? 1 : 0,
                        security.getRegistrationTime(), security.isInitialLoadEnabled() ? 1 : 0,
                        security.getInitialLoadTime(), security.getCreatedAtNodeId(),
                        security.isRevInitialLoadEnabled() ? 1 : 0, security.getRevInitialLoadTime(),
                        security.getInitialLoadId(), security.getInitialLoadCreateBy(),
                        security.getRevInitialLoadId(), security.getRevInitialLoadCreateBy(),
                        security.getNodeId() },
                new int[] { Types.VARCHAR, Types.INTEGER, Types.TIMESTAMP, Types.INTEGER, Types.TIMESTAMP,
                        Types.VARCHAR, Types.INTEGER, Types.TIMESTAMP, Types.BIGINT, Types.VARCHAR, Types.BIGINT,
                        Types.VARCHAR, Types.VARCHAR }) == 1;
        flushNodeAuthorizedCache();
        return updated;
    }

    public boolean setInitialLoadEnabled(ISqlTransaction transaction, String nodeId, boolean initialLoadEnabled,
            boolean syncChange, long loadId, String createBy) {
        try {
            if (!syncChange) {
                symmetricDialect.disableSyncTriggers(transaction, nodeId);
            }
            NodeSecurity nodeSecurity = findNodeSecurity(nodeId, true);
            if (nodeSecurity != null) {
                nodeSecurity.setInitialLoadEnabled(initialLoadEnabled);
                nodeSecurity.setInitialLoadId(loadId);
                if (initialLoadEnabled) {
                    nodeSecurity.setInitialLoadTime(null);
                    nodeSecurity.setInitialLoadCreateBy(createBy);
                } else {
                    nodeSecurity.setInitialLoadTime(new Date());
                }
                return updateNodeSecurity(transaction, nodeSecurity);
            }
            return false;
        } finally {
            if (!syncChange) {
                symmetricDialect.enableSyncTriggers(transaction);
            }
        }
    }

    public boolean setInitialLoadEnabled(String nodeId, boolean initialLoadEnabled, boolean syncChange, long loadId,
            String createBy) {
        ISqlTransaction transaction = null;
        try {
            transaction = sqlTemplate.startSqlTransaction();
            boolean updated = setInitialLoadEnabled(transaction, nodeId, initialLoadEnabled, syncChange, loadId,
                    createBy);
            transaction.commit();
            return updated;
        } catch (Error ex) {
            if (transaction != null) {
                transaction.rollback();
            }
            throw ex;
        } catch (RuntimeException ex) {
            if (transaction != null) {
                transaction.rollback();
            }
            throw ex;
        } finally {
            close(transaction);
        }
    }

    public boolean setReverseInitialLoadEnabled(ISqlTransaction transaction, String nodeId,
            boolean initialLoadEnabled, boolean syncChange, long loadId, String createBy) {
        try {
            if (!syncChange) {
                symmetricDialect.disableSyncTriggers(transaction, nodeId);
            }

            NodeSecurity nodeSecurity = findNodeSecurity(nodeId, true);
            if (nodeSecurity != null) {
                nodeSecurity.setRevInitialLoadEnabled(initialLoadEnabled);
                nodeSecurity.setRevInitialLoadId(loadId);
                if (initialLoadEnabled) {
                    nodeSecurity.setRevInitialLoadTime(null);
                    nodeSecurity.setRevInitialLoadCreateBy(createBy);
                } else {
                    nodeSecurity.setRevInitialLoadTime(new Date());
                }
                return updateNodeSecurity(transaction, nodeSecurity);
            }
            return false;
        } finally {
            if (!syncChange) {
                symmetricDialect.enableSyncTriggers(transaction);
            }
        }
    }

    public boolean setReverseInitialLoadEnabled(String nodeId, boolean initialLoadEnabled, boolean syncChange,
            long loadId, String createBy) {
        ISqlTransaction transaction = null;
        try {
            transaction = sqlTemplate.startSqlTransaction();
            boolean updated = setReverseInitialLoadEnabled(transaction, nodeId, initialLoadEnabled, syncChange,
                    loadId, createBy);
            transaction.commit();
            return updated;
        } catch (Error ex) {
            if (transaction != null) {
                transaction.rollback();
            }
            throw ex;
        } catch (RuntimeException ex) {
            if (transaction != null) {
                transaction.rollback();
            }
            throw ex;
        } finally {
            close(transaction);
        }
    }

    public boolean isExternalIdRegistered(String nodeGroupId, String externalId) {
        return sqlTemplate.queryForInt(getSql("isNodeRegisteredSql"), new Object[] { nodeGroupId, externalId }) > 0;
    }

    public boolean isDataLoadCompleted() {
        return getNodeStatus() == NodeStatus.DATA_LOAD_COMPLETED;
    }

    public boolean isDataLoadStarted() {
        return getNodeStatus() == NodeStatus.DATA_LOAD_STARTED;
    }

    public boolean isRegistrationServer() {
        return parameterService.isRegistrationServer();
    }

    public NodeStatus getNodeStatus() {
        long ts = System.currentTimeMillis();
        try {
            class DataLoadStatus {
                int initialLoadEnabled;
                Date initialLoadTime;
            }

            List<DataLoadStatus> results = sqlTemplate.query(getSql("getDataLoadStatusSql"),
                    new ISqlRowMapper<DataLoadStatus>() {
                        public DataLoadStatus mapRow(Row rs) {
                            DataLoadStatus status = new DataLoadStatus();
                            status.initialLoadEnabled = rs.getInt("initial_load_enabled");
                            status.initialLoadTime = rs.getDateTime("initial_load_time");
                            return status;
                        }
                    });

            if (results.size() > 0) {
                DataLoadStatus status = results.get(0);
                if (status.initialLoadEnabled == 1) {
                    return NodeStatus.DATA_LOAD_STARTED;
                } else if (status.initialLoadTime != null) {
                    return NodeStatus.DATA_LOAD_COMPLETED;
                }
            }
            return NodeStatus.DATA_LOAD_NOT_STARTED;
        } catch (SqlException ex) {
            log.error("Could not query table after {} ms.  The status is unknown.",
                    (System.currentTimeMillis() - ts), ex);
            return NodeStatus.STATUS_UNKNOWN;
        }
    }

    public void setNodePasswordFilter(INodePasswordFilter nodePasswordFilter) {
        this.nodePasswordFilter = nodePasswordFilter;
    }

    private String filterPasswordOnSaveIfNeeded(String password) {
        String s = password;
        if (nodePasswordFilter != null) {
            s = nodePasswordFilter.onNodeSecuritySave(password);
        }
        return s;
    }

    private String filterPasswordOnRenderIfNeeded(String password) {
        String s = password;
        if (nodePasswordFilter != null) {
            s = nodePasswordFilter.onNodeSecurityRender(password);
        }
        return s;
    }

    public void checkForOfflineNodes() {
        long offlineNodeDetectionMinutes = parameterService
                .getLong(ParameterConstants.OFFLINE_NODE_DETECTION_PERIOD_MINUTES);
        List<IOfflineServerListener> offlineServerListeners = extensionService
                .getExtensionPointList(IOfflineServerListener.class);
        // Only check for offline nodes if there is a listener and the
        // offline detection period is a positive value. The default value
        // of -1 disables the feature.
        if (offlineServerListeners != null && offlineNodeDetectionMinutes > 0) {

            List<Node> list = findOfflineNodes();
            if (list.size() > 0) {
                fireOffline(list);
            }
        }
    }

    public List<Node> findOfflineNodes() {
        return findOfflineNodes(parameterService.getLong(ParameterConstants.OFFLINE_NODE_DETECTION_PERIOD_MINUTES));
    }

    public List<Node> findOfflineNodes(long minutesOffline) {
        List<Node> offlineNodeList = new ArrayList<Node>();
        Node myNode = findIdentity();

        if (myNode != null) {
            long offlineNodeDetectionMillis = minutesOffline * 60 * 1000;

            List<Row> list = sqlTemplate.query(getSql("findNodeHeartbeatsSql"));
            for (Row node : list) {
                String nodeId = node.getString("node_id");
                Date time = node.getDateTime("heartbeat_time");
                String offset = node.getString("timezone_offset");
                // Take the timezone of the client node into account when
                // checking the hearbeat time.
                Date clientNodeCurrentTime = null;
                if (offset != null) {
                    clientNodeCurrentTime = AppUtils.getLocalDateForOffset(offset);
                } else {
                    clientNodeCurrentTime = new Date();
                }
                long cutOffTimeMillis = clientNodeCurrentTime.getTime() - offlineNodeDetectionMillis;
                if (time == null || time.getTime() < cutOffTimeMillis) {
                    offlineNodeList.add(findNode(nodeId));
                }
            }
        }

        return offlineNodeList;
    }

    public Map<String, Date> findLastHeartbeats() {
        Map<String, Date> dates = new HashMap<String, Date>();
        Node myNode = findIdentity();
        if (myNode != null) {
            List<Row> list = sqlTemplate.query(getSql("findNodeHeartbeatsSql"));
            for (Row node : list) {
                String nodeId = node.getString("node_id");
                Date time = node.getDateTime("heartbeat_time");
                dates.put(nodeId, time);
            }
        }
        return dates;
    }

    public List<String> findOfflineNodeIds(long minutesOffline) {
        List<String> offlineNodeList = new ArrayList<String>();
        Node myNode = findIdentity();

        if (myNode != null) {
            long offlineNodeDetectionMillis = minutesOffline * 60 * 1000;

            List<Row> list = sqlTemplate.query(getSql("findNodeHeartbeatsSql"));
            for (Row node : list) {
                String nodeId = node.getString("node_id");
                Date time = node.getDateTime("heartbeat_time");
                String offset = node.getString("timezone_offset");
                // Take the timezone of the client node into account when
                // checking the hearbeat time.
                Date clientNodeCurrentTime = null;
                if (offset != null) {
                    clientNodeCurrentTime = AppUtils.getLocalDateForOffset(offset);
                } else {
                    clientNodeCurrentTime = new Date();
                }
                long cutOffTimeMillis = clientNodeCurrentTime.getTime() - offlineNodeDetectionMillis;
                if (time == null || time.getTime() < cutOffTimeMillis) {
                    offlineNodeList.add(nodeId);
                }
            }
        }
        return offlineNodeList;
    }

    protected void fireOffline(List<Node> offlineClientNodeList) {
        for (IOfflineServerListener listener : extensionService
                .getExtensionPointList(IOfflineServerListener.class)) {
            for (Node node : offlineClientNodeList) {
                listener.clientNodeOffline(node);
            }
        }
    }

    class NodeRowMapper implements ISqlRowMapper<Node> {
        public Node mapRow(Row rs) {
            Node node = new Node();
            node.setNodeId(rs.getString("node_id"));
            node.setNodeGroupId(rs.getString("node_group_id"));
            node.setExternalId(rs.getString("external_id"));
            node.setSyncEnabled(rs.getBoolean("sync_enabled"));
            node.setSyncUrl(rs.getString("sync_url"));
            node.setSchemaVersion(rs.getString("schema_version"));
            node.setDatabaseType(rs.getString("database_type"));
            node.setDatabaseVersion(rs.getString("database_version"));
            node.setSymmetricVersion(rs.getString("symmetric_version"));
            node.setCreatedAtNodeId(rs.getString("created_at_node_id"));
            node.setBatchToSendCount(rs.getInt("batch_to_send_count"));
            node.setBatchInErrorCount(rs.getInt("batch_in_error_count"));
            node.setDeploymentType(rs.getString("deployment_type"));
            return node;
        }
    }

    class NodeSecurityRowMapper implements ISqlRowMapper<NodeSecurity> {
        public NodeSecurity mapRow(Row rs) {
            NodeSecurity nodeSecurity = new NodeSecurity();
            nodeSecurity.setNodeId(rs.getString("node_id"));
            nodeSecurity.setNodePassword(filterPasswordOnRenderIfNeeded(rs.getString("node_password")));
            nodeSecurity.setRegistrationEnabled(rs.getBoolean("registration_enabled"));
            nodeSecurity.setRegistrationTime(rs.getDateTime("registration_time"));
            nodeSecurity.setInitialLoadEnabled(rs.getBoolean("initial_load_enabled"));
            nodeSecurity.setInitialLoadTime(rs.getDateTime("initial_load_time"));
            nodeSecurity.setCreatedAtNodeId(rs.getString("created_at_node_id"));
            nodeSecurity.setRevInitialLoadEnabled(rs.getBoolean("rev_initial_load_enabled"));
            nodeSecurity.setRevInitialLoadTime(rs.getDateTime("rev_initial_load_time"));
            nodeSecurity.setInitialLoadId(rs.getLong("initial_load_id"));
            nodeSecurity.setInitialLoadCreateBy(rs.getString("initial_load_create_by"));
            nodeSecurity.setRevInitialLoadId(rs.getLong("rev_initial_load_id"));
            nodeSecurity.setRevInitialLoadCreateBy(rs.getString("rev_initial_load_create_by"));
            return nodeSecurity;
        }
    }

    class NodeHostRowMapper implements ISqlRowMapper<NodeHost> {
        public NodeHost mapRow(Row rs) {
            NodeHost nodeHost = new NodeHost();
            nodeHost.setNodeId(rs.getString("node_id"));
            nodeHost.setHostName(rs.getString("host_name"));
            nodeHost.setIpAddress(rs.getString("ip_address"));
            nodeHost.setOsUser(rs.getString("os_user"));
            nodeHost.setOsName(rs.getString("os_name"));
            nodeHost.setOsArch(rs.getString("os_arch"));
            nodeHost.setOsVersion(rs.getString("os_version"));
            nodeHost.setAvailableProcessors(rs.getInt("available_processors"));
            nodeHost.setFreeMemoryBytes(rs.getLong("free_memory_bytes"));
            nodeHost.setTotalMemoryBytes(rs.getLong("total_memory_bytes"));
            nodeHost.setMaxMemoryBytes(rs.getLong("max_memory_bytes"));
            nodeHost.setJavaVersion(rs.getString("java_version"));
            nodeHost.setJavaVendor(rs.getString("java_vendor"));
            nodeHost.setJdbcVersion(rs.getString("jdbc_version"));
            nodeHost.setSymmetricVersion(rs.getString("symmetric_version"));
            nodeHost.setTimezoneOffset(rs.getString("timezone_offset"));
            nodeHost.setHeartbeatTime(rs.getDateTime("heartbeat_time"));
            nodeHost.setLastRestartTime(rs.getDateTime("last_restart_time"));
            nodeHost.setCreateTime(rs.getDateTime("create_time"));
            return nodeHost;
        }
    }

}