001    package org.hackystat.sensor.ant.antbuild;
002    
003    import java.util.ArrayList;
004    import java.util.EmptyStackException;
005    import java.util.Hashtable;
006    import java.util.List;
007    import java.util.Map;
008    import java.util.Stack;
009    import java.util.TreeMap;
010    
011    import javax.xml.datatype.XMLGregorianCalendar;
012    
013    import org.apache.tools.ant.BuildEvent;
014    import org.apache.tools.ant.BuildException;
015    import org.apache.tools.ant.BuildListener;
016    import org.apache.tools.ant.Task;
017    import org.hackystat.sensor.ant.util.LongTimeConverter;
018    import org.hackystat.sensorshell.SensorShell;
019    import org.hackystat.sensorshell.SensorShellException;
020    import org.hackystat.sensorshell.SensorShellProperties;
021    import org.hackystat.sensorshell.usermap.SensorShellMap;
022    import org.hackystat.sensorshell.usermap.SensorShellMapException;
023    
024    /**
025     * Ant build sensor. It's implemented as an ant listener.  Note that we are unable to access the
026     * Ant properties directly. Because of this almost all the Ant methods have a check to see if
027     * properties have been retrieved by this sensor. The order of the methods depends on the build
028     * so almost all methods need to do this check.
029     * 
030     * @author (Cedric) Qin Zhang, Julie Ann Sakuda
031     */
032    public class BuildSensorAntListener implements BuildListener {
033      /** Flag indicating if the sensor properties have already been retrieved. */
034      private boolean propertiesSet = false;
035    
036      private boolean debug = false;
037    
038      private String tool;
039      private String toolAccount;
040      
041      private String buildType;
042    
043      private SensorShell shell = null;
044    
045      /** Stack of task names. */
046      private Stack<String> taskNameStack = new Stack<String>();
047    
048      // the following two stacks are always synchronized.
049      /** List of messages from single tasks. */
050      private Stack<List<String>> messagesStack = new Stack<List<String>>();
051      /** Stack of target names. */
052      private List<String> targetNameStack = new ArrayList<String>();
053    
054      /** Build start time. */
055      private long startTimeMillis;
056      /** The last Ant target executed. */
057      private String lastTargetName = "Unknown";
058      /** The string that prefixes all error messages from this sensor. */
059      private String errMsgPrefix = "Hackystat Build Sensor Error: ";
060    
061      /**
062       * Constructs an instance of the build sensor listener. This constructor allows the build sensor
063       * to be installed using the -listener ant argument. Unfortunately, using this approach we lose
064       * the ability to pass in constructor arguments. This constructor must be public with no
065       * parameters.
066       */
067      public BuildSensorAntListener() {
068        // can't do anything here because we don't have the sensor properties yet
069      }
070    
071      /**
072       * Prints out debug message.
073       * 
074       * @param message The debug message.
075       */
076      private void logDebugMessage(String message) {
077        if (this.debug) {
078          System.out.println("[Hackystat Build Sensor] " + message);
079        }
080      }
081    
082      /**
083       * Callback function when ant starts the build. Note that the Ant properties at this point do
084       * not have any properties specified with '-D'. Do not try to get the property values here.
085       * 
086       * @param buildEvent The build event object.
087       */
088      public void buildStarted(BuildEvent buildEvent) {
089        this.startTimeMillis = System.currentTimeMillis();
090      }
091    
092      /**
093       * Callback function when ant finishes the build. It sends out the build sensor data if build
094       * sensor is enabled.
095       * 
096       * @param buildEvent The build event object.
097       */
098      public void buildFinished(BuildEvent buildEvent) {
099        if (!propertiesSet) {
100          this.setProperties(buildEvent);
101        }
102    
103        // set up/initialize the sensor shell to use
104        this.setUpSensorShell();
105    
106        long endTimeMillis = System.currentTimeMillis();
107        String fileSep = System.getProperty("file.separator");
108        String workingDirectory = buildEvent.getProject().getBaseDir().getAbsolutePath() + fileSep;
109    
110        Map<String, String> keyValMap = new TreeMap<String, String>();
111        keyValMap.put("Tool", "Ant");
112        XMLGregorianCalendar startTime = LongTimeConverter.convertLongToGregorian(this.startTimeMillis);
113        keyValMap.put("Timestamp", startTime.toString());
114        keyValMap.put("Resource", workingDirectory);
115        keyValMap.put("SensorDataType", "Build");
116        keyValMap.put("Target", this.lastTargetName);
117    
118        // put result in the map
119        if (buildEvent.getException() == null) {
120          keyValMap.put("Result", "Success");
121        }
122        else {
123          keyValMap.put("Result", "Failure");
124        }
125    
126        // optional
127        XMLGregorianCalendar endTime = LongTimeConverter.convertLongToGregorian(endTimeMillis);
128        keyValMap.put("EndTime", endTime.toString());
129        
130        if (this.buildType != null) {
131          keyValMap.put("Type", this.buildType);
132        }
133    
134        System.out.print("Sending build result to Hackystat server... ");
135        try {
136          this.shell.add(keyValMap);
137        }
138        catch (Exception e) {
139          throw new BuildException(errMsgPrefix + "Error adding data to SensorShell.", e);
140        }
141        try {
142          this.shell.send();
143        }
144        catch (SensorShellException e) {
145          throw new BuildException("errMsgPrefix + Error sensor data.", e);
146        }
147        System.out.println();
148      }
149    
150      /**
151       * Callback function when ant starts a build target. It's used to record last ant target invoked.
152       * 
153       * @param buildEvent The build event object.
154       */
155      public void targetStarted(BuildEvent buildEvent) {
156        String targetName = buildEvent.getTarget().getName();
157        this.targetNameStack.add(targetName);
158        this.logDebugMessage("TargetStarted - " + targetName);
159      }
160    
161      /**
162       * Callback function when ant finishes a build target.
163       * 
164       * @param buildEvent The build event object.
165       */
166      public void targetFinished(BuildEvent buildEvent) {
167        if (!propertiesSet) {
168          this.setProperties(buildEvent);
169        }
170        
171        String targetName = buildEvent.getTarget().getName();
172        this.logDebugMessage("TargetFinished - " + targetName);
173    
174        int size = this.targetNameStack.size();
175        if (size > 0) {
176          this.targetNameStack.remove(size - 1);
177        }
178    
179        // TODO: This scheme to get top level build target works ok when build is successful
180        // But if build failed, you only get the last target invoked by ANT.
181        // This is a possible problem to handle in the next release.
182        if (targetName != null) {
183          this.lastTargetName = targetName;
184        }
185      }
186    
187      /**
188       * Callback function when ant starts a build task.
189       * 
190       * @param buildEvent The build event object.
191       */
192      public void taskStarted(BuildEvent buildEvent) {
193        if (!propertiesSet) {
194          this.setProperties(buildEvent);
195        }
196        
197        Task task = buildEvent.getTask();
198        String taskName = task.getTaskName();
199        this.logDebugMessage("TaskStarted - " + taskName);
200        this.taskNameStack.push(taskName);
201        this.messagesStack.push(new ArrayList<String>());
202      }
203    
204      /**
205       * Callback function when ant finishes a build task.
206       * 
207       * @param buildEvent The build event object.
208       */
209      public void taskFinished(BuildEvent buildEvent) {
210        if (!propertiesSet) {
211          this.setProperties(buildEvent);
212        }
213        
214        String taskName = buildEvent.getTask().getTaskName();
215        this.logDebugMessage("TaskFinished - " + taskName + ";  error = "
216            + (buildEvent.getException() != null));
217    
218        // when you install the listener in a task, you will never be able to get that TaskStart event.
219        // The first event you hear is TaskFinished, this is to handle this special case.
220        if (this.taskNameStack.isEmpty()) {
221          return;
222        }
223    
224        this.taskNameStack.pop();
225      }
226    
227      /**
228       * Callback function when ant logs a message.
229       * 
230       * @param buildEvent The build event object.
231       */
232      public void messageLogged(BuildEvent buildEvent) {
233        try {
234          Task task = buildEvent.getTask();
235          if (task != null && !this.taskNameStack.isEmpty()
236              && task.getTaskName().equals(this.taskNameStack.peek())) {
237            String message = buildEvent.getMessage();
238            if (message != null) {
239              List<String> list = this.messagesStack.peek();
240              list.add(message);
241            }
242          }
243        }
244        catch (EmptyStackException ex) {
245          // This shouldn't actually happen
246          System.out.println("Error: internal stack structure has nothing to peek at.");
247        }
248      }
249    
250      /**
251       * Gets whether or not this sensor instance is using a mapping in the UserMap.
252       * 
253       * @return Returns true of the tool and tool account are set, otherwise false.
254       */
255      private boolean isUsingUserMap() {
256        return (this.tool != null && this.toolAccount != null);
257      }
258      
259      /** Sets up the sensorshell instance that should be used. */
260      private void setUpSensorShell() {
261        if (isUsingUserMap()) {
262          try {
263            // get shell from SensorShellMap/UserMap when user supplies tool and toolAccount.
264            SensorShellMap map = new SensorShellMap(this.tool);
265            this.shell = map.getUserShell(this.toolAccount);
266          }
267          catch (SensorShellMapException e) {
268            throw new BuildException(errMsgPrefix + "Could not create SensorShellMap", e);
269          }
270        }
271        // User did not supply tool/toolAccount, so do a normal default instantiation of SensorShell.
272        else {
273          try {
274            this.shell = new SensorShell(new SensorShellProperties(), false, "Ant");
275          }
276          catch (SensorShellException e) {
277            throw new BuildException(errMsgPrefix + "Unable to initialize sensor properties.", e);
278          }
279        }
280      }
281      
282      /**
283       * Sets the Hackystat properties using the properties map from Ant.
284       * 
285       * @param buildEvent Build event to use to retrieve sensor properties.
286       */
287      @SuppressWarnings("unchecked")
288      private void setProperties(BuildEvent buildEvent) {
289        Hashtable properties = buildEvent.getProject().getProperties();
290    
291        Object debugObject = properties.get("hackystat.ant.debug");
292        if (debugObject != null) {
293          this.debug = Boolean.valueOf((String) debugObject);
294        }
295    
296        Object toolObject = properties.get("hackystat.ant.tool");
297        if (toolObject != null) {
298          this.tool = (String) toolObject;
299        }
300    
301        Object toolAccountObject = properties.get("hackystat.ant.toolAccount");
302        if (toolAccountObject != null) {
303          this.toolAccount = (String) toolAccountObject;
304        }
305        
306        Object typeObject = properties.get("hackystat.ant.build.type");
307        if (typeObject != null) {
308          this.buildType = (String) typeObject;
309        }
310    
311        this.propertiesSet = true;
312      }
313    }