com.jkoolcloud.tnt4j.streams.inputs.RedirectTNT4JStream.java Source code

Java tutorial

Introduction

Here is the source code for com.jkoolcloud.tnt4j.streams.inputs.RedirectTNT4JStream.java

Source

/*
 * Copyright 2014-2017 JKOOL, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.jkoolcloud.tnt4j.streams.inputs;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.lang3.StringUtils;

import com.jkoolcloud.tnt4j.core.OpLevel;
import com.jkoolcloud.tnt4j.sink.DefaultEventSinkFactory;
import com.jkoolcloud.tnt4j.sink.EventSink;
import com.jkoolcloud.tnt4j.streams.configure.StreamProperties;
import com.jkoolcloud.tnt4j.streams.inputs.feeds.Feed;
import com.jkoolcloud.tnt4j.streams.inputs.feeds.ReaderFeed;
import com.jkoolcloud.tnt4j.streams.outputs.JKCloudJsonOutput;
import com.jkoolcloud.tnt4j.streams.utils.RedirectTNT4JStreamFormatter;
import com.jkoolcloud.tnt4j.streams.utils.StreamsResources;
import com.jkoolcloud.tnt4j.streams.utils.StreamsThread;
import com.jkoolcloud.tnt4j.streams.utils.Utils;

/**
 * Implements a redirecting activity stream, where activity data is prepared by other TNT4J based streaming libraries
 * (e.g., tnt4j-stream-jmx, tnt4j-stream-gc) using {@link com.jkoolcloud.tnt4j.format.JSONFormatter} to format activity
 * data. Redirected activities JSON data ban be read from the specified InputStream-based stream or Reader-based reader.
 * This class wraps the raw {@link InputStream} or {@link Reader} with a {@link BufferedReader}. Input source also can
 * be {@link File} descriptor or {@link ServerSocket} connections.
 * <p>
 * In case input source is {@link ServerSocket} connections, there is stream property 'RestartOnInputClose' allowing to
 * restart {@link ServerSocket} (open new {@link ServerSocket} instance) if listened {@link ServerSocket} gets closed or
 * fails to accept connections.
 * <p>
 * This activity stream requires parsers that can support {@link String} activity data.
 * <p>
 * This activity stream supports the following properties (in addition to those supported by {@link TNTInputStream}):
 * <ul>
 * <li>FileName - the system-dependent file name. (Required - just one 'FileName' or 'Port')</li>
 * <li>Port - port number to accept character stream over TCP/IP. (Required - just one 'FileName' or 'Port')</li>
 * <li>RestartOnInputClose - flag indicating to restart {@link ServerSocket} (open new {@link ServerSocket} instance) if
 * listened server socked gets closed or fails to accept connection. (Optional)</li>
 * <li>BufferSize - maximal buffer queue capacity. Default value - 512. (Optional)</li>
 * <li>BufferOfferTimeout - how long to wait if necessary for space to become available when adding data item to buffer
 * queue. Default value - 45sec. (Optional)</li>
 * </ul>
 *
 * @version $Revision: 1 $
 *
 * @see ArrayBlockingQueue
 * @see BlockingQueue#offer(Object, long, TimeUnit)
 * @see RedirectTNT4JStreamFormatter
 * @see ReaderFeed
 */
public class RedirectTNT4JStream extends TNTInputStream<String, String> {
    private static final EventSink LOGGER = DefaultEventSinkFactory.defaultEventSink(RedirectTNT4JStream.class);

    private static final int DEFAULT_INPUT_BUFFER_SIZE = 512;
    private static final int DEFAULT_INPUT_BUFFER_OFFER_TIMEOUT = 3 * 15; // NOTE:
    // sec.
    private static final Object DIE_MARKER = new Object();

    private int bufferSize = DEFAULT_INPUT_BUFFER_SIZE;
    private int bufferOfferTimeout = DEFAULT_INPUT_BUFFER_OFFER_TIMEOUT;
    private boolean restartOnInputClose = false;

    private Reader rawReader;
    private String fileName = null;
    private Integer socketPort = null;

    private FeedersProducer feedsProducer;

    protected BlockingQueue<Object> inputBuffer;

    /**
     * Constructs an empty RedirectTNT4JStream. Requires configuration settings to set input stream source.
     */
    public RedirectTNT4JStream() {
        super();
    }

    /**
     * Constructs a new RedirectTNT4JStream to obtain activity data from the specified {@link InputStream}.
     *
     * @param stream
     *            input stream to read data from
     */
    public RedirectTNT4JStream(InputStream stream) {
        this();
        setStream(stream);
    }

