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    }