All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.triceo.splitlog.MessageStore Maven / Gradle / Ivy

package com.github.triceo.splitlog;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

import org.slf4j.Logger;

import com.github.triceo.splitlog.api.LogWatch;
import com.github.triceo.splitlog.api.Message;
import com.github.triceo.splitlog.logging.SplitlogLoggerFactory;

/**
 * Data storage for a particular {@link LogWatch}.
 *
 * This class is thread-safe.
 */
final class MessageStore {

    public static final int INITIAL_MESSAGE_POSITION = 0;
    private static final Logger LOGGER = SplitlogLoggerFactory.getLogger(MessageStore.class);

    private final int messageLimit;
    private int nextMessagePosition = MessageStore.INITIAL_MESSAGE_POSITION;
    private final SortedMap store = new TreeMap();

    /**
     * Create a message store with a maximum capacity of
     * {@link Integer#MAX_VALUE} messages. Will not actually allocate all that
     * space, but instead will keep growing as necessary.
     */
    public MessageStore() {
        this(Integer.MAX_VALUE);
    }

    /**
     * Create a message store with a given message capacity. Will not actually
     * allocate all that space, but instead will keep growing as necessary.
     */
    public MessageStore(final int size) {
        if (size <= 0) {
            throw new IllegalArgumentException("The message storage cannot have 0 or less capacity.");
        } else {
            this.messageLimit = size;
        }
    }

    /**
     * Add message to the storage.
     *
     * Every call will change values returned by {@link #getNextPosition()} and
     * {@link #getLatestPosition()}. Any call may change value returned by
     * {@link #getFirstPosition()}, which will happen if a message is discarded
     * due to hitting the message store capacity.
     *
     * @param msg
     *            Message in question.
     * @return Position of the message.
     */
    public synchronized int add(final Message msg) {
        final int nextKey = this.getNextPosition();
        this.store.put(nextKey, msg);
        MessageStore.LOGGER.info("Message #{} stored on position #{}", msg.getUniqueId(), nextKey);
        this.nextMessagePosition++;
        if (this.store.size() > this.messageLimit) {
            // discard first message if we're over limit
            this.store.remove(this.store.firstKey());
        }
        return nextKey;
    }

    /**
     * The maximum number of messages that will be held by this store at a time.
     * When a message is added that pushes the store over the limit, first
     * inserted message will be removed.
     *
     * @return Maximum possible amount of messages this store can hold before it
     *         starts discarding messages.
     */
    public int capacity() {
        return this.messageLimit;
    }

    /**
     * Remove messages from the queue that come before the given position. If
     * the ID is larger than {@link #getLatestPosition()}, all messages will be
     * discarded while marking no future messages for discarding.
     *
     * @param firstPositionNotToDiscard
     *            Messages be kept from this position onward, inclusive.
     * @return Number of messages actually discarded.
     */
    public synchronized int discardBefore(final int firstPositionNotToDiscard) {
        final int firstMessagePosition = this.getFirstPosition();
        if (this.getNextPosition() == MessageStore.INITIAL_MESSAGE_POSITION) {
            MessageStore.LOGGER.info("Not discarding any messages, as there haven't been any messages yet.");
            return 0;
        } else if ((firstPositionNotToDiscard < MessageStore.INITIAL_MESSAGE_POSITION)
                || (firstPositionNotToDiscard <= firstMessagePosition)) {
            MessageStore.LOGGER.info(
                    "Not discarding any messages, as there are no messages with position lower than {}.",
                    firstPositionNotToDiscard);
            return 0;
        } else if (firstPositionNotToDiscard > this.getLatestPosition()) {
            MessageStore.LOGGER.info("Discarding all messages, as all have lower positions than {}.",
                    firstPositionNotToDiscard);
            final int size = this.store.size();
            this.store.clear();
            return size;
        }
        // and now actually discard
        MessageStore.LOGGER.info("Discarding messages in positions <{},{}).", firstMessagePosition,
                firstPositionNotToDiscard);
        final SortedMap toDiscard = this.store.headMap(firstPositionNotToDiscard);
        final int size = toDiscard.size();
        toDiscard.clear();
        return size;
    }

    /**
     * Return all messages currently present.
     *
     * @return Unmodifiable list containing those messages.
     */
    public synchronized List getAll() {
        final int firstMessagePosition = this.getFirstPosition();
        if (firstMessagePosition < MessageStore.INITIAL_MESSAGE_POSITION) {
            return Collections.unmodifiableList(Collections. emptyList());
        }
        return this.getFrom(firstMessagePosition);
    }

    /**
     * The first position that is occupied by a message.
     *
     * @return -1 if no messages yet. 0 if no messages have been discarded. Add
     *         one for every discarded message.
     */
    public synchronized int getFirstPosition() {
        if (this.store.isEmpty()) {
            return MessageStore.INITIAL_MESSAGE_POSITION - 1;
        } else {
            return this.store.firstKey();
        }
    }

    /**
     * Return all messages on positions higher or equal to the given.
     *
     * @param startPosition
     *            Least position, inclusive.
     * @return Unmodifiable list containing those messages.
     */
    public synchronized List getFrom(final int startPosition) {
        return this.getFromRange(startPosition, this.getNextPosition());
    }

    /**
     * Return all messages on positions in the given range.
     *
     * @param startPosition
     *            Least position, inclusive.
     * @param endPosition
     *            Greatest position, exclusive.
     * @return Unmodifiable list containing those messages.
     */
    public synchronized List getFromRange(final int startPosition, final int endPosition) {
        // cache this here, so all parts of the method operate on the same data
        final int firstMessageId = this.getFirstPosition();
        // input validation
        if ((startPosition < MessageStore.INITIAL_MESSAGE_POSITION)
                || (endPosition < MessageStore.INITIAL_MESSAGE_POSITION)) {
            throw new IllegalArgumentException("Message position cannot be negative.");
        } else if ((firstMessageId >= MessageStore.INITIAL_MESSAGE_POSITION) && (startPosition < firstMessageId)) {
            throw new IllegalArgumentException("Message at position " + startPosition
                    + " had already been discarded. First available message position is " + firstMessageId + ".");
        } else if (endPosition <= startPosition) {
            throw new IllegalArgumentException("Ending position must be larger than starting message position.");
        } else if (endPosition > this.getNextPosition()) {
            throw new IllegalArgumentException("Range end cannot be greater than the next message position.");
        }
        // and properly synchronized range retrieval
        return Collections.unmodifiableList(new LinkedList(this.store.subMap(startPosition, endPosition)
                .values()));
    }

    /**
     * The latest position that has already been filled with a message.
     *
     * @return -1 if no messages yet.
     */
    public synchronized int getLatestPosition() {
        return (this.store.isEmpty()) ? this.nextMessagePosition - 1 : this.store.lastKey();
    }

    /**
     * The position that will be occupied by the message that goes through the
     * very next {@link #add(Message)} call.
     *
     * @return 0 if no messages have been inserted yet.
     */
    public synchronized int getNextPosition() {
        return this.nextMessagePosition;
    }

    /**
     * Whether or not this message store currently holds any messages.
     *
     * @return True if so, false if not. Will be false even if there were some
     *         messages before that got discarded and now there are none.
     */
    public synchronized boolean isEmpty() {
        return this.store.isEmpty();
    }

    /**
     * How many messages are currently stored here.
     *
     * It is the number of messages that went through {@link #add(Message)} and
     * that have not been discarded since, either through
     * {@link #discardBefore(int)} or automatically due to capacity.
     *
     * @return
     */
    public synchronized int size() {
        return this.store.size();
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy