package co.codewizards.cloudstore.core.progress; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.regex.Pattern; import org.slf4j.Logger; /** * A progress monitor implementation which logs to an SLF4J {@link Logger}. * * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de */ public class LoggerProgressMonitor implements ProgressMonitor { private final Logger logger; /** * Create a monitor logging to the specified logger. * * @param logger the logger to write to. Must not be <code>null</code>. */ public LoggerProgressMonitor(final Logger logger) { if (logger == null) throw new IllegalArgumentException("logger == null"); this.logger = logger; } /** * The variable containing the task-name (i.e. what is done). This is the name which is passed to * the {@link ProgressMonitor#beginTask(String, int)} method. * @see #setMessage(String) */ public static final String MESSAGE_VARIABLE_NAME = "${name}"; /** * The variable containing the current percentage. * @see #setMessage(String) */ public static final String MESSAGE_VARIABLE_PERCENTAGE = "${percentage}"; private String message = MESSAGE_VARIABLE_NAME + ": " + MESSAGE_VARIABLE_PERCENTAGE; /** * Get the log-message. For details see {@link #setMessage(String)}. * @return the message to be logged. * @see #setMessage(String) */ public synchronized String getMessage() { return message; } /** * <p>Set the message which will be written to the logger.</p> * <p> * Before writing to the logger, the variables contained in this message * are replaced by their actual values. The following variables can be used: * </p> * <ul> * <li>{@link #MESSAGE_VARIABLE_NAME}</li> * <li>{@link #MESSAGE_VARIABLE_PERCENTAGE}</li> * </ul> * * @param message the message to be logged. * @see #getMessage() */ public synchronized void setMessage(final String message) { if (message == null) throw new IllegalArgumentException("message must not be null!"); this.message = message; } /** * Get the logger to which this monitor is writing. It is the one that has been passed * to {@link #LoggerProgressMonitor(Logger)} before. * @return the logger. */ public Logger getLogger() { return logger; } private String name; private double internalTotalWork = Double.NaN; private double internalWorked = 0; protected float percentageWorked = 0; private float lastLogMessage_percentageWorked = Float.MIN_VALUE; private long lastLogMessage_timestamp = 0; private long logMinPeriodMSec = 5000; /** * Get the minimum log period in milliseconds. For details see {@link #setLogMinPeriodMSec(long)}. * @return the minimum log period. * @see #setLogMinPeriodMSec(long) */ public synchronized long getLogMinPeriodMSec() { return logMinPeriodMSec; } /** * <p> * Set the minimum period between log messages in milliseconds. * </p> * <p> * In order to prevent log flooding as well as to improve performance, * calling {@link #worked(int)} or {@link #internalWorked(double)} will only cause * a log message to be printed, if at least one of the following criteria are met: * </p> * <ul> * <li> * The last log message happened longer ago than the time (in milliseconds) configured * by the property <code>logMinPeriodMSec</code> (i.e. by this method). * </li> * <li> * The percentage difference to the last log message is larger than * {@link #getLogMinPercentageDifference() the min-percentage-difference}. * </li> * <li> * 100% are reached. * </li> * </ul> * @param logMinPeriodMSec the minimum period between log messages. * @see #getLogMinPeriodMSec() * @see #setLogMinPercentageDifference(float) */ public synchronized void setLogMinPeriodMSec(final long logMinPeriodMSec) { this.logMinPeriodMSec = logMinPeriodMSec; } private float logMinPercentageDifference = 5f; /** * Get the minimum percentage difference to trigger a new log message. For details see * {@link #setLogMinPercentageDifference(float)}. * @return the minimum percentage difference. * @see #setLogMinPercentageDifference(float) */ public synchronized float getLogMinPercentageDifference() { return logMinPercentageDifference; } /** * <p> * Set the minimum period between log messages in milliseconds. * </p> * <p> * In order to prevent log flooding as well as to improve performance, * calling {@link #worked(int)} or {@link #internalWorked(double)} will only cause * a log message to be printed, if at least one of the following criteria are met: * </p> * <ul> * <li> * The last log message happened longer ago than the time (in milliseconds) configured * by the property {@link #setLogMinPeriodMSec(long) logMinPeriodMSec}. * </li> * <li> * The percentage difference to the last log message is larger than * the percentage configured by the property * <code>logMinPercentageDifference</code> (i.e. by this method). * </li> * <li> * 100% are reached. * </li> * </ul> * @param logMinPercentageDifference the minimum percentage difference between log messages. * @see #getLogMinPercentageDifference() * @see #setLogMinPeriodMSec(long) */ public synchronized void setLogMinPercentageDifference(final float logMinPercentageDifference) { this.logMinPercentageDifference = logMinPercentageDifference; } private int nestedBeginTasks = 0; @Override public synchronized void beginTask(String name, int totalWork) { // Ignore nested begin task calls. if (++nestedBeginTasks > 1) return; if (name == null) name = "anonymous"; if (totalWork < 0) totalWork = 0; this.name = name; this.internalTotalWork = totalWork; } @Override public synchronized void done() { // Ignore if more done calls than beginTask calls or if we are still // in some nested beginTasks if (nestedBeginTasks == 0 || --nestedBeginTasks > 0) return; final double stillToWork = internalTotalWork - internalWorked; if (stillToWork > 0) internalWorked(stillToWork); // To do whatever still needs to be done } @Override public synchronized void internalWorked(final double worked) { if (worked < 0 || worked == Double.NaN) return; if (this.internalWorked == this.internalTotalWork) return; this.internalWorked += worked; if (this.internalWorked > this.internalTotalWork) this.internalWorked = this.internalTotalWork; this.percentageWorked = (float) (100d * this.internalWorked / this.internalTotalWork); boolean doLog = false; // log at 100% if (!doLog && (this.internalWorked == this.internalTotalWork)) doLog = true; // log when the percentage difference is larger than our minimum if (!doLog && (this.percentageWorked - lastLogMessage_percentageWorked >= logMinPercentageDifference)) doLog = true; // log when the last log happened very long ago (longer than our minimum period). final long now = System.currentTimeMillis(); if (!doLog && (now - lastLogMessage_timestamp >= logMinPeriodMSec)) doLog = true; if (doLog) { lastLogMessage_percentageWorked = this.percentageWorked; lastLogMessage_timestamp = now; final String percentageString = PERCENTAGE_FORMAT.format(this.percentageWorked) + '%'; String msg = message.replaceAll(Pattern.quote(MESSAGE_VARIABLE_NAME), name); msg = msg.replaceAll(Pattern.quote(MESSAGE_VARIABLE_PERCENTAGE), percentageString); switch (logLevel) { case trace: logger.trace(msg); break; case debug: logger.debug(msg); break; case info: logger.info(msg); break; case warn: logger.warn(msg); break; case error: logger.error(msg); break; default: throw new IllegalStateException("Unknown logLevel: " + logLevel); } } } /** * The level to use for logging. * * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de * * @see LoggerProgressMonitor#setLogLevel(LogLevel) */ public enum LogLevel { trace, debug, info, warn, error } private LogLevel logLevel = LogLevel.info; /** * Get the log-level that is used when writing to the logger. * @return the log-level to be used. */ public LogLevel getLogLevel() { return logLevel; } /** * Set the log-level to use when writing to the logger. * @param logLevel the {@link LogLevel} to be used. */ public synchronized void setLogLevel(final LogLevel logLevel) { if (logLevel == null) throw new IllegalArgumentException("logLevel must not be null!"); this.logLevel = logLevel; } private static final NumberFormat PERCENTAGE_FORMAT = new DecimalFormat("0.00"); private volatile boolean canceled = false; // better use volatile, because the canceled flag might be accessed from different threads. @Override public boolean isCanceled() { return canceled; } @Override public void setCanceled(final boolean canceled) { this.canceled = canceled; } @Override public synchronized void setTaskName(final String name) { this.name = name; // TODO not sure, if this is a correct implementation } @Override public synchronized void subTask(final String name) { this.name = name; // TODO not sure, if this is a correct implementation } @Override public void worked(final int work) { internalWorked(work); } }