jgnash.engine.attachment.NettyTransferHandler.java Source code

Java tutorial

Introduction

Here is the source code for jgnash.engine.attachment.NettyTransferHandler.java

Source

/*
 * jGnash, a personal finance application
 * Copyright (C) 2001-2015 Craig Cavanaugh
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package jgnash.engine.attachment;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;

import jgnash.engine.AttachmentUtils;
import jgnash.util.EncryptionManager;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import static javax.xml.bind.DatatypeConverter.parseBase64Binary;
import static javax.xml.bind.DatatypeConverter.printBase64Binary;

/**
 * Handles the details of bi-directional transfer of files between a client and server.
 *
 * @author Craig Cavanaugh
 */
@ChannelHandler.Sharable
class NettyTransferHandler extends SimpleChannelInboundHandler<String> {

    public static final String FILE_REQUEST = "<FILE_REQUEST>";

    public static final String DELETE = "<DELETE>";

    public static final String EOL_DELIMITER = "\r\n";

    private static final String FILE_STARTS = "<FILE_STARTS>";

    private static final String FILE_ENDS = "<FILE_ENDS>";

    private static final String FILE_CHUNK = "<FILE_CHUNK>";

    private static final String ERROR = "<ERROR>";

    private static final Logger logger = Logger.getLogger(NettyTransferHandler.class.getName());

    public static final int TRANSFER_BUFFER_SIZE = 1024; // too large can break netty... bug?

    public static final int PATH_MAX = 4096;

    private final Map<String, Attachment> fileMap = new ConcurrentHashMap<>();

    private final Path attachmentPath;

    private final EncryptionManager encryptionManager;

    /**
     * Netty Handler.  The specified path may be a temporary location for clients or a persistent location for servers.
     *
     * @param attachmentPath Path for attachments.
     */
    public NettyTransferHandler(final Path attachmentPath, final EncryptionManager encryptionManager) {
        this.attachmentPath = attachmentPath;
        this.encryptionManager = encryptionManager;
    }

    @Override
    public void channelRead0(final ChannelHandlerContext ctx, final String msg) {

        final String plainMessage;

        if (encryptionManager != null) {
            plainMessage = encryptionManager.decrypt(msg);
        } else {
            plainMessage = msg;
        }

        if (plainMessage.startsWith(FILE_REQUEST)) {
            sendFile(ctx, attachmentPath + File.separator + plainMessage.substring(FILE_REQUEST.length()));
        } else if (plainMessage.startsWith(FILE_STARTS)) {
            openOutputStream(plainMessage.substring(FILE_STARTS.length()));
        } else if (plainMessage.startsWith(FILE_CHUNK)) {
            writeOutputStream(plainMessage.substring(FILE_CHUNK.length()));
        } else if (plainMessage.startsWith(FILE_ENDS)) {
            closeOutputStream(plainMessage.substring(FILE_ENDS.length()));
        } else if (plainMessage.startsWith(DELETE)) {
            deleteFile(plainMessage.substring(FILE_ENDS.length()));
        }
    }

    private void deleteFile(final String fileName) {
        Path path = Paths.get(attachmentPath + File.separator + fileName);

        if (Files.exists(path)) {
            try {
                Files.delete(path);
            } catch (final IOException e) {
                logger.log(Level.SEVERE, e.getLocalizedMessage(), e);
            }
        }
    }

    @Override
    public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
        for (Attachment object : fileMap.values()) {
            try {
                object.fileOutputStream.close();
            } catch (IOException e) {
                logger.log(Level.SEVERE, e.getLocalizedMessage(), e);
            }
        }