    /**
     * Constructs a new RedirectTNT4JStream to obtain activity data from the specified {@link Reader}.
     *
     * @param reader
     *            reader to read data from
     */
    public RedirectTNT4JStream(Reader reader) {
        this();
        setReader(reader);
    }

    @Override
    protected EventSink logger() {
        return LOGGER;
    }

    /**
     * Sets {@link InputStream} from which activity data should be read.
     *
     * @param stream
     *            input stream to read data from
     */
    public void setStream(InputStream stream) {
        setReader(new InputStreamReader(stream));
    }

    /**
     * Sets {@link Reader} from which activity data should be read.
     *
     * @param reader
     *            reader to read data from
     */
    public void setReader(Reader reader) {
        rawReader = reader;
    }

    @Override
    protected void setDefaultStreamOutput() {
        setOutput(new JKCloudJsonOutput());
    }

    @Override
    public void setProperties(Collection<Map.Entry<String, String>> props) throws Exception {
        if (props == null) {
            return;
        }

        super.setProperties(props);

        for (Map.Entry<String, String> prop : props) {
            String name = prop.getKey();
            String value = prop.getValue();
            if (StreamProperties.PROP_FILENAME.equalsIgnoreCase(name)) {
                if (socketPort != null) {
                    throw new IllegalStateException(StreamsResources.getStringFormatted(
                            StreamsResources.RESOURCE_BUNDLE_NAME, "TNTInputStream.cannot.set.both",
                            StreamProperties.PROP_FILENAME, StreamProperties.PROP_PORT));
                }
                fileName = value;
            } else if (StreamProperties.PROP_PORT.equalsIgnoreCase(name)) {
                if (StringUtils.isNotEmpty(fileName)) {
                    throw new IllegalStateException(StreamsResources.getStringFormatted(
                            StreamsResources.RESOURCE_BUNDLE_NAME, "TNTInputStream.cannot.set.both",
                            StreamProperties.PROP_FILENAME, StreamProperties.PROP_PORT));
                }
                socketPort = Integer.valueOf(value);
            } else if (StreamProperties.PROP_RESTART_ON_CLOSE.equalsIgnoreCase(name)) {
                restartOnInputClose = Boolean.parseBoolean(value);
            } else if (StreamProperties.PROP_BUFFER_SIZE.equalsIgnoreCase(name)) {
                bufferSize = Integer.parseInt(value);
            } else if (StreamProperties.PROP_OFFER_TIMEOUT.equalsIgnoreCase(name)) {
                bufferOfferTimeout = Integer.parseInt(value);
            }
        }
    }

    @Override
    public Object getProperty(String name) {
        if (StreamProperties.PROP_FILENAME.equalsIgnoreCase(name)) {
            return fileName;
        }
        if (StreamProperties.PROP_PORT.equalsIgnoreCase(name)) {
            return socketPort;
        }
        if (StreamProperties.PROP_RESTART_ON_CLOSE.equalsIgnoreCase(name)) {
            return restartOnInputClose;
        }
        if (StreamProperties.PROP_BUFFER_SIZE.equalsIgnoreCase(name)) {
            return bufferSize;
        }
        if (StreamProperties.PROP_OFFER_TIMEOUT.equalsIgnoreCase(name)) {
            return bufferOfferTimeout;
        }

        return super.getProperty(name);
    }

    @Override
    protected void initialize() throws Exception {
        super.initialize();

        inputBuffer = new ArrayBlockingQueue<>(bufferSize, true);

        initializeStreamInternals();
    }

