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

Java tutorial

Introduction

Here is the source code for org.jumpmind.symmetric.service.impl.FileSyncService.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.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.lang.StringUtils;
import org.jumpmind.db.model.Table;
import org.jumpmind.db.sql.ISqlReadCursor;
import org.jumpmind.db.sql.ISqlRowMapper;
import org.jumpmind.db.sql.ISqlTransaction;
import org.jumpmind.db.sql.Row;
import org.jumpmind.exception.IoException;
import org.jumpmind.symmetric.ISymmetricEngine;
import org.jumpmind.symmetric.SymmetricException;
import org.jumpmind.symmetric.common.Constants;
import org.jumpmind.symmetric.common.ParameterConstants;
import org.jumpmind.symmetric.common.TableConstants;
import org.jumpmind.symmetric.file.DirectorySnapshot;
import org.jumpmind.symmetric.file.FileConflictException;
import org.jumpmind.symmetric.file.FileSyncZipDataWriter;
import org.jumpmind.symmetric.file.FileTriggerTracker;
import org.jumpmind.symmetric.io.data.CsvData;
import org.jumpmind.symmetric.io.data.DataEventType;
import org.jumpmind.symmetric.io.stage.IStagedResource;
import org.jumpmind.symmetric.io.stage.IStagingManager;
import org.jumpmind.symmetric.model.BatchAck;
import org.jumpmind.symmetric.model.Channel;
import org.jumpmind.symmetric.model.Data;
import org.jumpmind.symmetric.model.FileConflictStrategy;
import org.jumpmind.symmetric.model.FileSnapshot;
import org.jumpmind.symmetric.model.FileSnapshot.LastEventType;
import org.jumpmind.symmetric.model.FileTrigger;
import org.jumpmind.symmetric.model.FileTriggerRouter;
import org.jumpmind.symmetric.model.IncomingBatch;
import org.jumpmind.symmetric.model.Node;
import org.jumpmind.symmetric.model.NodeCommunication;
import org.jumpmind.symmetric.model.NodeCommunication.CommunicationType;
import org.jumpmind.symmetric.model.NodeSecurity;
import org.jumpmind.symmetric.model.OutgoingBatch;
import org.jumpmind.symmetric.model.OutgoingBatch.Status;
import org.jumpmind.symmetric.model.OutgoingBatches;
import org.jumpmind.symmetric.model.ProcessInfo;
import org.jumpmind.symmetric.model.ProcessInfoKey;
import org.jumpmind.symmetric.model.ProcessType;
import org.jumpmind.symmetric.model.RemoteNodeStatus;
import org.jumpmind.symmetric.model.RemoteNodeStatuses;
import org.jumpmind.symmetric.service.ClusterConstants;
import org.jumpmind.symmetric.service.IFileSyncService;
import org.jumpmind.symmetric.service.IIncomingBatchService;
import org.jumpmind.symmetric.service.INodeCommunicationService;
import org.jumpmind.symmetric.service.INodeCommunicationService.INodeCommunicationExecutor;
import org.jumpmind.symmetric.service.INodeService;
import org.jumpmind.symmetric.transport.IIncomingTransport;
import org.jumpmind.symmetric.transport.IOutgoingTransport;
import org.jumpmind.symmetric.transport.IOutgoingWithResponseTransport;
import org.jumpmind.util.AppUtils;

import bsh.EvalError;
import bsh.Interpreter;
import bsh.TargetError;

