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 }