    @Override
    protected void start() throws Exception {
        super.start();

        startDataStream();

        logger().log(OpLevel.DEBUG,
                StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME, "TNTInputStream.stream.start"),
                getClass().getSimpleName(), getName());
    }

    private void initializeStreamInternals() throws Exception {
        if (rawReader == null) {
            if (StringUtils.isEmpty(fileName) && socketPort == null) {
                throw new IllegalStateException(StreamsResources.getStringFormatted(
                        StreamsResources.RESOURCE_BUNDLE_NAME, "TNTInputStream.property.undefined.one.of",
                        StreamProperties.PROP_FILENAME, StreamProperties.PROP_PORT));
            }

            if (fileName != null) {
                setStream(new FileInputStream(fileName));
            } else if (socketPort != null) {
                feedsProducer = new ServerSocketFeedsProducer(socketPort);
            } else {
                throw new IllegalStateException(StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                        "FeedInputStream.no.stream.source"));
            }
        }
    }

    /**
     * Sets up the input data stream or reader to prepare it for reading.
     *
     * @throws IOException
     *             if an I/O error preparing the stream
     */
    protected void startDataStream() throws IOException {
        if (rawReader == null && feedsProducer == null) {
            throw new IOException(StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                    "FeedInputStream.no.stream.reader"));
        }

        if (rawReader != null) {
            feedsProducer = new RawReaderFeedsProducer(rawReader);
        }

        feedsProducer.start();
    }

    private boolean addInputToBuffer(String inputData) {
        if (inputData != null && !isHalted()) {
            try {
                boolean added = inputBuffer.offer(inputData, bufferOfferTimeout, TimeUnit.SECONDS);

                if (!added) {
                    logger().log(OpLevel.WARNING, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                            "AbstractBufferedStream.changes.buffer.limit"), bufferOfferTimeout, inputData);
                }

                return added;
            } catch (InterruptedException exc) {
                logger().log(OpLevel.WARNING, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                        "AbstractBufferedStream.offer.interrupted"), inputData);
            }
        }

        return false;
    }

    /**
     * Adds terminator object to input buffer.
     */
    @Override
    protected void stopInternals() {
        if (inputBuffer != null) {
            // inputBuffer.clear(); //???
            inputBuffer.offer(DIE_MARKER);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method does not actually return the next item, but the {@link BufferedReader} from which the next item
     * should be read. This is useful for parsers that accept {@link Reader}s that are using underlying classes to
     * process the data from an input stream. The parser, or its underlying data reader needs to handle all I/O, along
     * with any associated errors.
     */
    @Override
    public String getNextItem() throws Exception {
        if (inputBuffer == null) {
            throw new IllegalStateException(StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                    "AbstractBufferedStream.changes.buffer.uninitialized"));
        }

        // Buffer is empty and producer input is ended. No more items going to
        // be available.
        if (inputBuffer.isEmpty() && isInputEnded()) {
            return null;
        }

        Object qe = inputBuffer.take();

        // Producer input was slower than consumer, but was able to put "DIE"
        // marker object to queue. No more items going to be available.
        if (DIE_MARKER.equals(qe)) {
            return null;
        }

        String activityInput = (String) qe;

        addStreamedBytesCount(activityInput == null ? 0 : activityInput.getBytes().length);

        return activityInput;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Redirects raw activity data (already JSON formatted by TNT4J based producers) from input to output.
     */
    @Override
    protected void processActivityItem(String item, AtomicBoolean failureFlag) throws Exception {
        notifyProgressUpdate(incrementCurrentActivitiesCount(), getTotalActivities());

        logger().log(OpLevel.TRACE, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                "RedirectTNT4JStream.sending.item"), item);

        getOutput().logItem(item);
    }

    private boolean isInputEnded() {
        return feedsProducer == null || feedsProducer.isStopRunning();
    }

    @Override
    protected void cleanup() {
        cleanupStreamInternals();

        super.cleanup();
    }

    private void cleanupStreamInternals() {
        if (feedsProducer != null) {
            feedsProducer.halt();
        }

        if (inputBuffer != null) {
            inputBuffer.clear();
        }
    }

    private abstract class FeedersProducer extends StreamsThread implements Closeable {
        List<ActivitiesFeeder> activeFeedersList = new ArrayList<>();

        void removeInactiveFeeder(ActivitiesFeeder conn) {
            activeFeedersList.remove(conn);
        }

        @Override
        public void close() {
            for (ActivitiesFeeder f : activeFeedersList) {
                f.halt();
            }

            activeFeedersList.clear();

            inputBuffer.offer(DIE_MARKER);
        }

        @Override
        public void halt(boolean interrupt) {
            super.halt(interrupt);
            close();
        }
    }

    private class ServerSocketFeedsProducer extends FeedersProducer {
        private int srvSocketPort;
        private ServerSocket srvSocket;

        ServerSocketFeedsProducer(int srvSocketPort) throws IOException {
            this.srvSocketPort = srvSocketPort;
            srvSocket = new ServerSocket(srvSocketPort);
        }

        @Override
        public void run() {
            while (!isStopRunning() && !srvSocket.isClosed()) {
                Socket connSocket = null;
                try {
                    logger().log(OpLevel.INFO, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                            "FeedInputStream.waiting.for.connection"), srvSocketPort);
                    connSocket = srvSocket.accept();
                    logger().log(OpLevel.INFO, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                            "FeedInputStream.accepted.connection"), connSocket);
                } catch (Exception e) {
                    logger().log(OpLevel.ERROR, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                            "RedirectTNT4JStream.failed.accept.connection"), e.getLocalizedMessage(), e);

                    boolean recovered = restartOnInputClose && resetDataStream();

                    if (!recovered) {
                        halt(false);
                    }
                }

                if (!isStopRunning() && connSocket != null) {
                    try {
                        ActivitiesFeeder feeder = new ActivitiesFeeder(connSocket);
                        activeFeedersList.add(feeder);
                        feeder.start();
                    } catch (Exception e) {
                        logger().log(OpLevel.ERROR,
                                StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                                        "RedirectTNT4JStream.socket.initialization"),
                                e.getLocalizedMessage(), e);
                    }
                }
            }

            close();
        }

        private boolean resetDataStream() {
            logger().log(OpLevel.DEBUG, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                    "RedirectTNT4JStream.resetting.stream"), getName());

            Utils.close(srvSocket);

            try {
                srvSocket = new ServerSocket(srvSocketPort);

                logger().log(OpLevel.DEBUG, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                        "RedirectTNT4JStream.stream.reset"), srvSocketPort);
            } catch (Exception exc) {
                logger().log(OpLevel.ERROR, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                        "RedirectTNT4JStream.resetting.failed"), getName(), exc);

                return false;
            }

            return true;
        }

        @Override
        public void close() {
            Utils.close(srvSocket);

            super.close();
        }
    }

    private class RawReaderFeedsProducer extends FeedersProducer {
        private ActivitiesFeeder rawFeeder;

        RawReaderFeedsProducer(Reader rawReader) {
            this.rawFeeder = new ActivitiesFeeder(rawReader);
        }

        @Override
        public void run() {
            activeFeedersList.add(rawFeeder);
            rawFeeder.start();
        }
    }

    private class ActivitiesFeeder extends StreamsThread implements Closeable {
        private Socket socket = null;

        /**
         * BufferedReader that wraps {@link Socket#getInputStream()} or {@link Reader}
         */
        protected ReaderFeed dataReader = null;

        ActivitiesFeeder(Socket socket) throws IOException {
            this.socket = socket;
            this.dataReader = new ReaderFeed(socket.getInputStream());
            this.dataReader.addFeedListener(new StreamFeedsListener());
        }

        ActivitiesFeeder(Reader reader) {
            this.dataReader = new ReaderFeed(reader);
            this.dataReader.addFeedListener(new StreamFeedsListener());
        }

        @Override
        public void run() {
            while (!isStopRunning() && !(dataReader.isClosed() || dataReader.hasError())) {
                try {
                    String line = dataReader.getInput().readLine();

                    if (line == null) {
                        logger().log(OpLevel.INFO, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                                "RedirectTNT4JStream.feeder.data.ended"));
                        halt(); // no more data items to process
                    } else {
                        if (line.isEmpty()) {
                            logger().log(OpLevel.WARNING,
                                    StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                                            "RedirectTNT4JStream.redirect.empty.input"));
                            incrementSkippedActivitiesCount();
                            notifyStreamEvent(OpLevel.WARNING,
                                    StreamsResources.getStringFormatted(StreamsResources.RESOURCE_BUNDLE_NAME,
                                            "RedirectTNT4JStream.redirect.empty.input", line),
                                    line);
                        } else {
                            addInputToBuffer(line);
                        }
                    }
                } catch (IOException ioe) {
                    logger().log(OpLevel.WARNING, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                            "RedirectTNT4JStream.feeder.failure"), ioe.getLocalizedMessage());
                    halt();
                } catch (Exception e) {
                    logger().log(OpLevel.ERROR, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                            "RedirectTNT4JStream.feeder.failure"), e.getLocalizedMessage(), e);
                }
            }

            close();
        }

        @Override
        public void close() {
            Utils.close(dataReader);
            dataReader = null;

            if (socket != null) {
                logger().log(OpLevel.DEBUG, StreamsResources.getString(StreamsResources.RESOURCE_BUNDLE_NAME,
                        "FeedInputStream.closing.stream.connection"), socket);

                Utils.close(socket);
                socket = null;
            }

            if (feedsProducer != null) {
                feedsProducer.removeInactiveFeeder(this);
            }
        }

        @Override
        public void halt(boolean interrupt) {
            super.halt(interrupt);
            close();
        }
    }

    private class StreamFeedsListener implements Feed.FeedListener {
        @Override
        public void bytesReadFromInput(int bCount) {
            addStreamedBytesCount(bCount);
        }
    }
}