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}