001 package org.hackystat.sensor.ant.task; 002 003 import java.io.File; 004 import java.util.ArrayList; 005 import java.util.Date; 006 import java.util.List; 007 import java.util.Properties; 008 009 import org.apache.tools.ant.BuildException; 010 import org.apache.tools.ant.DirectoryScanner; 011 import org.apache.tools.ant.Project; 012 import org.apache.tools.ant.Task; 013 import org.apache.tools.ant.types.FileSet; 014 import org.hackystat.sensorshell.SensorShell; 015 import org.hackystat.sensorshell.SensorShellException; 016 import org.hackystat.sensorshell.SensorShellProperties; 017 import org.hackystat.sensorshell.usermap.SensorShellMap; 018 import org.hackystat.sensorshell.usermap.SensorShellMapException; 019 import org.hackystat.utilities.stacktrace.StackTrace; 020 import org.hackystat.utilities.tstamp.TstampSet; 021 022 /** 023 * An abstract superclass containing instance variables and helper methods used by Hackystat Sensors 024 * implemented as Ant tasks. This includes almost all of the sensors except for the Ant Build 025 * sensor, which is implemented as a listener. 026 * 027 * @author Philip Johnson 028 */ 029 public abstract class HackystatSensorTask extends Task { 030 031 /** The list of datafile element instances found in the task. */ 032 protected List<DataFiles> dataFilesList = new ArrayList<DataFiles>(); 033 034 /** The list of sourcefile element instances found in this task. */ 035 protected List<SourceFiles> sourceFilesList = new ArrayList<SourceFiles>(); 036 037 /** Whether or not to print out additional messages during sensor execution. */ 038 protected boolean verbose = false; 039 040 /** Whether or not to throw a BuildException if problems occur during sensor execution. */ 041 protected boolean failOnError = true; 042 043 /** Provides a fixed runtime value for use in sensors that need one. */ 044 protected long runtime = new Date().getTime(); 045 046 /** The sensor shell instance used by this sensor. */ 047 protected SensorShell sensorShell; 048 049 /** Sensor properties to be used with the sensor. */ 050 protected SensorShellProperties sensorProps; 051 052 /** Supports generation of unique timestamps. */ 053 protected TstampSet tstampSet = new TstampSet(); 054 055 /** The name of this tool, passed to SensorShell and also looked for in UserMap. */ 056 protected String tool; 057 058 /** Tool account in the UserMap to use, or null if not using UserMap. */ 059 protected String toolAccount; 060 061 /** The string that prefixes all error messages from this sensor. */ 062 protected String errMsgPrefix; 063 064 /** The string that prefixes all normal messages from this sensor. */ 065 protected String msgPrefix; 066 067 /** Prevent no-arg constructor from being instantiated externally by accident. */ 068 @SuppressWarnings("unused") 069 private HackystatSensorTask() { 070 } 071 072 /** The number of times you can retry this sensor before failing. */ 073 private int retryAttempts = 0; 074 075 /** The amount of time to wait between retries. */ 076 private int retryWaitInterval = 1; 077 078 /** 079 * The standard constructor, which will instantiate a SensorShell using the configuration data in 080 * sensorshell.properties. 081 * 082 * @param tool The tool associated with this sensor. 083 */ 084 protected HackystatSensorTask(String tool) { 085 this.tool = tool; 086 this.msgPrefix = "[Hackystat " + tool + " Sensor] "; 087 this.errMsgPrefix = msgPrefix + "ERROR: "; 088 } 089 090 /** 091 * A special constructor to be used only for testing purposes, which will use the supplied 092 * SensorBase host, email, and password. Automatically sets verbose mode to true. 093 * 094 * @param host The host. 095 * @param email The email. 096 * @param password The password. 097 * @param tool The tool associated with this sensor. 098 */ 099 protected HackystatSensorTask(String host, String email, String password, String tool) { 100 this(tool); 101 this.verbose = true; 102 verboseInfo("Creating a test sensorshell..."); 103 try { 104 this.sensorProps = SensorShellProperties.getTestInstance(host, email, password); 105 } 106 catch (SensorShellException e) { 107 throw new BuildException(errMsgPrefix + "Problem making test sensorshell.properties", e); 108 } 109 this.sensorShell = new SensorShell(this.sensorProps, false, tool); 110 } 111 112 /** 113 * Instantiates the internal sensorshell using either the data in the UserMap or the data in 114 * sensorshell.properties. Does not instantiate the sensorshell if it already has been done in the 115 * testing constructor. 116 * 117 * DO NOT call this method in your constructor. Instead, call it at the beginning of your 118 * execute() method. The reason is that the toolAccount property which controls where the 119 * SensorShell data is obtained is not set until Ant finishes constructor setup. 120 */ 121 protected void setupSensorShell() { 122 Properties preferMultiShell = new Properties(); 123 // Enabling multishell adds almost 5 seconds of startup time. Therefore, I'm not going to 124 // enable it by default. I'm leaving this in here as commented out code in case someone 125 // in future wants to see how to create a custom default for all Ant sensors that can still 126 // be overridden in the user's sensorshell.properties file. 127 // For now, I just pass in an empty properties instance, which does nothing. 128 // preferMultiShell.setProperty(SensorShellProperties.SENSORSHELL_MULTISHELL_ENABLED_KEY, 129 // "true"); 130 131 if (isUsingUserMap()) { 132 verboseInfo("Creating a SensorShell based upon UserMap data..."); 133 try { 134 SensorShellMap map = new SensorShellMap(this.tool); 135 this.sensorShell = map.getUserShell(this.toolAccount, preferMultiShell); 136 this.sensorProps = this.sensorShell.getProperties(); 137 } 138 catch (SensorShellMapException e) { 139 signalError("Problem creating a UserMap-based SensorShell", e); 140 } 141 } 142 // Instantiate the sensorshell using sensorshell.properties, unless we've already created one 143 // using the test constructor. 144 else if (this.sensorProps == null && this.sensorShell == null) { 145 verboseInfo("Creating a SensorShell using sensorshell.properties data..."); 146 // use the sensor.properties file 147 try { 148 this.sensorProps = new SensorShellProperties(preferMultiShell, false); 149 this.sensorShell = new SensorShell(this.sensorProps, false, this.tool); 150 } 151 catch (SensorShellException e) { 152 signalError("Problem creating the SensorShell", e); 153 } 154 } 155 verboseInfo(this.sensorShell.getProperties().toString()); 156 verboseInfo("Maximum Java heap size is: " + Runtime.getRuntime().maxMemory()); 157 158 } 159 160 /** 161 * Set the verbose attribute to "on", "true", or "yes" to enable trace messages while the sensor 162 * is running. Default is false. 163 * 164 * @param mode The new verbose value: should be "on", "true", or "yes" to enable. 165 */ 166 public void setVerbose(String mode) { 167 this.verbose = Project.toBoolean(mode); 168 verboseInfo("verbose is set to: " + this.verbose); 169 } 170 171 /** 172 * Set the retryWaitIntervalSeconds value to an integer, or set to default if the supplied value 173 * was not an integer. 174 * 175 * @param retryString The new retryWaitIntervalSeconds value, an integer, as a string. 176 */ 177 public void setRetryWaitInterval(String retryString) { 178 int retry = 0; 179 try { 180 retry = Integer.parseInt(retryString); 181 } 182 catch (Exception e) { 183 info("Failed to parse attribute retryAttempts. Setting to default."); 184 } 185 this.retryWaitInterval = retry; 186 verboseInfo("retryWaitInterval is set to: " + this.retryWaitInterval); 187 } 188 189 /** 190 * Set the retryAttempts value to an integer, or set to default if the supplied value was not an 191 * integer. 192 * 193 * @param retryString The new retryAttempts value, an integer, as a string. 194 */ 195 public void setRetryAttempts(String retryString) { 196 int retry = 0; 197 try { 198 retry = Integer.parseInt(retryString); 199 } 200 catch (Exception e) { 201 info("Failed to parse attribute retryAttempts. Setting to default."); 202 } 203 this.retryAttempts = retry; 204 verboseInfo("retryAttempts is set to: " + this.retryAttempts); 205 } 206 207 /** 208 * The execute() method invoked by Ant. This method invokes the subclass executeInternal() method, 209 * and if that method throws an exception, it will retry according to the values of retryAttempts 210 * and retryWaitInterval. 211 * 212 * @throws BuildException If there is an error after all the retries are done. 213 */ 214 @Override 215 public void execute() throws BuildException { 216 for (int i = retryAttempts; i >= 0; i--) { 217 try { 218 executeInternal(); 219 return; 220 } 221 catch (Exception e) { 222 // If we're all out of retries (or never had any), then just rethrow this exception. 223 if (i == 0) { 224 throw new BuildException(e); 225 } 226 // Else, we indicate what happened, sleep, and go through the loop again. 227 info("Sensor failed: " + e.getMessage()); 228 info("Retrying (" + i + " retries remaining.)"); 229 info("Pausing for: " + this.retryWaitInterval + " seconds."); 230 try { 231 Thread.sleep(this.retryWaitInterval * 1000); 232 } 233 catch (Exception f) { 234 info("Problem trying to sleep. We ignore."); 235 } 236 } 237 } 238 } 239 240 /** 241 * This must be implemented by all subclasses to provide the traditional Ant execute() method. 242 * The executeInternal may be invoked multiple times, depending upon the retryAttempts value. 243 */ 244 public abstract void executeInternal(); 245 246 /** 247 * Set the failOnError attribute to "on", "true", or "yes" to throw a BuildException if problems 248 * occur during execution. Default is true. 249 * 250 * @param mode The new verbose value: should be "on", "true", or "yes" to enable. 251 */ 252 public void setFailOnError(String mode) { 253 this.failOnError = Project.toBoolean(mode); 254 verboseInfo("failOnError is set to: " + this.failOnError); 255 } 256 257 /** 258 * Allows the user to override the default tool string to be used to retrieve data from the 259 * UserMap. Note that UserMap processing is only enabled when the toolAccount value is provided. 260 * 261 * @param tool The tool containing the tool account to be used when sending data. 262 */ 263 public void setUserMapTool(String tool) { 264 this.tool = tool; 265 } 266 267 /** 268 * If the user specifies a toolAccount, then the UserMap is consulted to specify the sensorshell 269 * host, user, and password data rather than sensorshell.properties. Note that the user does not 270 * need to specify the tool explicitly, it will be provided with a default value by the sensor, 271 * although specifying it explicitly might be good for documentation purposes. 272 * 273 * @param toolAccount The tool account in the UserMap to use when sending data. 274 */ 275 public void setUserMapToolAccount(String toolAccount) { 276 this.toolAccount = toolAccount; 277 } 278 279 /** 280 * Creates a returns a new SourceFiles instance to Ant. Ant will then populate this guy with its 281 * internal file set. 282 * 283 * @return The SourceFiles instance. 284 */ 285 public SourceFiles createSourceFiles() { 286 SourceFiles newSourceFiles = new SourceFiles(); 287 this.sourceFilesList.add(newSourceFiles); 288 return newSourceFiles; 289 } 290 291 /** 292 * Creates a returns a new DataFiles instance to Ant. Ant will then populate this guy with its 293 * internal file set. 294 * 295 * @return The DataFiles instance. 296 */ 297 public DataFiles createDataFiles() { 298 DataFiles newDataFiles = new DataFiles(); 299 this.dataFilesList.add(newDataFiles); 300 return newDataFiles; 301 } 302 303 /** 304 * Returns the list of files indicated in the sourcefiles element. 305 * 306 * @return The list of files in the sourcefiles element. 307 */ 308 protected List<File> getSourceFiles() { 309 List<FileSet> filesets = new ArrayList<FileSet>(); 310 // Create our list of filesets from all nested SourceFiles. 311 for (SourceFiles sourceFiles : this.sourceFilesList) { 312 filesets.addAll(sourceFiles.getFileSets()); 313 } 314 return getFiles(filesets); 315 } 316 317 /** 318 * Returns the list of files indicated in the datafiles element. 319 * 320 * @return The list of files in the datafiles element. 321 */ 322 protected List<File> getDataFiles() { 323 List<FileSet> filesets = new ArrayList<FileSet>(); 324 // Create our list of filesets from all nested SourceFiles. 325 for (DataFiles dataFiles : this.dataFilesList) { 326 filesets.addAll(dataFiles.getFileSets()); 327 } 328 return getFiles(filesets); 329 } 330 331 /** 332 * Converts a list of FileSets into a list of their associated files. 333 * 334 * @param filesets The filesets of interest. 335 * @return The files of interest. 336 */ 337 protected List<File> getFiles(List<FileSet> filesets) { 338 ArrayList<File> fileList = new ArrayList<File>(); 339 final int size = filesets.size(); 340 for (int i = 0; i < size; i++) { 341 FileSet fs = filesets.get(i); 342 DirectoryScanner ds = fs.getDirectoryScanner(getProject()); 343 ds.scan(); 344 String[] f = ds.getIncludedFiles(); 345 346 for (int j = 0; j < f.length; j++) { 347 String pathname = f[j]; 348 File file = new File(ds.getBasedir(), pathname); 349 file = getProject().resolveFile(file.getPath()); 350 fileList.add(file); 351 } 352 } 353 return fileList; 354 } 355 356 /** 357 * Returns true if the user has indicated they want to use the UserMap to obtain the SensorBase 358 * host, user, and password. If false, then the sensorshell.properties file should be consulted. 359 * 360 * @return Returns true if UserMap processing is enabled, false otherwise. 361 */ 362 protected boolean isUsingUserMap() { 363 return (this.tool != null && this.toolAccount != null); 364 } 365 366 /** 367 * Output a verbose message if verbose is enabled. 368 * 369 * @param msg The message to print if verbose mode is enabled. 370 */ 371 protected final void verboseInfo(String msg) { 372 if (this.verbose) { 373 System.out.println(msg); 374 } 375 } 376 377 /** 378 * Output an informational message from sensor. 379 * 380 * @param msg The message. 381 */ 382 protected final void info(String msg) { 383 System.out.println(msg); 384 } 385 386 /** 387 * Logs a final summary message regarding the number of entries sent and the elapsed time. 388 * 389 * @param startTime The start time of the sensor. 390 * @param sdt The type of data sent. 391 * @param numEntries The number of entries sent. 392 */ 393 protected void summaryInfo(Date startTime, String sdt, int numEntries) { 394 Date endTime = new Date(); 395 long elapsedTime = (endTime.getTime() - startTime.getTime()) / 1000; 396 info(numEntries + " " + sdt + " sensor data instances created."); 397 if (this.sensorShell.hasOfflineData()) { 398 info("Some or all of these instances were saved offline and not sent to the server."); 399 } 400 else { 401 info("These instances were transmitted to: " + this.sensorProps.getSensorBaseHost() + " (" 402 + elapsedTime + " secs.)"); 403 } 404 } 405 406 /** 407 * Sends any accumulated data in the SensorShell to the server and quits the shell. 408 */ 409 protected void sendAndQuit() { 410 try { 411 this.sensorShell.quit(); 412 } 413 catch (SensorShellException e) { 414 signalError("Problem during quit() of SensorShell", e); 415 } 416 } 417 418 /** 419 * Signals an error, which means throwing a BuildException if failOnError is true, or just logging 420 * the problem if it's not. 421 * 422 * @param msg The informative error message from the client. 423 * @param e The exception. 424 */ 425 protected void signalError(String msg, Exception e) { 426 String paddedMsg = msg + " "; 427 if (this.failOnError) { 428 throw new BuildException(errMsgPrefix + paddedMsg + e.getMessage(), e); 429 } 430 else { 431 System.out.println(errMsgPrefix + paddedMsg + e.getMessage()); 432 System.out.println(StackTrace.toString(e)); 433 System.out.println("Continuing execution since failOnError is false"); 434 } 435 } 436 437 }