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