        ctx.fireChannelInactive(); // forward to the next handler in the pipeline
    }

    @Override
    public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        logger.log(Level.WARNING, "Unexpected exception from downstream.", cause);
        ctx.close();
    }

    private String encrypt(final String message) {
        if (encryptionManager != null) {
            return encryptionManager.encrypt(message);
        }
        return message;
    }

    /**
     * Sends a file across the channel. The Fu
     * @param channel  Channel to send file through
     * @param fileName the file name
     * @return the future of the potentially asynchronous send is returned. A null value is returned if fileName is a path.
     */
    public Future<Void> sendFile(final Channel channel, final String fileName) {
        Future<Void> future = null;

        Path path = Paths.get(fileName);

        if (Files.exists(path)) {

            if (Files.isDirectory(path)) {
                channel.writeAndFlush(encrypt(ERROR + "Not a file: " + path) + EOL_DELIMITER);
                return null;
            }

            try (InputStream fileInputStream = Files.newInputStream(path)) {
                channel.writeAndFlush(
                        encrypt(FILE_STARTS + path.getFileName() + ":" + Files.size(path)) + EOL_DELIMITER);

                byte[] bytes = new byte[TRANSFER_BUFFER_SIZE]; // leave room for base 64 expansion

                int bytesRead;

                while ((bytesRead = fileInputStream.read(bytes)) != -1) {
                    if (bytesRead > 0) {
                        channel.write(encrypt(FILE_CHUNK + path.getFileName() + ':'
                                + printBase64Binary(Arrays.copyOfRange(bytes, 0, bytesRead))) + EOL_DELIMITER);
                    }
                }
                future = channel.writeAndFlush(encrypt(FILE_ENDS + path.getFileName()) + EOL_DELIMITER).sync();

            } catch (IOException | InterruptedException e) {
                logger.log(Level.SEVERE, e.getLocalizedMessage(), e);
            }
        } else {
            try {
                future = channel.writeAndFlush(encrypt(ERROR + "File not found: " + path) + EOL_DELIMITER).sync();
            } catch (final InterruptedException e) {
                logger.log(Level.SEVERE, e.getLocalizedMessage(), e);
            }
            logger.log(Level.WARNING, "File not found: {0}", path);
        }

        return future;
    }

    private void sendFile(final ChannelHandlerContext ctx, final String msg) {
        sendFile(ctx.channel(), msg);
    }

    private void closeOutputStream(final String msg) {
        Attachment attachment = fileMap.get(msg);

        try {
            attachment.fileOutputStream.close();
            fileMap.remove(msg);
        } catch (final IOException e) {
            logger.log(Level.SEVERE, e.getLocalizedMessage(), e);
        }

        if (attachment.path.toFile().length() != attachment.fileSize) {
            logger.severe("Invalid file length");
        }
    }

    private void writeOutputStream(final String msg) {
        String[] msgParts = msg.split(":");

        Attachment attachment = fileMap.get(msgParts[0]);

        if (attachment != null) {
            try {
                attachment.fileOutputStream.write(parseBase64Binary(msgParts[1]));
            } catch (final IOException e) {
                logger.log(Level.SEVERE, e.getLocalizedMessage(), e);
            }
        }
    }

    private void openOutputStream(final String msg) {
        String[] msgParts = msg.split(":");

        final String fileName = msgParts[0];
        final long fileLength = Long.parseLong(msgParts[1]);

        final Path filePath = Paths.get(attachmentPath + File.separator + fileName);

        // Lazy creation of the attachment path if needed
        if (!AttachmentUtils.createAttachmentDirectory(attachmentPath)) {
            logger.severe("Unable to find or create the attachment directory");
            return;
        }

        try {
            fileMap.put(fileName, new Attachment(filePath, fileLength));
        } catch (IOException e) {
            logger.log(Level.SEVERE, e.getLocalizedMessage(), e);
        }
    }

    private static class Attachment {
        final Path path;

        final OutputStream fileOutputStream;

        final long fileSize;

        private Attachment(final Path path, long fileSize) throws IOException {
            this.path = path;
            fileOutputStream = Files.newOutputStream(path);

            this.fileSize = fileSize;
        }
    }
}