001 package org.hackystat.sensorshell; 002 003 import java.io.BufferedReader; 004 import java.io.File; 005 import java.io.FileReader; 006 import java.io.IOException; 007 import java.io.InputStreamReader; 008 import java.text.SimpleDateFormat; 009 import java.util.ArrayList; 010 import java.util.Date; 011 import java.util.HashMap; 012 import java.util.Locale; 013 import java.util.Map; 014 import java.util.StringTokenizer; 015 import java.util.logging.FileHandler; 016 import java.util.logging.Formatter; 017 import java.util.logging.Logger; 018 019 import org.hackystat.sensorbase.client.SensorBaseClient; 020 import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData; 021 import org.hackystat.sensorshell.command.SensorDataCommand; 022 import org.hackystat.sensorshell.command.AutoSendCommand; 023 import org.hackystat.sensorshell.command.PingCommand; 024 import org.hackystat.sensorshell.command.QuitCommand; 025 import org.hackystat.utilities.home.HackystatUserHome; 026 import org.hackystat.utilities.logger.OneLineFormatter; 027 028 /** 029 * Provides the implementation of a single SensorShell instance. 030 * 031 * @author Philip M. Johnson 032 */ 033 public class SingleSensorShell implements Shell { 034 035 /** Indicates if SensorShell is running interactively and thus output should be printed. */ 036 private boolean isInteractive = false; 037 038 /** The notification shell prompt string. */ 039 private String prompt = ">> "; 040 041 /** A string indicating the tool name invoking SensorShell, added to the log file name. */ 042 private String toolName = "interactive"; 043 044 /** The delimiter for entry fields. */ 045 private String delimiter = "#"; 046 047 /** The line separator (carriage return character(s)). */ 048 private String cr = System.getProperty("line.separator"); 049 050 /** The input stream used to read input from the command line. */ 051 private BufferedReader bufferedReader = null; 052 053 /** The sensor properties instance. */ 054 private SensorShellProperties sensorProperties; 055 056 /** The logging instance for SensorShells. */ 057 private Logger logger; 058 059 /** The logging formatter that adds a timestamp but doesn't add a newline. */ 060 private Formatter oneLineFormatter = new OneLineFormatter(true, false); 061 062 /** The ping command. */ 063 private PingCommand pingCommand; 064 065 /** The send command. */ 066 private SensorDataCommand sensorDataCommand; 067 068 /** The quit command. */ 069 private QuitCommand quitCommand; 070 071 /** The OfflineManager used to recover data. */ 072 private OfflineManager offlineManager; 073 074 /** The startup time for this sensorshell. */ 075 private Date startTime = new Date(); 076 077 /** 078 * Constructs a new SensorShell instance that can be provided with 079 * notification data to be sent eventually to a specific user key and host. 080 * The toolName field in the log file name is set to "interactive" if the tool 081 * is invoked interactively and "tool" if it is invoked programmatically. 082 * 083 * @param properties The sensor properties instance for this run. 084 * @param isInteractive Whether this SensorShell is being interactively invoked or not. 085 */ 086 public SingleSensorShell(SensorShellProperties properties, boolean isInteractive) { 087 this(properties, isInteractive, (isInteractive) ? "interactive" : "tool", null); 088 } 089 090 091 /** 092 * Constructs a new SensorShell instance that can be provided with 093 * notification data to be sent eventually to a specific user key and host. 094 * 095 * @param properties The sensor properties instance for this run. 096 * @param isInteractive Whether this SensorShell is being interactively invoked or not. 097 * @param tool Indicates the invoking tool that is added to the log file name. 098 */ 099 public SingleSensorShell(SensorShellProperties properties, boolean isInteractive, String tool) { 100 this(properties, isInteractive, tool, null); 101 } 102 103 104 /** 105 * Constructs a new SensorShell instance that can be provided with 106 * notification data to be sent eventually to a specific user key and host. 107 * (For testing purposes only, you may want to disable offline data recovery.) 108 * 109 * @param properties The sensor properties instance for this run. 110 * @param isInteractive Whether this SensorShell is being interactively invoked or not. 111 * @param toolName The invoking tool that is added to the log file name. 112 * @param commandFile A file containing shell commands, or null if none provided. 113 */ 114 public SingleSensorShell(SensorShellProperties properties, boolean isInteractive, String toolName, 115 File commandFile) { 116 this.isInteractive = isInteractive; 117 this.toolName = toolName; 118 this.sensorProperties = properties; 119 boolean commandFilePresent = ((commandFile != null)); 120 SensorBaseClient client = new SensorBaseClient(properties.getSensorBaseHost(), 121 properties.getSensorBaseUser(), properties.getSensorBasePassword()); 122 client.setTimeout(properties.getTimeout() * 1000); 123 initializeLogger(); 124 printBanner(); 125 this.pingCommand = new PingCommand(this, properties); 126 this.sensorDataCommand = new SensorDataCommand(this, properties, this.pingCommand, client); 127 AutoSendCommand autoSendCommand = new AutoSendCommand(this, properties); 128 this.quitCommand = new QuitCommand(this, properties, sensorDataCommand, autoSendCommand); 129 130 // Now determine whether to read commands from the input stream or from the command file. 131 try { 132 this.bufferedReader = (commandFilePresent) ? 133 new BufferedReader(new FileReader(commandFile)) : 134 new BufferedReader(new InputStreamReader(System.in)); 135 } 136 catch (IOException e) { 137 this.logger.info(cr); 138 } 139 140 this.offlineManager = new OfflineManager(this, this.toolName); 141 142 // attempts to recover data if enabled; logs appropriate message in any case. 143 try { 144 recoverOfflineData(); 145 } 146 catch (SensorShellException e) { 147 this.logger.warning("Error recovering offline data."); 148 } 149 } 150 151 /** 152 * Returns the offline manager associated with this instance. 153 * @return The offline manager. 154 */ 155 public synchronized OfflineManager getOfflineManager() { 156 return this.offlineManager; 157 } 158 159 160 /** 161 * Looks for offline data and recovers any that is found if offline data 162 * management is enabled and if the server is currently pingable. 163 * @throws SensorShellException If problems occur recovering the data. 164 */ 165 private void recoverOfflineData() throws SensorShellException { 166 // Return immediately if server is not available. 167 boolean isOfflineRecoveryEnabled = this.sensorProperties.isOfflineRecoveryEnabled(); 168 // Return immediately if offline recovery is not enabled, but print this to the logger. 169 if (!isOfflineRecoveryEnabled) { 170 return; 171 } 172 173 boolean isPingable = this.pingCommand.isPingable(); 174 if (isPingable) { 175 this.println("Checking for offline data to recover."); 176 this.offlineManager.recover(); 177 } 178 else { 179 this.println("Not checking for offline data: Server not available."); 180 } 181 } 182 183 184 /** 185 * Initializes SensorShell logging. All client input is recorded to a log file 186 * in [user.home]/.hackystat/sensorshell/logs. Note that [user.home] is obtained 187 * from HackystatUserHome.getHome(). 188 */ 189 private void initializeLogger() { 190 try { 191 // First, create the logs directory. 192 File logDir = new File(HackystatUserHome.getHome(), ".hackystat/sensorshell/logs/"); 193 boolean dirOk = logDir.mkdirs(); 194 if (!dirOk && !logDir.exists()) { 195 throw new RuntimeException("mkdirs() failed"); 196 } 197 198 // Now set up logging to a file in that directory. 199 this.logger = Logger.getLogger("org.hackystat.sensorshell-" + this.toolName); 200 this.logger.setUseParentHandlers(false); 201 String fileName = logDir.getAbsolutePath() + "/" + this.toolName + ".%u.log"; 202 FileHandler handler = new FileHandler(fileName, 500000, 1, true); 203 handler.setFormatter(this.oneLineFormatter); 204 this.logger.addHandler(handler); 205 // Add a couple of newlines to the log file to distinguish new shell sessions. 206 logger.info(cr + cr); 207 // Now set the logging level based upon the SensorShell Property. 208 logger.setLevel(this.sensorProperties.getLoggingLevel()); 209 logger.getHandlers()[0].setLevel(this.sensorProperties.getLoggingLevel()); 210 } 211 catch (Exception e) { 212 System.out.println("Error initializing SensorShell logger:\n" + e); 213 } 214 } 215 216 217 /** 218 * Prints out initial information about the SensorShell. 219 */ 220 private void printBanner() { 221 this.println("Hackystat SensorShell Version: " + getVersion()); 222 this.println("SensorShell started at: " + this.startTime); 223 this.println(sensorProperties.toString()); 224 this.println("Type 'help' for a list of commands."); 225 // Ping the host to determine availability. 226 String host = sensorProperties.getSensorBaseHost(); 227 String email = sensorProperties.getSensorBaseUser(); 228 String password = sensorProperties.getSensorBasePassword(); 229 String availability = SensorBaseClient.isHost(host) ? "available." : "not available"; 230 this.println("Host: " + sensorProperties.getSensorBaseHost() + " is " + availability); 231 String authorized = SensorBaseClient.isRegistered(host, email, password) ? 232 " authorized " : " not authorized "; 233 this.println("User " + email + " is" + authorized + "to login at this host."); 234 this.println("Maximum Java heap size (bytes): " + Runtime.getRuntime().maxMemory()); 235 } 236 237 238 /** 239 * Process a single input string representing a command. 240 * @param inputString A command as a String. 241 * @throws SensorShellException If problems occur sending the data. 242 */ 243 void processInputString(String inputString) throws SensorShellException { 244 // Ignore empty commands. 245 if ((inputString == null) || ("".equals(inputString))) { 246 return; 247 } 248 // Log the command if we're not running interactively. 249 if (!this.isInteractive) { 250 logger.info("#> " + inputString); 251 } 252 // Process quit command. 253 if ("quit".equals(inputString)) { 254 this.quitCommand.quit(); 255 return; 256 } 257 258 // Process help command. 259 if ("help".equals(inputString)) { 260 this.printHelp(); 261 return; 262 } 263 264 // Process send command. 265 if ("send".equals(inputString)) { 266 this.sensorDataCommand.send(); 267 return; 268 } 269 270 // Process ping command. 271 if ("ping".equals(inputString)) { 272 boolean isPingable = this.pingCommand.isPingable(); 273 this.println("Ping of host " + this.sensorProperties.getSensorBaseHost() + " for user " + 274 this.sensorProperties.getSensorBaseUser() + 275 (isPingable ? " succeeded." : " did not succeed")); 276 return; 277 } 278 279 // Process commands with arguments. 280 StringTokenizer tokenizer = new StringTokenizer(inputString, this.delimiter); 281 int numTokens = tokenizer.countTokens(); 282 // All remaining commands must have arguments. 283 if (numTokens == 0) { 284 this.println("Error: unknown command or command requires arguments."); 285 return; 286 } 287 288 // Get the command name and any arguments. 289 String commandName = tokenizer.nextToken(); 290 ArrayList<String> argList = new ArrayList<String>(); 291 while (tokenizer.hasMoreElements()) { 292 argList.add(tokenizer.nextToken()); 293 } 294 295 if ("add".equals(commandName)) { 296 // For an Add command, the argument list should be a set of key-value pairs. 297 // So, build the Map of key-value pairs. 298 Map<String, String> keyValMap = new HashMap<String, String>(); 299 for (String arg : argList) { 300 int delim = arg.indexOf('='); 301 if (delim == -1) { 302 this.println("Error: can't parse argument string for add command."); 303 } 304 keyValMap.put(arg.substring(0, delim), arg.substring(delim + 1)); 305 } 306 try { 307 this.sensorDataCommand.add(keyValMap); 308 } 309 catch (Exception e) { 310 this.println("Error: Can't parse the Timestamp or Runtime arguments."); 311 } 312 return; 313 } 314 315 if ("statechange".equals(commandName)) { 316 String resourceCheckSumString = argList.get(0); 317 int resourceCheckSum = 0; 318 try { 319 resourceCheckSum = Integer.parseInt(resourceCheckSumString); 320 } 321 catch (Exception e) { 322 this.println("Error: Can't parse the checksum into an integer."); 323 return; 324 } 325 argList.remove(0); 326 // Now do almost the same as for an add command. 327 // Build the Map of key-value pairs. 328 Map<String, String> keyValMap = new HashMap<String, String>(); 329 for (String arg : argList) { 330 int delim = arg.indexOf('='); 331 if (delim == -1) { 332 this.println("Error: can't parse argument string for statechange command."); 333 } 334 keyValMap.put(arg.substring(0, delim), arg.substring(delim + 1)); 335 } 336 try { 337 this.sensorDataCommand.statechange(resourceCheckSum, keyValMap); 338 } 339 catch (Exception e) { 340 this.println("Error: Can't parse the Timestamp or Runtime arguments."); 341 } 342 return; 343 } 344 345 // Otherwise we don't understand. 346 this.println("Invalid command entered and ignored. Type 'help' for help."); 347 } 348 349 350 351 /** Prints the help strings associated with all commands. */ 352 private void printHelp() { 353 String helpString = 354 "SensorShell Command Summary " + cr 355 + " add#<key>=<value>[#<key>=<value>]..." + cr 356 + " Adds a new sensor data instance for subsequent sending." + cr 357 + " Provide fields and properties as key=value pairs separated by '#'." + cr 358 + " Owner, Timestamp, and Runtime fields will default to the current user and time." + cr 359 + " Example: add#Tool=Eclipse#SensorDataType=DevEvent#DevEventType=Compile" + cr 360 + " send" + cr 361 + " Sends any added sensor data to the server. " + cr 362 + " Server is pinged, and if it does not respond, then data is stored offline." + cr 363 + " Example: send" + cr 364 + " ping" + cr 365 + " Pings the server and checks email/password credentials." + cr 366 + " Example: ping" + cr 367 + " statechange#<ResourceCheckSum>#<key>=<value>[#<key>=<value>]..." + cr 368 + " Generates an 'add' command when the 'state' has changed." + cr 369 + " ResourceCheckSum is an integer that represents the current state of the Resource." + cr 370 + " This command compares ResourceCheckSum and the Resource field value to the values" + cr 371 + " saved from the last call to statechange. If either have changed, indicating that" + cr 372 + " the state has changed, then the key-value pairs are passed to the Add command." + cr 373 + " This command facilitates the implementation of timer-based sensor processes that" + cr 374 + " wake up periodically and emit statechange commands, with the knowledge that if " + cr 375 + " the user has not been active, these statechange commands will not result in" + cr 376 + " actual sensor data being sent to the server." + cr 377 + " quit" + cr 378 + " Sends any remaining data and exits the sensorshell." + cr 379 + " Example: quit" + cr; 380 this.print(helpString); 381 } 382 383 /** Print out a prompt if in interactive mode. */ 384 void printPrompt() { 385 this.print(this.prompt); 386 } 387 388 /** 389 * Returns a string with the next line of input from the user. If input 390 * errors, returns the string "quit". 391 * 392 * @return A string with user input. 393 */ 394 String readLine() { 395 try { 396 String line = this.bufferedReader.readLine(); 397 logger.info(line); 398 return (line == null) ? "" : line; 399 } 400 catch (IOException e) { 401 //logger.info(cr); 402 return "quit"; 403 } 404 } 405 406 /** 407 * Prints out the line plus newline if in interactive mode, and always logs the line. 408 * Provided to clients to support logging of error messages. 409 * 410 * @param line The line to be printed. 411 */ 412 public final synchronized void println(String line) { 413 logger.info(line + cr); 414 if (isInteractive) { 415 SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.US); 416 System.out.print(dateFormat.format(new Date()) + " " + line + cr); 417 } 418 } 419 420 421 /** 422 * Prints out the line without newline if in interactive mode. 423 * 424 * @param line The line to be printed. 425 */ 426 public final synchronized void print(String line) { 427 logger.info(line); 428 if (isInteractive) { 429 SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.US); 430 System.out.print(dateFormat.format(new Date()) + " " + line); 431 } 432 } 433 434 /** 435 * Returns a Date instance indicating when this SensorShell was started. 436 * @return The Date when this instance started up. 437 */ 438 public synchronized Date getStartTime() { 439 return new Date(this.startTime.getTime()); 440 } 441 442 /** 443 * Returns true if this shell has stored any data offline. 444 * @return True if any data has been stored offline. 445 */ 446 public synchronized boolean hasOfflineData() { 447 return this.offlineManager.hasOfflineData(); 448 } 449 450 /** 451 * Returns the total number of instances sent by this shell's SensorDataCommand. 452 * @return The total number of instances sent. 453 */ 454 public synchronized long getTotalSent() { 455 return this.sensorDataCommand.getTotalSent(); 456 } 457 458 /** 459 * Return the current version number. 460 * 461 * @return The version number, or "Unknown" if could not be determined. 462 */ 463 private String getVersion() { 464 String release; 465 try { 466 Package thisPackage = Class.forName("org.hackystat.sensorshell.SensorShell").getPackage(); 467 release = thisPackage.getImplementationVersion(); 468 } 469 catch (Exception e) { 470 release = "Unknown"; 471 } 472 return release; 473 } 474 475 /** 476 * Returns true if this sensorshell is being run interactively from the command line. 477 * @return True if sensorshell is interactive. 478 */ 479 public synchronized boolean isInteractive() { 480 return this.isInteractive; 481 } 482 483 /** 484 * Returns the Logger associated with this sensorshell. 485 * @return The Logger. 486 */ 487 public synchronized Logger getLogger() { 488 return this.logger; 489 } 490 491 492 /** {@inheritDoc} */ 493 public synchronized void add(Map<String, String> keyValMap) throws SensorShellException { 494 this.sensorDataCommand.add(keyValMap); 495 } 496 497 /** {@inheritDoc} */ 498 public synchronized void add(SensorData sensorData) throws SensorShellException { 499 this.sensorDataCommand.add(sensorData); 500 } 501 502 /** {@inheritDoc} */ 503 public synchronized int send() throws SensorShellException { 504 return this.sensorDataCommand.send(); 505 } 506 507 /** {@inheritDoc} */ 508 public synchronized void quit() throws SensorShellException { 509 this.quitCommand.quit(); 510 } 511 512 /** {@inheritDoc} */ 513 public synchronized boolean ping() { 514 return this.pingCommand.isPingable(); 515 } 516 517 /** {@inheritDoc} */ 518 public synchronized void statechange(long resourceCheckSum, Map<String, String> keyValMap) 519 throws Exception { 520 this.sensorDataCommand.statechange(resourceCheckSum, keyValMap); 521 } 522 523 /** {@inheritDoc} */ 524 public synchronized SensorShellProperties getProperties() { 525 return this.sensorProperties; 526 } 527 } 528