001package co.codewizards.cloudstore.core.progress;
002
003import java.text.DecimalFormat;
004import java.text.NumberFormat;
005import java.util.regex.Pattern;
006
007import org.slf4j.Logger;
008
009/**
010 * A progress monitor implementation which logs to an SLF4J {@link Logger}.
011 *
012 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
013 */
014public class LoggerProgressMonitor implements ProgressMonitor
015{
016        private Logger logger;
017
018        /**
019         * Create a monitor logging to the specified logger.
020         *
021         * @param logger the logger to write to. Must not be <code>null</code>.
022         */
023        public LoggerProgressMonitor(Logger logger) {
024                if (logger == null)
025                        throw new IllegalArgumentException("logger == null");
026
027                this.logger = logger;
028        }
029
030        /**
031         * The variable containing the task-name (i.e. what is done). This is the name which is passed to
032         * the {@link ProgressMonitor#beginTask(String, int)} method.
033         * @see #setMessage(String)
034         */
035        public static final String MESSAGE_VARIABLE_NAME = "co.codewizards.cloudstore.aggregator";
036
037        /**
038         * The variable containing the current percentage.
039         * @see #setMessage(String)
040         */
041        public static final String MESSAGE_VARIABLE_PERCENTAGE = "${percentage}";
042
043        private String message = MESSAGE_VARIABLE_NAME + ": " + MESSAGE_VARIABLE_PERCENTAGE;
044
045        /**
046         * Get the log-message. For details see {@link #setMessage(String)}.
047         * @return the message to be logged.
048         * @see #setMessage(String)
049         */
050        public synchronized String getMessage() {
051                return message;
052        }
053
054        /**
055         * <p>Set the message which will be written to the logger.</p>
056         * <p>
057         * Before writing to the logger, the variables contained in this message
058         * are replaced by their actual values. The following variables can be used:
059         * </p>
060         * <ul>
061         * <li>{@link #MESSAGE_VARIABLE_NAME}</li>
062         * <li>{@link #MESSAGE_VARIABLE_PERCENTAGE}</li>
063         * </ul>
064         *
065         * @param message the message to be logged.
066         * @see #getMessage()
067         */
068        public synchronized void setMessage(String message) {
069                if (message == null)
070                        throw new IllegalArgumentException("message must not be null!");
071
072                this.message = message;
073        }
074
075        /**
076         * Get the logger to which this monitor is writing. It is the one that has been passed
077         * to {@link #LoggerProgressMonitor(Logger)} before.
078         * @return the logger.
079         */
080        public Logger getLogger() {
081                return logger;
082        }
083
084        private String name;
085
086        private double internalTotalWork = Double.NaN;
087
088        private double internalWorked = 0;
089
090        private float percentageWorked = 0;
091        private float lastLogMessage_percentageWorked = Float.MIN_VALUE;
092
093        private long lastLogMessage_timestamp = 0;
094
095        private long logMinPeriodMSec = 5000;
096
097        /**
098         * Get the minimum log period in milliseconds. For details see {@link #setLogMinPeriodMSec(long)}.
099         * @return the minimum log period.
100         * @see #setLogMinPeriodMSec(long)
101         */
102        public synchronized long getLogMinPeriodMSec() {
103                return logMinPeriodMSec;
104        }
105        /**
106         * <p>
107         * Set the minimum period between log messages in milliseconds.
108         * </p>
109         * <p>
110         * In order to prevent log flooding as well as to improve performance,
111         * calling {@link #worked(int)} or {@link #internalWorked(double)} will only cause
112         * a log message to be printed, if at least one of the following criteria are met:
113         * </p>
114         * <ul>
115         * <li>
116         * The last log message happened longer ago than the time (in milliseconds) configured
117         * by the property <code>logMinPeriodMSec</code> (i.e. by this method).
118         * </li>
119         * <li>
120         * The percentage  difference to the last log message is larger than
121         * {@link #getLogMinPercentageDifference() the min-percentage-difference}.
122         * </li>
123         * <li>
124         * 100% are reached.
125         * </li>
126         * </ul>
127         * @param logMinPeriodMSec the minimum period between log messages.
128         * @see #getLogMinPeriodMSec()
129         * @see #setLogMinPercentageDifference(float)
130         */
131        public synchronized void setLogMinPeriodMSec(long logMinPeriodMSec) {
132                this.logMinPeriodMSec = logMinPeriodMSec;
133        }
134
135        private float logMinPercentageDifference = 5f;
136
137        /**
138         * Get the minimum percentage difference to trigger a new log message. For details see
139         * {@link #setLogMinPercentageDifference(float)}.
140         * @return the minimum percentage difference.
141         * @see #setLogMinPercentageDifference(float)
142         */
143        public synchronized float getLogMinPercentageDifference() {
144                return logMinPercentageDifference;
145        }
146        /**
147         * <p>
148         * Set the minimum period between log messages in milliseconds.
149         * </p>
150         * <p>
151         * In order to prevent log flooding as well as to improve performance,
152         * calling {@link #worked(int)} or {@link #internalWorked(double)} will only cause
153         * a log message to be printed, if at least one of the following criteria are met:
154         * </p>
155         * <ul>
156         * <li>
157         * The last log message happened longer ago than the time (in milliseconds) configured
158         * by the property {@link #setLogMinPeriodMSec(long) logMinPeriodMSec}.
159         * </li>
160         * <li>
161         * The percentage difference to the last log message is larger than
162         * the percentage configured by the property
163         * <code>logMinPercentageDifference</code> (i.e. by this method).
164         * </li>
165         * <li>
166         * 100% are reached.
167         * </li>
168         * </ul>
169         * @param logMinPercentageDifference the minimum percentage difference between log messages.
170         * @see #getLogMinPercentageDifference()
171         * @see #setLogMinPeriodMSec(long)
172         */
173        public synchronized void setLogMinPercentageDifference(float logMinPercentageDifference) {
174                this.logMinPercentageDifference = logMinPercentageDifference;
175        }
176
177        private int nestedBeginTasks = 0;
178
179        @Override
180        public synchronized void beginTask(String name, int totalWork) {
181                // Ignore nested begin task calls.
182                if (++nestedBeginTasks > 1)
183                        return;
184
185                if (name == null)
186                        name = "anonymous";
187
188                if (totalWork < 0)
189                        totalWork = 0;
190
191                this.name = name;
192                this.internalTotalWork = totalWork;
193        }
194
195        @Override
196        public synchronized void done() {
197                // Ignore if more done calls than beginTask calls or if we are still
198                // in some nested beginTasks
199                if (nestedBeginTasks == 0 || --nestedBeginTasks > 0)
200                        return;
201
202                double stillToWork = internalTotalWork - internalWorked;
203                if (stillToWork > 0)
204                        internalWorked(stillToWork); // To do whatever still needs to be done
205        }
206
207        @Override
208        public synchronized void internalWorked(double worked) {
209                if (worked < 0 || worked == Double.NaN)
210                        return;
211
212                if (this.internalWorked == this.internalTotalWork)
213                        return;
214
215                this.internalWorked += worked;
216                if (this.internalWorked > this.internalTotalWork)
217                        this.internalWorked = this.internalTotalWork;
218
219                this.percentageWorked = (float) (100d * this.internalWorked / this.internalTotalWork);
220                boolean doLog = false;
221
222                // log at 100%
223                if (!doLog && (this.internalWorked == this.internalTotalWork))
224                        doLog = true;
225
226                // log when the percentage difference is larger than our minimum
227                if (!doLog && (this.percentageWorked - lastLogMessage_percentageWorked >= logMinPercentageDifference))
228                        doLog = true;
229
230                // log when the last log happened very long ago (longer than our minimum period).
231                long now = System.currentTimeMillis();
232                if (!doLog && (now - lastLogMessage_timestamp >= logMinPeriodMSec))
233                        doLog = true;
234
235                if (doLog) {
236                        lastLogMessage_percentageWorked = this.percentageWorked;
237                        lastLogMessage_timestamp = now;
238
239                        String percentageString = PERCENTAGE_FORMAT.format(this.percentageWorked) + '%';
240                        String msg = message.replaceAll(Pattern.quote(MESSAGE_VARIABLE_NAME), name);
241                        msg = msg.replaceAll(Pattern.quote(MESSAGE_VARIABLE_PERCENTAGE), percentageString);
242                        switch (logLevel) {
243                                case trace:
244                                        logger.trace(msg);
245                                        break;
246                                case debug:
247                                        logger.debug(msg);
248                                        break;
249                                case info:
250                                        logger.info(msg);
251                                        break;
252                                case warn:
253                                        logger.warn(msg);
254                                        break;
255                                case error:
256                                        logger.error(msg);
257                                        break;
258                                default:
259                                        throw new IllegalStateException("Unknown logLevel: " + logLevel);
260                        }
261                }
262        }
263
264        /**
265         * The level to use for logging.
266         *
267         * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
268         *
269         * @see LoggerProgressMonitor#setLogLevel(LogLevel)
270         */
271        public enum LogLevel {
272                trace,
273                debug,
274                info,
275                warn,
276                error
277        }
278
279        private LogLevel logLevel = LogLevel.info;
280
281        /**
282         * Get the log-level that is used when writing to the logger.
283         * @return the log-level to be used.
284         */
285        public LogLevel getLogLevel() {
286                return logLevel;
287        }
288        /**
289         * Set the log-level to use when writing to the logger.
290         * @param logLevel the {@link LogLevel} to be used.
291         */
292        public synchronized void setLogLevel(LogLevel logLevel) {
293                if (logLevel == null)
294                        throw new IllegalArgumentException("logLevel must not be null!");
295
296                this.logLevel = logLevel;
297        }
298
299        private static final NumberFormat PERCENTAGE_FORMAT = new DecimalFormat("0.00");
300
301        private volatile boolean canceled = false; // better use volatile, because the canceled flag might be accessed from different threads.
302
303        @Override
304        public boolean isCanceled() {
305                return canceled;
306        }
307
308        @Override
309        public void setCanceled(boolean canceled) {
310                this.canceled = canceled;
311        }
312
313        @Override
314        public synchronized void setTaskName(String name) {
315                this.name = name; // TODO not sure, if this is a correct implementation
316        }
317
318        @Override
319        public synchronized void subTask(String name) {
320                this.name = name; // TODO not sure, if this is a correct implementation
321        }
322
323        @Override
324        public void worked(int work) {
325                internalWorked(work);
326        }
327
328}