package com.github.triceo.splitlog;
import com.github.triceo.splitlog.api.*;
import com.github.triceo.splitlog.logging.SplitlogLoggerFactory;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/**
* Default log watch implementation which provides all the bells and whistles so
* that the rest of the tool can work together.
*
* The tailer thread will only be started after {@link #startFollowing()} or
* {@link #startConsuming(MessageListener)} is called. Subsequently, it will
* only be stopped after there no more running {@link Follower}s or
* {@link MessageConsumer}s.
*/
final class DefaultLogWatch implements LogWatch {
private static final AtomicLong ID_GENERATOR = new AtomicLong(0);
private static final Logger LOGGER = SplitlogLoggerFactory.getLogger(DefaultLogWatch.class);
private final ConsumerManager<LogWatch> consumers = new ConsumerManager<>(this);
private final SimpleMessageCondition gateCondition;
private final BidiMap<String, MessageMeasure<? extends Number, Follower>> handingDown = new DualHashBidiMap<>();
private final AtomicBoolean isStarted = new AtomicBoolean(false);
private final AtomicBoolean isStopped = new AtomicBoolean(false);
private final LogWatchStorageManager storage;
private final LogWatchTailingManager tailing;
private final long uniqueId = DefaultLogWatch.ID_GENERATOR.getAndIncrement();
private final File watchedFile;
protected DefaultLogWatch(final LogWatchBuilder builder, final TailSplitter splitter) {
this.gateCondition = builder.getGateCondition();
this.storage = new LogWatchStorageManager(this, builder);
this.watchedFile = builder.getFileToWatch();
this.tailing = new LogWatchTailingManager(this, builder, splitter);
}
@Override
public int countConsumers() {
return this.consumers.countConsumers();
}
/**
* <strong>This is not part of the public API.</strong> Purely for purposes
* of testing the automated message sweep.
*
* @return How many messages there currently are in the internal message
* store.
*/
int countMessagesInStorage() {
return this.storage.getMessageStore().size();
}
@Override
public int countMetrics() {
return this.consumers.countMetrics();
}
/**
* Return all messages that have been sent to a given {@link Follower}, from
* its {@link #startFollowing()} until either its
* {@link #stopFollowing(Follower)} or to this moment, whichever is
* relevant.
*
* @param follower
* The follower in question.
* @return Unmodifiable list of all the received messages, in the order
* received.
*/
protected Iterable<Message> getAllMessages(final Follower follower) {
return this.storage.getAllMessages(follower);
}
@Override
public MessageMetric<? extends Number, LogWatch> getMetric(final String id) {
return this.consumers.getMetric(id);
}
@Override
public String getMetricId(final MessageMetric<? extends Number, LogWatch> measure) {
return this.consumers.getMetricId(measure);
}
public long getUniqueId() {
return this.uniqueId;
}
@Override
public File getWatchedFile() {
return this.watchedFile;
}
private boolean hasToLetMessageThroughTheGate(final Message message) {
if (this.gateCondition.accept(message)) {
DefaultLogWatch.LOGGER.debug("Message '{}' passed gate condition {} in {}.", message, this.gateCondition,
this);
return true;
} else {
DefaultLogWatch.LOGGER.info("Message '{}' stopped at the gate in {}.", message, this);
return false;
}
}
@Override
public boolean isConsuming(final MessageConsumer<LogWatch> consumer) {
return this.consumers.isConsuming(consumer);
}
@Override
public boolean isFollowedBy(final Follower follower) {
return this.isConsuming(follower);
}
@Override
public synchronized boolean isHandingDown(final MessageMeasure<? extends Number, Follower> measure) {
return this.handingDown.containsValue(measure);
}
@Override
public synchronized boolean isHandingDown(final String id) {
return this.handingDown.containsKey(id);
}
@Override
public boolean isMeasuring(final MessageMetric<? extends Number, LogWatch> metric) {
return this.consumers.isMeasuring(metric);
}
@Override
public boolean isMeasuring(final String id) {
return this.consumers.isMeasuring(id);
}
@Override
public boolean isStarted() {
return this.isStarted.get();
}
@Override
public boolean isStopped() {
return this.isStopped.get();
}
/**
* Notify {@link MessageConsumer}s of a message that is either
* {@link MessageDeliveryStatus#ACCEPTED} or
* {@link MessageDeliveryStatus#REJECTED}.
*
* @param message
* The message in question.
* @return Null if stopped at the gate by
* {@link LogWatchBuilder#getGateCondition()},
* {@link MessageDeliveryStatus#ACCEPTED} if accepted in
* {@link LogWatchBuilder#getStorageCondition()},
* {@link MessageDeliveryStatus#REJECTED} otherwise.
*/
public MessageDeliveryStatus messageArrived(final Message message) {
if (!this.hasToLetMessageThroughTheGate(message)) {
return null;
}
final boolean messageAccepted = this.storage.registerMessage(message, this);
final MessageDeliveryStatus status = messageAccepted ? MessageDeliveryStatus.ACCEPTED
: MessageDeliveryStatus.REJECTED;
this.consumers.messageReceived(message, status, this);
return status;
}
/**
* Notify {@link MessageConsumer}s of a message that is
* {@link MessageDeliveryStatus#INCOMING}.
*
* @param message
* The message in question.
* @return True if the message was passed to {@link MessageConsumer}s, false
* if stopped at the gate by
* {@link LogWatchBuilder#getGateCondition()}.
*/
public boolean messageIncoming(final Message message) {
if (!this.hasToLetMessageThroughTheGate(message)) {
return false;
}
this.consumers.messageReceived(message, MessageDeliveryStatus.INCOMING, this);
return true;
}
@Override
public boolean start() {
if (!this.isStarted.compareAndSet(false, true)) {
return false;
}
this.tailing.start();
return true;
}
@Override
public MessageConsumer<LogWatch> startConsuming(final MessageListener<LogWatch> consumer) {
return this.consumers.startConsuming(consumer);
}
@Override
public Follower startFollowing() {
return this.startFollowingActually(null).getKey();
}
private synchronized Map.Entry<Follower, Future<Message>> startFollowingActually(
final MidDeliveryMessageCondition<LogWatch> condition) {
if (this.isStopped()) {
throw new IllegalStateException("Cannot start following on an already terminated LogWatch.");
}
// assemble list of consumers to be handing down and then the follower
final List<Pair<String, MessageMeasure<? extends Number, Follower>>> pairs = new ArrayList<>();
for (final BidiMap.Entry<String, MessageMeasure<? extends Number, Follower>> entry : this.handingDown
.entrySet()) {
pairs.add(ImmutablePair.of(entry.getKey(),
entry.getValue()));
}
// register the follower
final Follower follower = new DefaultFollower(this, pairs);
final Future<Message> expectation = condition == null ? null : follower.expect(condition);
this.consumers.registerConsumer(follower);
this.storage.followerStarted(follower);
DefaultLogWatch.LOGGER.info("Registered {} for {}.", follower, this);
return ImmutablePair.of(follower, expectation);
}
@Override
public synchronized boolean startHandingDown(final MessageMeasure<? extends Number, Follower> measure,
final String id) {
if (this.isStopped()) {
throw new IllegalStateException("Log watch already terminated.");
} else if (measure == null) {
throw new IllegalArgumentException("Measure may not be null.");
} else if (id == null) {
throw new IllegalArgumentException("ID may not be null.");
} else if (this.handingDown.containsKey(id) || this.handingDown.containsValue(measure)) {
return false;
}
this.handingDown.put(id, measure);
return true;
}
@Override
public <T extends Number> MessageMetric<T, LogWatch> startMeasuring(final MessageMeasure<T, LogWatch> measure,
final String id) {
return this.consumers.startMeasuring(measure, id);
}
/**
* Invoking this method will cause the running message sweep to be
* de-scheduled. Any currently present {@link Message}s will only be removed
* from memory when this watch instance is removed from memory.
*/
@Override
public synchronized boolean stop() {
if (!this.isStarted()) {
throw new IllegalStateException("Cannot terminate what was not started.");
} else if (!this.isStopped.compareAndSet(false, true)) {
return false;
}
DefaultLogWatch.LOGGER.info("Terminating {}.", this);
this.tailing.stop();
this.consumers.stop();
this.handingDown.clear();
this.storage.logWatchTerminated();
DefaultLogWatch.LOGGER.info("Terminated {}.", this);
return true;
}
@Override
public boolean stopConsuming(final MessageConsumer<LogWatch> consumer) {
if (!this.consumers.stopConsuming(consumer)) {
return false;
}
if (consumer instanceof Follower) {
this.storage.followerTerminated((Follower) consumer);
}
return true;
}
/**
* Is synchronized since we want to prevent multiple stops of the same
* follower.
*/
@Override
public synchronized boolean stopFollowing(final Follower follower) {
if (!this.isFollowedBy(follower)) {
return false;
}
this.stopConsuming(follower);
DefaultLogWatch.LOGGER.info("Unregistered {} for {}.", follower, this);
return true;
}
@Override
public synchronized boolean stopHandingDown(final MessageMeasure<? extends Number, Follower> measure) {
return (this.handingDown.removeValue(measure) != null);
}
@Override
public synchronized boolean stopHandingDown(final String id) {
return (this.handingDown.remove(id) != null);
}
@Override
public boolean stopMeasuring(final MessageMetric<? extends Number, LogWatch> metric) {
return this.consumers.stopMeasuring(metric);
}
@Override
public boolean stopMeasuring(final String id) {
return this.consumers.stopMeasuring(id);
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("DefaultLogWatch [getUniqueId()=").append(this.getUniqueId()).append(", ");
if (this.getWatchedFile() != null) {
builder.append("getWatchedFile()=").append(this.getWatchedFile()).append(", ");
}
builder.append("isStopped()=").append(this.isStopped()).append("]");
return builder.toString();
}
}