public class FileSyncService extends AbstractOfflineDetectorService
        implements IFileSyncService, INodeCommunicationExecutor {

    private ISymmetricEngine engine;

    // TODO cache trigger routers

    public FileSyncService(ISymmetricEngine engine) {
        super(engine.getParameterService(), engine.getSymmetricDialect(), engine.getExtensionService());
        this.engine = engine;
        setSqlMap(new FileSyncServiceSqlMap(platform, createSqlReplacementTokens()));
    }

    public boolean refreshFromDatabase() {
        // TODO implement with cache
        return false;
    }

    public void trackChanges(boolean force) {
        if (force || engine.getClusterService().lock(ClusterConstants.FILE_SYNC_TRACKER)) {
            try {
                log.debug("Attempting to get exclusive lock for file sync track changes");
                if (engine.getClusterService().lock(ClusterConstants.FILE_SYNC_SHARED,
                        ClusterConstants.TYPE_EXCLUSIVE,
                        getParameterService().getLong(ParameterConstants.FILE_SYNC_LOCK_WAIT_MS))) {
                    try {
                        log.debug("Tracking changes for file sync");
                        List<FileTriggerRouter> fileTriggerRouters = getFileTriggerRoutersForCurrentNode();
                        for (FileTriggerRouter fileTriggerRouter : fileTriggerRouters) {
                            if (fileTriggerRouter.isEnabled()) {
                                FileTriggerTracker tracker = new FileTriggerTracker(fileTriggerRouter,
                                        getDirectorySnapshot(fileTriggerRouter));
                                try {
                                    DirectorySnapshot dirSnapshot = tracker.trackChanges();
                                    for (FileSnapshot fileSnapshot : dirSnapshot) {
                                        File file = fileTriggerRouter.getFileTrigger()
                                                .createSourceFile(fileSnapshot);
                                        String filePath = file.getParentFile().getPath().replace('\\', '/');
                                        String fileName = file.getName();
                                        String nodeId = findSourceNodeIdFromFileIncoming(filePath, fileName,
                                                fileSnapshot.getFileModifiedTime());
                                        if (StringUtils.isNotBlank(nodeId)) {
                                            fileSnapshot.setLastUpdateBy(nodeId);
                                        } else {
                                            fileSnapshot.setLastUpdateBy(null);
                                        }
                                        log.debug("Captured change " + fileSnapshot.getLastEventType()
                                                + " change of " + fileSnapshot.getFileName() + " (lastmodified="
                                                + fileSnapshot.getFileModifiedTime() + ",size="
                                                + fileSnapshot.getFileSize() + ") from "
                                                + fileSnapshot.getLastUpdateBy());
                                    }
                                    save(dirSnapshot);
                                } catch (Exception ex) {
                                    log.error("Failed to track changes for file trigger router: "
                                            + fileTriggerRouter.getFileTrigger().getTriggerId() + "::"
                                            + fileTriggerRouter.getRouter().getRouterId(), ex);
                                }
                            }
                        }

                        deleteFromFileIncoming();
                    } finally {
                        log.debug("Done tracking changes for file sync");
                        engine.getClusterService().unlock(ClusterConstants.FILE_SYNC_SHARED,
                                ClusterConstants.TYPE_EXCLUSIVE);
                    }
                } else {
                    log.warn("Did not run the track file sync changes process because it was shared locked");
                }
            } finally {
                if (!force) {
                    engine.getClusterService().unlock(ClusterConstants.FILE_SYNC_TRACKER);
                }
            }
        } else {
            log.debug("Did not run the track file sync changes process because it was cluster locked");
        }
    }

    protected String findSourceNodeIdFromFileIncoming(String filePath, String fileName, long lastUpdateDate) {
        return sqlTemplate.queryForString(getSql("findNodeIdFromFileIncoming"), filePath, fileName, lastUpdateDate);
    }

    protected void deleteFromFileIncoming() {
        sqlTemplate.update(getSql("deleteFileIncoming"));
    }

    public List<FileTrigger> getFileTriggers() {
        return sqlTemplate.query(getSql("selectFileTriggersSql"), new FileTriggerMapper());
    }

    public FileTrigger getFileTrigger(String triggerId) {
        return sqlTemplate.queryForObject(getSql("selectFileTriggersSql", "triggerIdWhere"),
                new FileTriggerMapper(), triggerId);
    }

    public List<FileTriggerRouter> getFileTriggerRoutersForCurrentNode() {
        return sqlTemplate.query(getSql("selectFileTriggerRoutersSql", "fileTriggerRoutersForCurrentNodeWhere"),
                new FileTriggerRouterMapper(), parameterService.getNodeGroupId());
    }

    public List<FileTriggerRouter> getFileTriggerRouters() {
        return sqlTemplate.query(getSql("selectFileTriggerRoutersSql"), new FileTriggerRouterMapper());
    }

    public FileTriggerRouter getFileTriggerRouter(String triggerId, String routerId) {
        return sqlTemplate.queryForObject(getSql("selectFileTriggerRoutersSql", "whereTriggerRouterId"),
                new FileTriggerRouterMapper(), triggerId, routerId);
    }

    public void saveFileTrigger(FileTrigger fileTrigger) {
        fileTrigger.setLastUpdateTime(new Date());
        if (0 == sqlTemplate.update(getSql("updateFileTriggerSql"), new Object[] { fileTrigger.getBaseDir(),
                fileTrigger.isRecurse() ? 1 : 0, fileTrigger.getIncludesFiles(), fileTrigger.getExcludesFiles(),
                fileTrigger.isSyncOnCreate() ? 1 : 0, fileTrigger.isSyncOnModified() ? 1 : 0,
                fileTrigger.isSyncOnDelete() ? 1 : 0, fileTrigger.isSyncOnCtlFile() ? 1 : 0,
                fileTrigger.isDeleteAfterSync() ? 1 : 0, fileTrigger.getBeforeCopyScript(),
                fileTrigger.getAfterCopyScript(), fileTrigger.getLastUpdateBy(), fileTrigger.getLastUpdateTime(),
                fileTrigger.getChannelId(), fileTrigger.getReloadChannelId(), fileTrigger.getTriggerId() },
                new int[] { Types.VARCHAR, Types.SMALLINT, Types.VARCHAR, Types.VARCHAR, Types.SMALLINT,
                        Types.SMALLINT, Types.SMALLINT, Types.SMALLINT, Types.SMALLINT, Types.VARCHAR,
                        Types.VARCHAR, Types.VARCHAR, Types.TIMESTAMP, Types.VARCHAR, Types.VARCHAR,
                        Types.VARCHAR })) {
            fileTrigger.setCreateTime(fileTrigger.getLastUpdateTime());
            sqlTemplate.update(getSql("insertFileTriggerSql"), new Object[] { fileTrigger.getBaseDir(),
                    fileTrigger.isRecurse() ? 1 : 0, fileTrigger.getIncludesFiles(), fileTrigger.getExcludesFiles(),
                    fileTrigger.isSyncOnCreate() ? 1 : 0, fileTrigger.isSyncOnModified() ? 1 : 0,
                    fileTrigger.isSyncOnDelete() ? 1 : 0, fileTrigger.isSyncOnCtlFile() ? 1 : 0,
                    fileTrigger.isDeleteAfterSync() ? 1 : 0, fileTrigger.getBeforeCopyScript(),
                    fileTrigger.getAfterCopyScript(), fileTrigger.getLastUpdateBy(),
                    fileTrigger.getLastUpdateTime(), fileTrigger.getTriggerId(), fileTrigger.getCreateTime(),
                    fileTrigger.getChannelId(), fileTrigger.getReloadChannelId() },
                    new int[] { Types.VARCHAR, Types.SMALLINT, Types.VARCHAR, Types.VARCHAR, Types.SMALLINT,
                            Types.SMALLINT, Types.SMALLINT, Types.SMALLINT, Types.SMALLINT, Types.VARCHAR,
                            Types.VARCHAR, Types.VARCHAR, Types.TIMESTAMP, Types.VARCHAR, Types.TIMESTAMP,
                            Types.VARCHAR, Types.VARCHAR });
        }

    }

    public void saveFileTriggerRouter(FileTriggerRouter fileTriggerRouter) {
        fileTriggerRouter.setLastUpdateTime(new Date());
        if (0 == sqlTemplate.update(getSql("updateFileTriggerRouterSql"),
                new Object[] { fileTriggerRouter.isEnabled() ? 1 : 0,
                        fileTriggerRouter.isInitialLoadEnabled() ? 1 : 0, fileTriggerRouter.getTargetBaseDir(),
                        fileTriggerRouter.getConflictStrategy().name(), fileTriggerRouter.getLastUpdateBy(),
                        fileTriggerRouter.getLastUpdateTime(), fileTriggerRouter.getFileTrigger().getTriggerId(),
                        fileTriggerRouter.getRouter().getRouterId() },
                new int[] { Types.SMALLINT, Types.SMALLINT, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR,
                        Types.TIMESTAMP, Types.VARCHAR, Types.VARCHAR })) {
            fileTriggerRouter.setCreateTime(fileTriggerRouter.getLastUpdateTime());
            sqlTemplate.update(getSql("insertFileTriggerRouterSql"),
                    new Object[] { fileTriggerRouter.isEnabled() ? 1 : 0,
                            fileTriggerRouter.isInitialLoadEnabled() ? 1 : 0, fileTriggerRouter.getTargetBaseDir(),
                            fileTriggerRouter.getConflictStrategy().name(), fileTriggerRouter.getCreateTime(),
                            fileTriggerRouter.getLastUpdateBy(), fileTriggerRouter.getLastUpdateTime(),
                            fileTriggerRouter.getFileTrigger().getTriggerId(),
                            fileTriggerRouter.getRouter().getRouterId() },
                    new int[] { Types.SMALLINT, Types.SMALLINT, Types.VARCHAR, Types.VARCHAR, Types.TIMESTAMP,
                            Types.VARCHAR, Types.TIMESTAMP, Types.VARCHAR, Types.VARCHAR });
        }
    }

    public void deleteFileTriggerRouter(String triggerId, String routerId) {
        sqlTemplate.update(getSql("deleteFileTriggerRouterSql"), triggerId, routerId);
    }

    public void deleteFileTriggerRouter(FileTriggerRouter fileTriggerRouter) {
        sqlTemplate.update(getSql("deleteFileTriggerRouterSql"),
                (Object) fileTriggerRouter.getFileTrigger().getTriggerId(),
                fileTriggerRouter.getRouter().getRouterId());
    }

    public void deleteFileTrigger(FileTrigger fileTrigger) {
        sqlTemplate.update(getSql("deleteFileTriggerSql"), (Object) fileTrigger.getTriggerId());
    }

    public List<FileTriggerRouter> getFileTriggerRouters(FileTrigger fileTrigger) {
        return sqlTemplate.query(getSql("selectFileTriggerRoutersSql", "whereTriggerIdSql"),
                new FileTriggerRouterMapper(), fileTrigger.getTriggerId());
    }

    public DirectorySnapshot getDirectorySnapshot(FileTriggerRouter fileTriggerRouter) {
        return new DirectorySnapshot(fileTriggerRouter,
                sqlTemplate.query(getSql("selectFileSnapshotSql"), new FileSnapshotMapper(),
                        fileTriggerRouter.getFileTrigger().getTriggerId(),
                        fileTriggerRouter.getRouter().getRouterId()));
    }

    public void save(List<FileSnapshot> changes) {
        if (changes != null) {
            ISqlTransaction sqlTransaction = null;
            try {
                sqlTransaction = sqlTemplate.startSqlTransaction();
                for (FileSnapshot fileSnapshot : changes) {
                    save(sqlTransaction, fileSnapshot);
                }

                sqlTransaction.commit();
            } catch (Error ex) {
                if (sqlTransaction != null) {
                    sqlTransaction.rollback();
                }
                throw ex;
            } catch (RuntimeException ex) {
                if (sqlTransaction != null) {
                    sqlTransaction.rollback();
                }
                throw ex;
            } finally {
                close(sqlTransaction);
            }
        }
    }

    public void save(ISqlTransaction sqlTransaction, FileSnapshot snapshot) {
        snapshot.setLastUpdateTime(new Date());
        if (0 == sqlTransaction.prepareAndExecute(getSql("updateFileSnapshotSql"),
                new Object[] { snapshot.getLastEventType().getCode(), snapshot.getCrc32Checksum(),
                        snapshot.getFileSize(), snapshot.getFileModifiedTime(), snapshot.getLastUpdateTime(),
                        snapshot.getLastUpdateBy(), snapshot.getChannelId(), snapshot.getReloadChannelId(),
                        snapshot.getTriggerId(), snapshot.getRouterId(), snapshot.getRelativeDir(),
                        snapshot.getFileName() },
                new int[] { Types.VARCHAR, Types.NUMERIC, Types.NUMERIC, Types.NUMERIC, Types.TIMESTAMP,
                        Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR,
                        Types.VARCHAR })) {
            snapshot.setCreateTime(snapshot.getLastUpdateTime());
            sqlTransaction.prepareAndExecute(getSql("insertFileSnapshotSql"),
                    new Object[] { snapshot.getLastEventType().getCode(), snapshot.getCrc32Checksum(),
                            snapshot.getFileSize(), snapshot.getFileModifiedTime(), snapshot.getCreateTime(),
                            snapshot.getLastUpdateTime(), snapshot.getLastUpdateBy(), snapshot.getChannelId(),
                            snapshot.getReloadChannelId(), snapshot.getTriggerId(), snapshot.getRouterId(),
                            snapshot.getRelativeDir(), snapshot.getFileName() },
                    new int[] { Types.VARCHAR, Types.NUMERIC, Types.NUMERIC, Types.NUMERIC, Types.TIMESTAMP,
                            Types.TIMESTAMP, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR,
                            Types.VARCHAR, Types.VARCHAR, Types.VARCHAR });
        }
        // now that we have captured an update, delete the row for cleanup
        if (snapshot.getLastEventType() == LastEventType.DELETE) {
            sqlTransaction.prepareAndExecute(getSql("deleteFileSnapshotSql"),
                    new Object[] { snapshot.getTriggerId(), snapshot.getRouterId(), snapshot.getRelativeDir(),
                            snapshot.getFileName() },
                    new int[] { Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR });
        }

    }

    synchronized public RemoteNodeStatuses pullFilesFromNodes(boolean force) {
        return queueJob(force, parameterService.getLong(ParameterConstants.FILE_PULL_MINIMUM_PERIOD_MS, -1),
                ClusterConstants.FILE_SYNC_PULL, CommunicationType.FILE_PULL);
    }

    synchronized public RemoteNodeStatuses pushFilesToNodes(boolean force) {
        return queueJob(force, parameterService.getLong(ParameterConstants.FILE_PUSH_MINIMUM_PERIOD_MS, -1),
                ClusterConstants.FILE_SYNC_PUSH, CommunicationType.FILE_PUSH);
    }

    public List<OutgoingBatch> sendFiles(ProcessInfo processInfo, Node targetNode,
            IOutgoingTransport outgoingTransport) {
        List<OutgoingBatch> processedBatches = new ArrayList<OutgoingBatch>();
        List<OutgoingBatch> batchesToProcess = new ArrayList<OutgoingBatch>();
        List<Channel> fileSyncChannels = engine.getConfigurationService().getFileSyncChannels();
        for (Channel channel : fileSyncChannels) {
            OutgoingBatches batches = engine.getOutgoingBatchService().getOutgoingBatches(targetNode.getNodeId(),
                    false);
            batchesToProcess.addAll(batches.filterBatchesForChannel(channel));
        }

        OutgoingBatch currentBatch = null;

        IStagingManager stagingManager = engine.getStagingManager();
        long memoryThresholdInBytes = parameterService.getLong(ParameterConstants.STREAM_TO_FILE_THRESHOLD);
        IStagedResource stagedResource = stagingManager.create(memoryThresholdInBytes,
                Constants.STAGING_CATEGORY_OUTGOING, processInfo.getSourceNodeId(), targetNode.getNodeId(),
                "filesync.zip");

        try {

            long maxBytesToSync = parameterService.getLong(ParameterConstants.TRANSPORT_MAX_BYTES_TO_SYNC);

            FileSyncZipDataWriter dataWriter = new FileSyncZipDataWriter(maxBytesToSync, this,
                    engine.getNodeService(), stagedResource);
            try {
                for (int i = 0; i < batchesToProcess.size(); i++) {
                    currentBatch = batchesToProcess.get(i);
                    processInfo.incrementBatchCount();
                    processInfo.setCurrentBatchId(currentBatch.getBatchId());

                    ((DataExtractorService) engine.getDataExtractorService()).extractOutgoingBatch(processInfo,
                            targetNode, dataWriter, currentBatch, false, true,
                            DataExtractorService.ExtractMode.FOR_SYM_CLIENT);

                    processedBatches.add(currentBatch);

                    /*
                     * check to see if max bytes to sync has been reached and
                     * stop processing batches
                     */
                    if (dataWriter.readyToSend()) {
                        break;
                    }
                }
            } finally {
                dataWriter.finish();
            }

            processInfo.setStatus(ProcessInfo.Status.TRANSFERRING);

            for (int i = 0; i < batchesToProcess.size(); i++) {
                batchesToProcess.get(i).setStatus(Status.SE);
            }
            engine.getOutgoingBatchService().updateOutgoingBatches(batchesToProcess);

            try {
                if (stagedResource.exists()) {
                    InputStream is = stagedResource.getInputStream();
                    try {
                        OutputStream os = outgoingTransport.openStream();
                        IOUtils.copy(is, os);
                        os.flush();
                    } catch (IOException e) {
                        throw new IoException(e);
                    }
                }

                for (int i = 0; i < batchesToProcess.size(); i++) {
                    batchesToProcess.get(i).setStatus(Status.LD);
                }
                engine.getOutgoingBatchService().updateOutgoingBatches(batchesToProcess);

            } finally {
                stagedResource.close();
            }

        } catch (RuntimeException e) {
            if (currentBatch != null) {
                engine.getStatisticManager().incrementDataExtractedErrors(currentBatch.getChannelId(), 1);
                currentBatch.setSqlMessage(getRootMessage(e));
                currentBatch.revertStatsOnError();
                if (currentBatch.getStatus() != Status.IG) {
                    currentBatch.setStatus(Status.ER);
                }
                currentBatch.setErrorFlag(true);
                engine.getOutgoingBatchService().updateOutgoingBatch(currentBatch);

                if (isStreamClosedByClient(e)) {
                    log.warn("Failed to extract batch {}.  The stream was closed by the client.  The error was: {}",
                            currentBatch, getRootMessage(e));
                } else {
                    log.error("Failed to extract batch {}", currentBatch, e);
                }
            } else {
                log.error("Could not log the outgoing batch status because the batch was null", e);
            }

            throw e;
        } finally {
            if (stagedResource != null) {
                stagedResource.delete();
            }
        }

        return processedBatches;
    }

    public void acknowledgeFiles(OutgoingBatch outgoingBatch) {
        log.debug("Acknowledging file_sync outgoing batch-{}", outgoingBatch.getBatchId());
        ISqlReadCursor<Data> cursor = engine.getDataService().selectDataFor(outgoingBatch.getBatchId(),
                outgoingBatch.getChannelId());
        Data data = null;
        List<File> filesToDelete = new ArrayList<File>();
        Table snapshotTable = platform.getTableFromCache(
                TableConstants.getTableName(tablePrefix, TableConstants.SYM_FILE_SNAPSHOT), false);
        for (int i = 0; i < outgoingBatch.getInsertEventCount(); i++) {
            data = cursor.next();
            if (data != null && (data.getDataEventType() == DataEventType.INSERT
                    || data.getDataEventType() == DataEventType.UPDATE)) {
                Map<String, String> columnData = data.toColumnNameValuePairs(snapshotTable.getColumnNames(),
                        CsvData.ROW_DATA);

                FileSnapshot fileSnapshot = new FileSnapshot();
                fileSnapshot.setTriggerId(columnData.get("TRIGGER_ID"));
                fileSnapshot.setRouterId(columnData.get("ROUTER_ID"));
                fileSnapshot.setFileModifiedTime(Long.parseLong(columnData.get("FILE_MODIFIED_TIME")));
                fileSnapshot.setFileName(columnData.get("FILE_NAME"));
                fileSnapshot.setRelativeDir(columnData.get("RELATIVE_DIR"));
                fileSnapshot.setLastEventType(LastEventType.fromCode(columnData.get("LAST_EVENT_TYPE")));

                FileTriggerRouter triggerRouter = this.getFileTriggerRouter(fileSnapshot.getTriggerId(),
                        fileSnapshot.getRouterId());
                if (triggerRouter != null) {
                    FileTrigger fileTrigger = triggerRouter.getFileTrigger();

                    if (fileTrigger.isDeleteAfterSync()) {
                        File file = fileTrigger.createSourceFile(fileSnapshot);
                        if (!file.isDirectory()) {
                            filesToDelete.add(file);
                            if (fileTrigger.isSyncOnCtlFile()) {
                                filesToDelete.add(new File(file.getAbsolutePath() + ".ctl"));
                            }
                        }
                    }
                }
            }
        }

        if (cursor != null) {
            cursor.close();
            cursor = null;
        }

        if (filesToDelete != null && filesToDelete.size() > 0) {
            for (File file : filesToDelete) {
                if (file != null && file.exists()) {
                    log.debug("Deleting the '{}' file", file.getAbsolutePath());
                    boolean deleted = FileUtils.deleteQuietly(file);
                    if (!deleted) {
                        log.warn("Failed to 'delete on sync' the {} file", file.getAbsolutePath());
                    }
                }
                file = null;
            }
            filesToDelete = null;
        }
    }

    public void loadFilesFromPush(String nodeId, InputStream in, OutputStream out) {
        INodeService nodeService = engine.getNodeService();
        Node local = nodeService.findIdentity();
        Node sourceNode = nodeService.findNode(nodeId);
        if (local != null && sourceNode != null) {
            ProcessInfo processInfo = engine.getStatisticManager().newProcessInfo(
                    new ProcessInfoKey(nodeId, local.getNodeId(), ProcessType.FILE_SYNC_PUSH_HANDLER));
            try {
                List<IncomingBatch> list = processZip(in, nodeId, processInfo);
                NodeSecurity security = nodeService.findNodeSecurity(local.getNodeId());
                processInfo.setStatus(ProcessInfo.Status.ACKING);
                engine.getTransportManager().writeAcknowledgement(out, sourceNode, list, local,
                        security != null ? security.getNodePassword() : null);
                processInfo.setStatus(ProcessInfo.Status.OK);
            } catch (Throwable e) {
                processInfo.setStatus(ProcessInfo.Status.ERROR);
                if (e instanceof IOException) {
                    throw new IoException((IOException) e);
                } else if (e instanceof RuntimeException) {
                    throw (RuntimeException) e;
                } else {
                    throw new RuntimeException(e);
                }
            }
        } else {
            throw new SymmetricException("Could not load data because the node is not registered");
        }
    }

    public void execute(NodeCommunication nodeCommunication, RemoteNodeStatus status) {
        Node identity = engine.getNodeService().findIdentity();
        if (identity != null) {
            NodeSecurity security = engine.getNodeService().findNodeSecurity(identity.getNodeId());
            if (security != null) {
                if (nodeCommunication.getCommunicationType() == CommunicationType.FILE_PULL) {
                    pullFilesFromNode(nodeCommunication, status, identity, security);
                } else if (nodeCommunication.getCommunicationType() == CommunicationType.FILE_PUSH) {
                    pushFilesToNode(nodeCommunication, status, identity, security);
                }
            }
        }
    }

    protected void pushFilesToNode(NodeCommunication nodeCommunication, RemoteNodeStatus status, Node identity,
            NodeSecurity security) {
        ProcessInfo processInfo = engine.getStatisticManager().newProcessInfo(new ProcessInfoKey(
                nodeCommunication.getNodeId(), identity.getNodeId(), ProcessType.FILE_SYNC_PUSH_JOB));
        IOutgoingWithResponseTransport transport = null;
        try {
            transport = engine.getTransportManager().getFilePushTransport(nodeCommunication.getNode(), identity,
                    security.getNodePassword(), parameterService.getRegistrationUrl());
            List<OutgoingBatch> batches = sendFiles(processInfo, nodeCommunication.getNode(), transport);
            if (batches.size() > 0) {
                List<BatchAck> batchAcks = readAcks(batches, transport, engine.getTransportManager(),
                        engine.getAcknowledgeService());
                status.updateOutgoingStatus(batches, batchAcks);
            }
        } catch (Exception e) {
            fireOffline(e, nodeCommunication.getNode(), status);
        } finally {
            if (transport != null) {
                transport.close();
            }
            if (processInfo.getStatus() != ProcessInfo.Status.ERROR) {
                processInfo.setStatus(ProcessInfo.Status.OK);
            }
        }
    }

    protected List<IncomingBatch> processZip(InputStream is, String sourceNodeId, ProcessInfo processInfo)
            throws IOException {
        File unzipDir = new File(parameterService.getTempDirectory(), String.format("filesync_incoming/%s/%s",
                engine.getNodeService().findIdentityNodeId(), sourceNodeId));
        FileUtils.deleteDirectory(unzipDir);
        unzipDir.mkdirs();

        AppUtils.unzip(is, unzipDir);

        Set<Long> batchIds = new TreeSet<Long>();
        String[] files = unzipDir.list(DirectoryFileFilter.INSTANCE);

        if (files != null) {
            for (int i = 0; i < files.length; i++) {
                try {
                    batchIds.add(Long.parseLong(files[i]));
                } catch (NumberFormatException e) {
                    log.error(
                            "Unexpected directory name.  Expected a number representing a batch id.  Instead the directory was named '{}'",
                            files[i]);
                }
            }
        }

        List<IncomingBatch> batchesProcessed = new ArrayList<IncomingBatch>();

        IIncomingBatchService incomingBatchService = engine.getIncomingBatchService();

        processInfo.setStatus(ProcessInfo.Status.LOADING);
        for (Long batchId : batchIds) {
            processInfo.setCurrentBatchId(batchId);
            processInfo.incrementBatchCount();
            File batchDir = new File(unzipDir, Long.toString(batchId));

            IncomingBatch incomingBatch = new IncomingBatch();

            File batchInfo = new File(batchDir, "batch-info.txt");
            if (batchInfo.exists()) {
                List<String> info = FileUtils.readLines(batchInfo);
                if (info != null && info.size() > 0) {
                    incomingBatch.setChannelId(info.get(0).trim());
                } else {
                    incomingBatch.setChannelId(Constants.CHANNEL_FILESYNC);
                }
            } else {
                incomingBatch.setChannelId(Constants.CHANNEL_FILESYNC);
            }

            incomingBatch.setBatchId(batchId);
            incomingBatch.setStatus(IncomingBatch.Status.LD);
            incomingBatch.setNodeId(sourceNodeId);
            incomingBatch.setByteCount(FileUtils.sizeOfDirectory(batchDir));
            batchesProcessed.add(incomingBatch);
            if (incomingBatchService.acquireIncomingBatch(incomingBatch)) {
                File syncScript = new File(batchDir, "sync.bsh");
                if (syncScript.exists()) {
                    String script = FileUtils.readFileToString(syncScript);
                    Interpreter interpreter = new Interpreter();
                    boolean isLocked = false;
                    try {
                        interpreter.set("log", log);
                        interpreter.set("batchDir", batchDir.getAbsolutePath().replace('\\', '/'));
                        interpreter.set("engine", engine);
                        interpreter.set("sourceNodeId", sourceNodeId);

                        long waitMillis = getParameterService().getLong(ParameterConstants.FILE_SYNC_LOCK_WAIT_MS);
                        log.debug("The {} node is attempting to get shared lock for to update incoming status",
                                sourceNodeId);
                        isLocked = engine.getClusterService().lock(ClusterConstants.FILE_SYNC_SHARED,
                                ClusterConstants.TYPE_SHARED, waitMillis);
                        if (isLocked) {
                            log.debug("The {} node got a shared file sync lock", sourceNodeId);
                            @SuppressWarnings("unchecked")
                            Map<String, String> filesToEventType = (Map<String, String>) interpreter.eval(script);
                            updateFileIncoming(sourceNodeId, filesToEventType);
                            incomingBatch.setStatementCount(filesToEventType != null ? filesToEventType.size() : 0);
                        } else {
                            throw new RuntimeException(
                                    "Could not obtain file sync shared lock within " + waitMillis + " millis");
                        }
                        incomingBatch.setStatus(IncomingBatch.Status.OK);
                        if (incomingBatchService.isRecordOkBatchesEnabled()) {
                            incomingBatchService.updateIncomingBatch(incomingBatch);
                        } else if (incomingBatch.isRetry()) {
                            incomingBatchService.deleteIncomingBatch(incomingBatch);
                        }
                    } catch (Throwable ex) {
                        if (ex instanceof TargetError) {
                            Throwable target = ((TargetError) ex).getTarget();
                            if (target != null) {
                                ex = target;
                            }
                        } else if (ex instanceof EvalError) {
                            log.error("Failed to evalulate the script:\n{}", script);
                        }

                        if (ex instanceof FileConflictException) {
                            log.error(ex.getMessage() + ".  Failed to process file sync batch " + batchId);
                        } else {
                            log.error("Failed to process file sync batch " + batchId, ex);
                        }

                        incomingBatch.setErrorFlag(true);
                        incomingBatch.setStatus(IncomingBatch.Status.ER);
                        incomingBatch.setSqlMessage(ex.getMessage());
                        if (incomingBatchService.isRecordOkBatchesEnabled() || incomingBatch.isRetry()) {
                            incomingBatchService.updateIncomingBatch(incomingBatch);
                        } else {
                            incomingBatchService.insertIncomingBatch(incomingBatch);
                        }
                        processInfo.setStatus(ProcessInfo.Status.ERROR);
                        break;
                    } finally {
                        log.debug("The {} node is done processing file sync files", sourceNodeId);
                        if (isLocked) {
                            engine.getClusterService().unlock(ClusterConstants.FILE_SYNC_SHARED,
                                    ClusterConstants.TYPE_SHARED);
                        }
                    }
                } else {
                    log.error("Could not find the sync.bsh script for batch {}", batchId);
                }
            }

        }

        return batchesProcessed;
    }

    protected void updateFileIncoming(String nodeId, Map<String, String> filesToEventType) {
        Set<String> filePaths = filesToEventType.keySet();
        for (String filePath : filePaths) {
            String eventType = filesToEventType.get(filePath);
            File file = new File(filePath);
            String fileName = file.getName();
            String dirName = file.getParentFile().getPath().replace('\\', '/');
            long lastUpdateTime = file.lastModified();
            int updateCount = sqlTemplate.update(getSql("updateFileIncoming"), nodeId, lastUpdateTime, eventType,
                    dirName, fileName);
            if (updateCount == 0) {
                sqlTemplate.update(getSql("insertFileIncoming"), nodeId, lastUpdateTime, eventType, dirName,
                        fileName);
            }
        }
    }

    protected void pullFilesFromNode(NodeCommunication nodeCommunication, RemoteNodeStatus status, Node identity,
            NodeSecurity security) {
        IIncomingTransport transport = null;
        ProcessInfo processInfo = engine.getStatisticManager().newProcessInfo(new ProcessInfoKey(
                nodeCommunication.getNodeId(), identity.getNodeId(), ProcessType.FILE_SYNC_PULL_JOB));
        try {
            processInfo.setStatus(ProcessInfo.Status.TRANSFERRING);

            transport = engine.getTransportManager().getFilePullTransport(nodeCommunication.getNode(), identity,
                    security.getNodePassword(), null, parameterService.getRegistrationUrl());

            List<IncomingBatch> batchesProcessed = processZip(transport.openStream(), nodeCommunication.getNodeId(),
                    processInfo);

            if (batchesProcessed.size() > 0) {
                processInfo.setStatus(ProcessInfo.Status.ACKING);
                status.updateIncomingStatus(batchesProcessed);
                sendAck(nodeCommunication.getNode(), identity, security, batchesProcessed,
                        engine.getTransportManager());
            }

        } catch (Exception e) {
            fireOffline(e, nodeCommunication.getNode(), status);
        } finally {
            if (transport != null) {
                transport.close();
            }

            if (processInfo.getStatus() != ProcessInfo.Status.ERROR) {
                processInfo.setStatus(ProcessInfo.Status.OK);
            }
        }

    }

    protected RemoteNodeStatuses queueJob(boolean force, long minimumPeriodMs, String clusterLock,
            CommunicationType type) {
        final RemoteNodeStatuses statuses = new RemoteNodeStatuses(
                engine.getConfigurationService().getChannels(false));
        Node identity = engine.getNodeService().findIdentity(false);
        if (identity != null && identity.isSyncEnabled()) {
            if (force || !engine.getClusterService().isInfiniteLocked(clusterLock)) {

                INodeCommunicationService nodeCommunicationService = engine.getNodeCommunicationService();
                List<NodeCommunication> nodes = nodeCommunicationService.list(type);
                int availableThreads = nodeCommunicationService.getAvailableThreads(type);
                for (NodeCommunication nodeCommunication : nodes) {
                    if (StringUtils.isNotBlank(nodeCommunication.getNode().getSyncUrl())
                            || !parameterService.isRegistrationServer()) {
                        boolean meetsMinimumTime = true;
                        if (minimumPeriodMs > 0 && nodeCommunication.getLastLockTime() != null
                                && (System.currentTimeMillis()
                                        - nodeCommunication.getLastLockTime().getTime()) < minimumPeriodMs) {
                            meetsMinimumTime = false;
                        }
                        if (availableThreads > 0 && meetsMinimumTime) {
                            if (nodeCommunicationService.execute(nodeCommunication, statuses, this)) {
                                availableThreads--;
                            }
                        }
                    } else {
                        log.warn(
                                "File sync cannot communicate with node '{}' in the group '{}'.  The sync url is blank",
                                nodeCommunication.getNode().getNodeId(),
                                nodeCommunication.getNode().getNodeGroupId());
                    }
                }
            } else {
                log.debug("Did not run the {} process because it has been stopped", type.name().toLowerCase());
            }
        }

        return statuses;
    }

    class FileTriggerMapper implements ISqlRowMapper<FileTrigger> {
        public FileTrigger mapRow(Row rs) {
            FileTrigger fileTrigger = new FileTrigger();
            fileTrigger.setBaseDir(
                    rs.getString("base_dir") == null ? null : rs.getString("base_dir").replace('\\', '/'));
            fileTrigger.setCreateTime(rs.getDateTime("create_time"));
            fileTrigger.setExcludesFiles(rs.getString("excludes_files"));
            fileTrigger.setIncludesFiles(rs.getString("includes_files"));
            fileTrigger.setLastUpdateBy(rs.getString("last_update_by"));
            fileTrigger.setLastUpdateTime(rs.getDateTime("last_update_time"));
            fileTrigger.setRecurse(rs.getBoolean("recurse"));
            fileTrigger.setSyncOnCreate(rs.getBoolean("sync_on_create"));
            fileTrigger.setSyncOnDelete(rs.getBoolean("sync_on_delete"));
            fileTrigger.setAfterCopyScript(rs.getString("after_copy_script"));
            fileTrigger.setBeforeCopyScript(rs.getString("before_copy_script"));
            fileTrigger.setSyncOnModified(rs.getBoolean("sync_on_modified"));
            fileTrigger.setSyncOnCtlFile(rs.getBoolean("sync_on_ctl_file"));
            fileTrigger.setDeleteAfterSync(rs.getBoolean("delete_after_sync"));
            fileTrigger.setTriggerId(rs.getString("trigger_id"));
            fileTrigger.setChannelId(rs.getString("channel_id"));
            fileTrigger.setReloadChannelId(rs.getString("reload_channel_id"));
            return fileTrigger;
        }
    }

    class FileTriggerRouterMapper implements ISqlRowMapper<FileTriggerRouter> {
        public FileTriggerRouter mapRow(Row rs) {
            FileTriggerRouter fileTriggerRouter = new FileTriggerRouter();
            String triggerId = rs.getString("trigger_id");
            FileTrigger fileTrigger = getFileTrigger(triggerId);
            fileTriggerRouter.setFileTrigger(fileTrigger);
            fileTriggerRouter.setConflictStrategy(
                    FileConflictStrategy.valueOf(rs.getString("conflict_strategy").toUpperCase()));
            fileTriggerRouter.setCreateTime(rs.getDateTime("create_time"));
            fileTriggerRouter.setLastUpdateBy(rs.getString("last_update_by"));
            fileTriggerRouter.setLastUpdateTime(rs.getDateTime("last_update_time"));
            fileTriggerRouter.setEnabled(rs.getBoolean("enabled"));
            fileTriggerRouter.setInitialLoadEnabled(rs.getBoolean("initial_load_enabled"));
            fileTriggerRouter.setTargetBaseDir((rs.getString("target_base_dir") == null) ? null
                    : rs.getString("target_base_dir").replace('\\', '/'));
            fileTriggerRouter
                    .setRouter(engine.getTriggerRouterService().getRouterById(true, rs.getString("router_id")));
            return fileTriggerRouter;
        }
    }

    class FileSnapshotMapper implements ISqlRowMapper<FileSnapshot> {
        public FileSnapshot mapRow(Row rs) {
            FileSnapshot fileSnapshot = new FileSnapshot();
            fileSnapshot.setCrc32Checksum(rs.getLong("crc32_checksum"));
            fileSnapshot.setCreateTime(rs.getDateTime("create_time"));
            fileSnapshot.setChannelId(rs.getString("channel_id"));
            fileSnapshot.setReloadChannelId(rs.getString("reload_channel_id"));
            fileSnapshot.setLastUpdateBy(rs.getString("last_update_by"));
            fileSnapshot.setLastUpdateTime(rs.getDateTime("last_update_time"));
            fileSnapshot.setFileModifiedTime(rs.getLong("file_modified_time"));
            fileSnapshot.setFileName(rs.getString("file_name"));
            fileSnapshot.setRelativeDir(
                    rs.getString("relative_dir") == null ? null : rs.getString("relative_dir").replace('\\', '/'));
            fileSnapshot.setFileSize(rs.getLong("file_size"));
            fileSnapshot.setLastEventType(LastEventType.fromCode(rs.getString("last_event_type")));
            fileSnapshot.setTriggerId(rs.getString("trigger_id"));
            fileSnapshot.setRouterId(rs.getString("router_id"));
            return fileSnapshot;
        }
    }
}