001    package org.hackystat.sensorshell;
002    
003    import java.io.File;
004    import java.io.FileInputStream;
005    import java.util.Properties;
006    import java.util.TreeMap;
007    import java.util.Map.Entry;
008    import java.util.logging.Level;
009    import java.util.logging.Logger;
010    
011    import org.hackystat.sensorbase.client.SensorBaseClient;
012    import org.hackystat.utilities.logger.HackystatLogger;
013    import org.hackystat.utilities.home.HackystatUserHome;
014    
015    /**
016     * Provides Hackystat sensors with access to standard Hackystat sensorshell properties.  These 
017     * properties are generally stored in ~/.hackystat/sensorshell/sensorshell.properties.
018     * This class manages access to those files for sensors, 
019     * performs type conversions on the String-based properties when appropriate, and sets default
020     * values when the properties are missing or incorrectly specified.
021     * <p>
022     * See the public static final Strings for descriptions of the "standard" Hackystat sensorshell
023     * properties.
024     * 
025     * @author Philip M. Johnson, Aaron A. Kagawa
026     */
027    public class SensorShellProperties {
028      
029      /**
030       * The property key retrieving an URL indicating the location of the SensorBase host. 
031       * This a required property; if not supplied, instantiation will fail.
032       * Example: "http://dasha.ics.hawaii.edu:9876/sensorbase".
033       * No default value. 
034       */
035      public static final String SENSORSHELL_SENSORBASE_HOST_KEY = "sensorshell.sensorbase.host";
036      
037      /**
038       * The property key retrieving the user account associated with the SensorBase.
039       * This a required property; if not supplied, instantiation will fail.
040       * Example: "johnson@hawaii.edu".
041       * No default value.
042       */
043      public static final String SENSORSHELL_SENSORBASE_USER_KEY = "sensorshell.sensorbase.user";
044      
045      /**
046       * The property key retrieving the password associated with the user.
047       * This a required property; if not supplied, instantiation will fail.
048       * Example: "xykdclwck".
049       * No default value.
050       */
051      public static final String SENSORSHELL_SENSORBASE_PASSWORD_KEY = 
052        "sensorshell.sensorbase.password";
053      
054      /**
055       * The property key retrieving the timeout value (in seconds) for SensorShell HTTP requests.
056       * Default: "10".
057       */
058      public static final String SENSORSHELL_TIMEOUT_KEY = "sensorshell.timeout";
059      
060      /**
061       * The property key retrieving the timeout value (in seconds) for SensorShell 'PING' requests.
062       * Default: "2".
063       */
064      public static final String SENSORSHELL_PING_TIMEOUT_KEY = "sensorshell.timeout.ping";
065      
066      /**
067       * The property key retrieving a boolean indicating whether the MultiSensorShell is enabled.
068       * If true, offline recovery and storage will be automatically disabled.
069       * Default: "false".
070       */
071      public static final String SENSORSHELL_MULTISHELL_ENABLED_KEY = "sensorshell.multishell.enabled";
072      
073      
074      /**
075       * The property key retrieving an integer indicating the number of shells to instantiate when 
076       * the MultiSensorShell is enabled.
077       * Default: "10".
078       */
079      public static final String SENSORSHELL_MULTISHELL_NUMSHELLS_KEY = 
080        "sensorshell.multishell.numshells";
081      
082      /**
083       * The property key retrieving an integer indicating the number of instances to send to a single
084       * shell in the MultiSensorShell at once. We make this one less than the default multishell 
085       * maxbuffer value so that the non-blocking autosend has chance to run rather than the 
086       * blocking send() resulting from hitting the maxbuffer size. 
087       * Default: "499".
088       */
089      public static final String SENSORSHELL_MULTISHELL_BATCHSIZE_KEY = 
090        "sensorshell.multishell.batchsize";
091      
092      /**
093       * The property key retrieving an integer indicating the maximum number of instances to buffer
094       * in each shell in the MultiSensorShell before a blocking send() is invoked.
095       * Default: "500".
096       */
097      public static final String SENSORSHELL_MULTISHELL_MAXBUFFER_KEY = 
098        "sensorshell.multishell.maxbuffer";
099      
100      /**
101       * The property key retrieving a double indicating how many minutes between autosends of 
102       * sensor data for each shell in a MultiShell.  If "0.0", then sensor data is not sent unless the
103       * client invokes the send() method explicitly. This value is typically around 0.05 to 0.10 
104       * (i.e. 3 to 6 seconds). 
105       * Default: "0.05".
106       */
107      public static final String SENSORSHELL_MULTISHELL_AUTOSEND_TIMEINTERVAL_KEY = 
108        "sensorshell.multishell.autosend.timeinterval";
109      
110      /**
111       * The property key retrieving a boolean indicating if data will be cached locally if the 
112       * SensorBase cannot be contacted. 
113       * Default: "true" if multi-shell is disabled. If multishell is enabled, then set to false. 
114       */
115      public static final String SENSORSHELL_OFFLINE_CACHE_ENABLED_KEY = 
116        "sensorshell.offline.cache.enabled";
117      
118      /**
119       * The property key retrieving a boolean indicating if offline data will be recovered at startup
120       * if the SensorBase can be contacted.
121       * Default: "true" if multi-shell is disabled. If multishell is enabled, then set to false. 
122       */
123      public static final String SENSORSHELL_OFFLINE_RECOVERY_ENABLED_KEY = 
124        "sensorshell.offline.recovery.enabled";
125      
126      /**
127       * The property key retrieving an integer indicating the number of seconds between "wakeups" of
128       * tool subprocesses that check for state changes. Typically used by editors such as Emacs or
129       * Eclipse to provide a standard measure of developer activity. 
130       * Default: "30".
131       */
132      public static final String SENSORSHELL_STATECHANGE_INTERVAL_KEY = 
133        "sensorshell.statechange.interval";
134      
135      /**
136       * The property key retrieving a double indicating how many minutes between autosends of 
137       * sensor data when not in MultiShell mode.  If "0.0", then sensor data is not sent unless the 
138       * client invokes the send() method explicitly. This value typically varies 
139       * from 1.0 to 10.0 (i.e. 1 to 10 minutes).  
140       * Default: "1.0".
141       */
142      public static final String SENSORSHELL_AUTOSEND_TIMEINTERVAL_KEY = 
143        "sensorshell.autosend.timeinterval";
144      
145      /**
146       * The property key retrieving an integer indicating the maximum number of sensor instances to 
147       * buffer locally between autosends of sensor data.  If "0", then no maximum size is defined.
148       * Default: "250". Note that this is the value used when multishell is not enabled, otherwise
149       * the value associated with SENSORSHELL_MULTISHELL_MAXBUFFER_KEY is used.
150       */
151      public static final String SENSORSHELL_AUTOSEND_MAXBUFFER_KEY = 
152        "sensorshell.autosend.maxbuffer";
153      
154      /**
155       * The property key retrieving a string indicating the logging level for the SensorShell(s).
156       * Default: "INFO".
157       */
158      public static final String SENSORSHELL_LOGGING_LEVEL_KEY = "sensorshell.logging.level";
159     
160      /** The internal properties object. */
161      private Properties sensorProps = new Properties();
162      
163      private File sensorShellPropertiesFile = new File(HackystatUserHome.getHome(), 
164          ".hackystat/sensorshell/sensorshell.properties");
165      
166      private Logger logger = HackystatLogger.getLogger("org.hackystat.sensorshell.properties", 
167          "sensorshell");
168    
169      /** The default timeout in seconds. */
170      private int timeout = 10;
171      /** The default ping timeout in seconds. */
172      private int pingTimeout = 2;
173      /** MultiShell processing is disabled by default. */
174      private boolean multiShellEnabled = false;
175      /** If MultiShell processing is enabled, then the default number of shells is 10. */
176      private int multiShellNumShells = 10;
177      /** If MultiShell processing is enabled, then the default num instances in a row is 499. */
178      private int multiShellBatchSize = 499;
179      /** If MultiShell processing is enabled, then the default max buffer is 500. */
180      private int multiShellMaxBuffer = 500;
181      /** If MultiShell processing is enabled, then the default autosend time interval is 0.10 . */
182      private double multiShellAutoSendTimeInterval = 0.05;
183      /** Offline caching of data is enabled by default. */
184      private boolean offlineCacheEnabled = true;
185      /** Recovery of offline data upon initialization is enabled by default. */
186      private boolean offlineRecoveryEnabled = true;
187      /** The default state change interval is 30 seconds. */
188      private int statechangeInterval = 30;
189      /** The default autosend time interval is 1.0 minutes. */
190      private double autosendTimeInterval = 1.0;
191      /** The default maximum number of buffered instances is 250. */
192      private int autosendMaxBuffer = 250;
193      /** Holds the required sensorbase host. */
194      private String sensorBaseHost = null;
195      /** Holds the required user. */
196      private String user = null;
197      /** Holds the required password. */
198      private String password = null;
199      /** Holds the default logging level. */
200      private Level loggingLevel = Level.INFO;
201      
202      /**
203       * Initializes SensorShell properties using the default sensorshell.properties file.
204       * It could be located in user.home, or hackystat.user.home (if the user has set the 
205       * latter in the System properties before invoking this constructor.)
206       * @throws SensorShellException If the SensorProperties instance cannot be 
207       * instantiated due to a missing host, user, and/or password properties. 
208       */
209      public SensorShellProperties() throws SensorShellException {
210        this(new File(HackystatUserHome.getHome(), 
211        ".hackystat/sensorshell/sensorshell.properties"));
212      }
213    
214      /**
215       * Creates a SensorShellProperties instance using the specified properties file. 
216       * All unspecified properties are set to their built-in default values. 
217       * @param sensorFile The sensorshell properties file to read.
218       * @throws SensorShellException If the SensorProperties instance cannot be 
219       * instantiated due to a missing host, user, and/or password properties.
220       */
221      public SensorShellProperties(File sensorFile) throws SensorShellException {
222        this.sensorShellPropertiesFile = sensorFile;
223        setDefaultSensorShellProperties(true);
224        FileInputStream fileStream = null;
225        try {
226          fileStream = new FileInputStream(this.sensorShellPropertiesFile);
227          this.sensorProps.load(fileStream);
228          validateProperties();
229        } 
230        catch (Exception e) {
231          String errMsg = "SensorShellProperties error loading: " + sensorFile;
232          this.logger.warning(errMsg);
233          throw new SensorShellException(errMsg, e);
234        }
235        finally {
236          try {
237            if (fileStream != null) {
238              fileStream.close();
239            }
240          }
241          catch (Exception e) { //NOPMD
242            // Don't say anything. 
243          }
244        }
245      }
246    
247      /**
248       * Constructs a "basic" instance with the supplied three required properties.
249       * All other properties are assigned values from sensorshell.properties, or the 
250       * built-in defaults if not specified there.
251       * <p>
252       * Use SensorShellProperties.getTestInstance to create an instance for testing purposes, since
253       * it will override certain properties that may be present in the sensorshell.properties file.
254       * @param host The hackystat host.
255       * @param email The user's email.
256       * @param password The user's password.
257       * @throws SensorShellException If the SensorProperties instance cannot be 
258       * instantiated due to a missing host, user, and/or password properties.
259       */
260      public SensorShellProperties(String host, String email, String password) 
261      throws SensorShellException {
262        this.setPropertiesFromFile(sensorShellPropertiesFile);
263        setDefaultSensorShellProperties(false);
264        this.sensorProps.setProperty(SENSORSHELL_SENSORBASE_HOST_KEY, host);
265        this.sensorProps.setProperty(SENSORSHELL_SENSORBASE_USER_KEY, email);
266        this.sensorProps.setProperty(SENSORSHELL_SENSORBASE_PASSWORD_KEY, password);
267        validateProperties();
268      }
269      
270      /**
271       * Constructs an instance with the supplied three required properties and any other
272       * properties provided in the properties argument.
273       * Any remaining properties are assigned values from sensorshell.properties, or the 
274       * built-in defaults if not specified there. 
275       * @param host The hackystat host.
276       * @param email The user's email.
277       * @param password The user's password.
278       * @param properties A properties instance with other properties.
279       * @param overrideFile If true, then the passed properties override sensorshell.properties. 
280       * @throws SensorShellException If the SensorProperties instance cannot be 
281       * instantiated due to a missing host, user, and/or password properties.
282       */
283      public SensorShellProperties(String host, String email, String password, Properties properties, 
284          boolean overrideFile) throws SensorShellException {
285        if (overrideFile) {
286          this.setPropertiesFromFile(sensorShellPropertiesFile);
287          this.sensorProps.putAll(properties);
288          this.setDefaultSensorShellProperties(false);
289        }
290        else {
291          this.sensorProps.putAll(properties);
292          this.setPropertiesFromFile(sensorShellPropertiesFile);
293          this.setDefaultSensorShellProperties(false);
294        }
295        this.sensorProps.setProperty(SENSORSHELL_SENSORBASE_HOST_KEY, host);
296        this.sensorProps.setProperty(SENSORSHELL_SENSORBASE_USER_KEY, email);
297        this.sensorProps.setProperty(SENSORSHELL_SENSORBASE_PASSWORD_KEY, password);
298        validateProperties();
299      }
300     
301      /**
302       * Creates and returns a new SensorShellProperties instance which is initialized to the contents
303       * of the passed SensorProperties instance, with additional new properties overriding the previous
304       * selection.
305       * @param orig The original properties.
306       * @param newProps The replacing properties.
307       * @throws SensorShellException If the SensorProperties instance cannot be instantiated due to 
308       * invalid or missing properties.  
309       */
310      public SensorShellProperties(SensorShellProperties orig, Properties newProps) 
311      throws SensorShellException {
312        this.sensorProps = orig.sensorProps;
313        this.sensorShellPropertiesFile = orig.sensorShellPropertiesFile;
314        this.sensorProps.putAll(newProps);
315        validateProperties();
316      }
317      
318      
319      /**
320       * Constructs a "test" instance with the supplied three required properties.
321       * <p>
322       * This testing-only factory class sets the three required properties, and overrides the 
323       * following properties for testing purposes:
324       * <ul>
325       * <li> Disables offline recovery and caching.
326       * <li> Disables logging. 
327       * <li> Disables multishell.
328       * </ul>
329       * All remaining properties are set to their built-in default values. 
330       * @param host The hackystat host.
331       * @param email The user's email.
332       * @param password The user's password.
333       * @return the new SensorShellProperties instance. 
334       * @throws SensorShellException If the SensorProperties instance cannot be 
335       * instantiated due to a missing host, user, and/or password properties.
336       */
337      public static SensorShellProperties getTestInstance(String host, String email, String password) 
338      throws SensorShellException {
339        String falseStr = "false";
340        Properties props = new Properties();
341        props.setProperty(SENSORSHELL_AUTOSEND_MAXBUFFER_KEY, "250");
342        props.setProperty(SENSORSHELL_AUTOSEND_TIMEINTERVAL_KEY, "1.0");
343        props.setProperty(SENSORSHELL_LOGGING_LEVEL_KEY, "OFF");
344        props.setProperty(SENSORSHELL_MULTISHELL_AUTOSEND_TIMEINTERVAL_KEY, "0.05");
345        props.setProperty(SENSORSHELL_MULTISHELL_BATCHSIZE_KEY, "499");
346        props.setProperty(SENSORSHELL_MULTISHELL_ENABLED_KEY, falseStr);
347        props.setProperty(SENSORSHELL_MULTISHELL_MAXBUFFER_KEY, "500");
348        props.setProperty(SENSORSHELL_MULTISHELL_NUMSHELLS_KEY, "10");
349        props.setProperty(SENSORSHELL_OFFLINE_CACHE_ENABLED_KEY, falseStr);
350        props.setProperty(SENSORSHELL_OFFLINE_RECOVERY_ENABLED_KEY, falseStr);
351        props.setProperty(SENSORSHELL_SENSORBASE_HOST_KEY, host);
352        props.setProperty(SENSORSHELL_SENSORBASE_PASSWORD_KEY, password);
353        props.setProperty(SENSORSHELL_SENSORBASE_USER_KEY, email);
354        props.setProperty(SENSORSHELL_STATECHANGE_INTERVAL_KEY, "30");
355        props.setProperty(SENSORSHELL_TIMEOUT_KEY, "10");
356        props.setProperty(SENSORSHELL_PING_TIMEOUT_KEY, "5");
357        return new SensorShellProperties(props, true);
358      }
359      
360      /**
361       * Creates a SensorShell properties instance, initializing it using the passed properties as 
362       * well as any settings found in the sensorshell.properties file. 
363       * If overrideFile is true, then the passed properties and settings will override matching 
364       * properties and settings found in the sensorshell.properties file.  If false, then the 
365       * property settings found in the sensorshell.properties will override the passed properties.
366       * <p>
367       * This constructor enables the user to provide default settings in sensorshell.properties for 
368       * things like MultiSensorShell, but enable a tool to provide command line args that could
369       * override these defaults. The tool could do this by processing its command line args, then
370       * creating a Properties instance containing the overridding values, then passing that in to 
371       * this constructor.
372       * <p>
373       * If overrideFile is false, then these properties will only take effect if the user has not
374       * specified them in their sensorshell.properties file. 
375       * <p>
376       * Standard properties not specified by either the sensorshell.properties file or the passed 
377       * properties instance will be given default values.
378       *  
379       * @param properties The properties. 
380       * @param overrideFile If true, then the passed properties will override any matching 
381       * sensorshell.properties properties.
382       * @throws SensorShellException if problems occur.
383       */
384      public SensorShellProperties(Properties properties, boolean overrideFile) 
385      throws SensorShellException {
386        if (overrideFile) {
387          this.setPropertiesFromFile(sensorShellPropertiesFile);
388          this.sensorProps.putAll(properties);
389          this.setDefaultSensorShellProperties(false);
390        }
391        else {
392          this.sensorProps.putAll(properties);
393          this.setPropertiesFromFile(sensorShellPropertiesFile);
394          this.setDefaultSensorShellProperties(false);
395        }
396        validateProperties();
397      }
398      
399      
400      /**
401       * Sets the internal sensorshell properties instance with the contents of file. If the file
402       * cannot be found, then the sensor properties instance is unchanged. 
403       * @param file The file to be processed.
404       */
405      private void setPropertiesFromFile(File file) {
406        FileInputStream fileStream = null; 
407        try {
408          fileStream = new FileInputStream(file);
409          this.sensorProps.load(fileStream);
410        } 
411        catch (Exception e) { //NOPMD
412        }
413        finally {
414          try {
415            if (fileStream != null) {
416              fileStream.close();
417            }
418          }
419          catch (Exception e) {
420            System.err.println("Error closing stream: " + e);
421          }
422        }
423      }
424      
425      
426      /**
427       * Ensures that the SensorProperties instance has values defined for all standard 
428       * properties with default values. Can optionally override existing values for these
429       * standard properties.  Does not check for or provide values for the three required properties.
430       * @param overridePreexisting If the default values should override the preexisting values. 
431       */
432      private void setDefaultSensorShellProperties (boolean overridePreexisting) {
433        setDefaultProperty(SENSORSHELL_TIMEOUT_KEY, 
434            String.valueOf(timeout), overridePreexisting);
435        setDefaultProperty(SENSORSHELL_PING_TIMEOUT_KEY, 
436            String.valueOf(pingTimeout), overridePreexisting);
437        setDefaultProperty(SENSORSHELL_MULTISHELL_ENABLED_KEY, 
438            String.valueOf(multiShellEnabled), overridePreexisting);
439        setDefaultProperty(SENSORSHELL_MULTISHELL_NUMSHELLS_KEY, 
440            String.valueOf(multiShellNumShells), overridePreexisting);
441        setDefaultProperty(SENSORSHELL_MULTISHELL_BATCHSIZE_KEY, 
442            String.valueOf(multiShellBatchSize), overridePreexisting);
443        setDefaultProperty(SENSORSHELL_MULTISHELL_MAXBUFFER_KEY, 
444            String.valueOf(multiShellMaxBuffer), overridePreexisting);
445        setDefaultProperty(SENSORSHELL_MULTISHELL_AUTOSEND_TIMEINTERVAL_KEY, 
446            String.valueOf(multiShellAutoSendTimeInterval), overridePreexisting);
447        setDefaultProperty(SENSORSHELL_OFFLINE_CACHE_ENABLED_KEY, 
448            String.valueOf(offlineCacheEnabled), overridePreexisting);
449        setDefaultProperty(SENSORSHELL_OFFLINE_RECOVERY_ENABLED_KEY, 
450            String.valueOf(offlineRecoveryEnabled), overridePreexisting);
451        setDefaultProperty(SENSORSHELL_STATECHANGE_INTERVAL_KEY, 
452            String.valueOf(statechangeInterval), overridePreexisting);
453        setDefaultProperty(SENSORSHELL_AUTOSEND_TIMEINTERVAL_KEY, 
454            String.valueOf(autosendTimeInterval), overridePreexisting);
455        setDefaultProperty(SENSORSHELL_AUTOSEND_MAXBUFFER_KEY, 
456            String.valueOf(autosendMaxBuffer), overridePreexisting);
457        setDefaultProperty(SENSORSHELL_LOGGING_LEVEL_KEY, 
458            String.valueOf(loggingLevel), overridePreexisting);
459      }
460      
461      /**
462       * Checks that the standard sensor properties have valid values.
463       * <p>
464       * The approach is that the instance variables (timeout, multishellEnabled, etc.) always start off
465       * holding the default values, while the properties start off with potentially new and 
466       * potentially invalid values.  We attempt to set the instance variables to the new values
467       * in the properties.  If this fails, we use the instance variables to reset the properties
468       * to the defaults.
469       * <p>
470       * The approach for the required variables is that if they do not have values, we throw
471       * an error.   
472       * @throws SensorShellException if problems occur.
473       */
474      private final void validateProperties() throws SensorShellException {
475        String newValue = null;
476        String origValue = null;
477        String errMsg = "SensorProperties instantiation error: "; 
478        // TIMEOUT
479        try {
480          origValue = String.valueOf(this.timeout);
481          newValue = this.getProperty(SENSORSHELL_TIMEOUT_KEY);
482          this.timeout = Integer.parseInt(newValue);
483          if (this.timeout < 1) {
484            this.logger.warning(errMsg + SENSORSHELL_TIMEOUT_KEY + " " + newValue); 
485            this.sensorProps.setProperty(SENSORSHELL_TIMEOUT_KEY, origValue);
486            this.timeout = Integer.parseInt(origValue);
487          }
488        }
489        catch (Exception e) {
490          this.logger.warning(errMsg + SENSORSHELL_TIMEOUT_KEY + " " + newValue); 
491          this.sensorProps.setProperty(SENSORSHELL_TIMEOUT_KEY, origValue);
492        }
493        // Now set the SensorBaseClient default timeout.
494        System.setProperty(SensorBaseClient.SENSORBASECLIENT_TIMEOUT_KEY, 
495            String.valueOf(this.timeout * 1000));
496        // PING TIMEOUT
497        try {
498          origValue = String.valueOf(this.pingTimeout);
499          newValue = this.getProperty(SENSORSHELL_PING_TIMEOUT_KEY);
500          this.pingTimeout = Integer.parseInt(newValue);
501          if (this.pingTimeout < 1) {
502            this.logger.warning(errMsg + SENSORSHELL_PING_TIMEOUT_KEY + " " + newValue); 
503            this.sensorProps.setProperty(SENSORSHELL_PING_TIMEOUT_KEY, origValue);
504            this.pingTimeout = Integer.parseInt(origValue);
505          }
506        }
507        catch (Exception e) {
508          this.logger.warning(errMsg + SENSORSHELL_PING_TIMEOUT_KEY + " " + newValue); 
509          this.sensorProps.setProperty(SENSORSHELL_PING_TIMEOUT_KEY, origValue);
510        }
511        // MULTISHELL_ENABLED
512        try {
513          origValue = String.valueOf(multiShellEnabled);
514          newValue = this.getProperty(SENSORSHELL_MULTISHELL_ENABLED_KEY);
515          this.multiShellEnabled = Boolean.parseBoolean(newValue);
516        }
517        catch (Exception e) {
518          this.logger.warning(errMsg + SENSORSHELL_MULTISHELL_ENABLED_KEY + " " + newValue); 
519          this.sensorProps.setProperty(SENSORSHELL_MULTISHELL_ENABLED_KEY, origValue); 
520        }
521        // MULTISHELL_NUMSHELLS
522        try {
523          origValue = String.valueOf(multiShellNumShells);
524          newValue = this.getProperty(SENSORSHELL_MULTISHELL_NUMSHELLS_KEY);
525          this.multiShellNumShells = Integer.parseInt(newValue);
526          if (this.multiShellNumShells < 1) {
527            this.logger.warning(errMsg + SENSORSHELL_MULTISHELL_NUMSHELLS_KEY + " " + newValue); 
528            this.sensorProps.setProperty(SENSORSHELL_MULTISHELL_NUMSHELLS_KEY, origValue);
529            this.multiShellNumShells = Integer.parseInt(origValue);
530          }
531        }
532        catch (Exception e) {
533          this.logger.warning(errMsg + SENSORSHELL_MULTISHELL_NUMSHELLS_KEY + " " + newValue); 
534          this.sensorProps.setProperty(SENSORSHELL_MULTISHELL_NUMSHELLS_KEY, origValue); 
535        }
536        // MULTISHELL_BATCHSIZE
537        try {
538          origValue = String.valueOf(multiShellBatchSize);
539          newValue = this.getProperty(SENSORSHELL_MULTISHELL_BATCHSIZE_KEY);
540          this.multiShellBatchSize = Integer.parseInt(newValue);
541          if (this.multiShellBatchSize < 1) {
542            this.logger.warning(errMsg + SENSORSHELL_MULTISHELL_BATCHSIZE_KEY + " " + newValue); 
543            this.sensorProps.setProperty(SENSORSHELL_MULTISHELL_BATCHSIZE_KEY, origValue);
544            this.multiShellBatchSize = Integer.parseInt(origValue);
545          }
546        }
547        catch (Exception e) {
548          this.logger.warning(errMsg + SENSORSHELL_MULTISHELL_BATCHSIZE_KEY + " " + newValue); 
549          this.sensorProps.setProperty(SENSORSHELL_MULTISHELL_BATCHSIZE_KEY, origValue);
550        }
551        // MULTISHELL_MAXBUFFER
552        try {
553          origValue = String.valueOf(multiShellMaxBuffer);
554          newValue = this.getProperty(SENSORSHELL_MULTISHELL_MAXBUFFER_KEY);
555          this.multiShellMaxBuffer = Integer.parseInt(newValue);
556          if (this.multiShellMaxBuffer < 1) {
557            this.logger.warning(errMsg + SENSORSHELL_MULTISHELL_MAXBUFFER_KEY + " " + newValue); 
558            this.sensorProps.setProperty(SENSORSHELL_MULTISHELL_MAXBUFFER_KEY, origValue);
559            this.multiShellMaxBuffer = Integer.parseInt(origValue);
560          }
561        }
562        catch (Exception e) {
563          this.logger.warning(errMsg + SENSORSHELL_MULTISHELL_MAXBUFFER_KEY + " " + newValue); 
564          this.sensorProps.setProperty(SENSORSHELL_MULTISHELL_MAXBUFFER_KEY, origValue);
565        }    
566        // MULTISHELL_AUTOSEND_TIMEINTERVAL
567        try {
568          origValue = String.valueOf(multiShellAutoSendTimeInterval);
569          newValue = this.getProperty(SENSORSHELL_MULTISHELL_AUTOSEND_TIMEINTERVAL_KEY);
570          this.multiShellAutoSendTimeInterval = Double.parseDouble(newValue);
571          if (this.multiShellAutoSendTimeInterval < 0.0) {
572            this.logger.warning(errMsg + SENSORSHELL_MULTISHELL_AUTOSEND_TIMEINTERVAL_KEY + newValue); 
573            this.sensorProps.setProperty(SENSORSHELL_MULTISHELL_AUTOSEND_TIMEINTERVAL_KEY, origValue);
574            this.multiShellAutoSendTimeInterval = Double.parseDouble(origValue);
575          }
576        }
577        catch (Exception e) {
578          this.logger.warning(errMsg + SENSORSHELL_MULTISHELL_AUTOSEND_TIMEINTERVAL_KEY + newValue); 
579          this.sensorProps.setProperty(SENSORSHELL_MULTISHELL_AUTOSEND_TIMEINTERVAL_KEY, origValue);
580        }
581        // OFFLINE_CACHE_ENABLED
582        try {
583          origValue = String.valueOf(offlineCacheEnabled);
584          newValue = this.getProperty(SENSORSHELL_OFFLINE_CACHE_ENABLED_KEY);
585          this.offlineCacheEnabled = Boolean.parseBoolean(newValue);
586        }
587        catch (Exception e) {
588          this.logger.warning(errMsg + SENSORSHELL_OFFLINE_CACHE_ENABLED_KEY + " " + newValue); 
589          this.sensorProps.setProperty(SENSORSHELL_OFFLINE_CACHE_ENABLED_KEY, origValue);
590        }
591        if (this.multiShellEnabled && this.offlineCacheEnabled) {
592          this.logger.warning("Warning: Offline cache disabled since multishell enabled.");
593          this.offlineCacheEnabled = false;
594        }
595    
596        // OFFLINE_RECOVERY_ENABLED
597        try {
598          origValue = String.valueOf(offlineRecoveryEnabled);
599          newValue = this.getProperty(SENSORSHELL_OFFLINE_RECOVERY_ENABLED_KEY);
600          this.offlineRecoveryEnabled = Boolean.parseBoolean(newValue);
601        }
602        catch (Exception e) {
603          this.logger.warning(errMsg + SENSORSHELL_OFFLINE_RECOVERY_ENABLED_KEY + " " + newValue); 
604          this.sensorProps.setProperty(SENSORSHELL_OFFLINE_RECOVERY_ENABLED_KEY, origValue);
605        }
606        if (this.multiShellEnabled && this.offlineRecoveryEnabled) {
607          this.logger.warning("Warning: Offline recovery disabled since multishell enabled.");
608          this.offlineRecoveryEnabled = false;
609        }
610        // STATECHANGE_INTERVAL
611        try {
612          origValue = String.valueOf(statechangeInterval);
613          newValue = this.getProperty(SENSORSHELL_STATECHANGE_INTERVAL_KEY);
614          this.statechangeInterval = Integer.parseInt(newValue);
615          if (this.statechangeInterval < 1) {
616            this.logger.warning(errMsg + SENSORSHELL_STATECHANGE_INTERVAL_KEY + " " + newValue); 
617            this.sensorProps.setProperty(SENSORSHELL_STATECHANGE_INTERVAL_KEY, origValue);
618            this.statechangeInterval = Integer.parseInt(origValue);
619          }
620        }
621        catch (Exception e) {
622          this.logger.warning(errMsg + SENSORSHELL_STATECHANGE_INTERVAL_KEY + " " + newValue); 
623          this.sensorProps.setProperty(SENSORSHELL_STATECHANGE_INTERVAL_KEY, origValue);
624        }
625        // AUTOSEND_TIMEINTERVAL (Single Shell)
626        try {
627          origValue = String.valueOf(autosendTimeInterval);
628          newValue = this.getProperty(SENSORSHELL_AUTOSEND_TIMEINTERVAL_KEY);
629          this.autosendTimeInterval = Double.parseDouble(newValue);
630          if (this.autosendTimeInterval < 0.0) {
631            this.logger.warning(errMsg + SENSORSHELL_AUTOSEND_TIMEINTERVAL_KEY + " " + newValue); 
632            this.sensorProps.setProperty(SENSORSHELL_AUTOSEND_TIMEINTERVAL_KEY, origValue);
633            this.autosendTimeInterval = Double.parseDouble(origValue);
634          }
635        }
636        catch (Exception e) {
637          this.logger.warning(errMsg + SENSORSHELL_AUTOSEND_TIMEINTERVAL_KEY + " " + newValue); 
638          this.sensorProps.setProperty(SENSORSHELL_AUTOSEND_TIMEINTERVAL_KEY, origValue);
639        }
640        // AUTOSEND_MAXBUFFER
641        try {
642          origValue = String.valueOf(autosendMaxBuffer);
643          newValue = this.getProperty(SENSORSHELL_AUTOSEND_MAXBUFFER_KEY);
644          this.autosendMaxBuffer = Integer.parseInt(newValue);
645          if (this.autosendMaxBuffer < 1) {
646            this.logger.warning(errMsg + SENSORSHELL_AUTOSEND_MAXBUFFER_KEY + " " + newValue); 
647            this.sensorProps.setProperty(SENSORSHELL_AUTOSEND_MAXBUFFER_KEY, origValue);
648            this.autosendMaxBuffer = Integer.parseInt(origValue);
649          }
650        }
651        catch (Exception e) {
652          this.logger.warning(errMsg + SENSORSHELL_AUTOSEND_MAXBUFFER_KEY + " " + newValue); 
653          this.sensorProps.setProperty(SENSORSHELL_AUTOSEND_MAXBUFFER_KEY, origValue);
654        }
655        // LOGGING_LEVEL
656        try {
657          origValue = String.valueOf(loggingLevel);
658          newValue = this.getProperty(SENSORSHELL_LOGGING_LEVEL_KEY);
659          this.loggingLevel = Level.parse(newValue);
660        }
661        catch (Exception e) {
662          this.logger.warning(errMsg + SENSORSHELL_LOGGING_LEVEL_KEY + " " + newValue); 
663          this.sensorProps.setProperty(SENSORSHELL_LOGGING_LEVEL_KEY, origValue);
664        }
665        
666        String errInfo = " You might wish to check the settings in " + 
667        sensorShellPropertiesFile.getAbsolutePath();
668        
669        
670        // SENSORBASE_HOST
671        this.sensorBaseHost = this.getProperty(SENSORSHELL_SENSORBASE_HOST_KEY);
672        if (this.sensorBaseHost == null) {
673          throw new SensorShellException("Missing sensorshell.sensorbase.host." + errInfo);
674        }
675        if (!this.sensorBaseHost.endsWith("/")) {
676          this.sensorBaseHost = this.sensorBaseHost + "/";
677        }
678        // SENSORBASE_USER
679        this.user = this.getProperty(SENSORSHELL_SENSORBASE_USER_KEY);
680        if (this.user == null) {
681          throw new SensorShellException("Missing sensorshell.sensorbase.user." + errInfo);
682        }
683        // SENSORBASE_PASSWORD
684        this.password = this.getProperty(SENSORSHELL_SENSORBASE_PASSWORD_KEY);
685        if (this.password == null) {
686          throw new SensorShellException("Missing sensorshell.sensorbase.password." + errInfo);
687        }
688      }
689      
690      /**
691       * Ensures that the specified property has a value in this sensorshell properties.
692       * If overridePreexisting is true, then property will be set to value regardless of whether
693       * it had a previously existing value. 
694       * If overridePreexisting is false, then property will be set to value only if there is no
695       * currently defined property.    
696       * @param property The property to set, if not yet defined.
697       * @param value The value to set the property to, if not yet defined.
698       * @param overridePreexisting True if property will be set to value even if it currently exists. 
699       */
700      private void setDefaultProperty(String property, String value, boolean overridePreexisting) {
701        if (overridePreexisting) {
702          this.sensorProps.setProperty(property, value);
703        }
704        else if (!this.sensorProps.containsKey(property)) {
705          this.sensorProps.setProperty(property, value);
706        }
707      }
708      
709      /**
710       * Returns the trimmed property value associated with the property key. This is useful for
711       * clients who want to look for "non-standard" properties supplied in sensorshell.properties.
712       * For standard properties, clients should use the accessor methods, since these will perform
713       * type conversion.
714       * @param key The parameter key.
715       * @return The trimmed property value associated with this property key, or null if not found.
716       */
717      public final String getProperty(String key) {
718        String value = this.sensorProps.getProperty(key);
719        if (value != null) {
720          value = value.trim();
721        }
722        return value;
723      }
724    
725      /**
726       * Returns the sensorbase host, such as "http://dasha.ics.hawaii.edu:9876/sensorbase".
727       * @return The sensorbase host.
728       */
729      public String getSensorBaseHost() {
730        return this.sensorBaseHost;
731      }
732    
733    
734      /**
735       * Returns the password for this user, such as "xu876csld".
736       * @return The user password.
737       */
738      public String getSensorBasePassword() {
739        return this.password;
740      }
741    
742      /**
743       * Returns the account for this user, such as "johnson@hawaii.edu".
744       * @return The user account.
745       */
746      public String getSensorBaseUser() {
747        return this.user;
748      }
749      
750      /**
751       * Returns the AutoSend time interval, such as 1.0.
752       * @return The autosend interval.
753       */
754      public double getAutoSendTimeInterval() {
755        return this.autosendTimeInterval;
756      }
757      
758      /**
759       * Returns the AutoSend batch size, such as 250.
760       * @return The autosend batch size.
761       */
762      public int getAutoSendMaxBuffer() {
763        return this.autosendMaxBuffer;
764      }
765    
766      /**
767       * Returns the StateChange interval.
768       * @return The state change interval in seconds.
769       */
770      public int getStateChangeInterval() {
771        return this.statechangeInterval;
772      }
773      
774      /**
775       * Returns the current timeout setting for non-Ping requests.
776       * @return The timeout in seconds. 
777       */
778      public int getTimeout() {
779        return this.timeout;
780      }
781      
782      /**
783       * Returns the current timeout setting for Ping requests.
784       * @return The ping timeout in seconds. 
785       */
786      public int getPingTimeout() {
787        return this.pingTimeout;
788      }
789      
790      /**
791       * Returns the logging level specified for SensorShells.
792       * @return The logging level.
793       */
794      public Level getLoggingLevel() {
795        return this.loggingLevel;
796      }
797      
798      /**
799       * Returns true if multishell processing is enabled.
800       * @return True if multishell processing.
801       */
802      public boolean isMultiShellEnabled () {
803        return this.multiShellEnabled;
804      }
805    
806      /**
807       * Returns the number of shells to instantiate if multishell processing is enabled.
808       * @return The number of shells to instantiate.
809       */
810      public int getMultiShellNumShells() {
811        return this.multiShellNumShells;
812      }
813      
814      /**
815       * Returns the number of instances to send to one shell in a row if multishell processing.
816       * @return The number of instances to send to one shell in a row.
817       */
818      public int getMultiShellBatchSize() {
819        return this.multiShellBatchSize;
820      }
821      
822      /**
823       * Returns the maximum number of instances to buffer before a blocking send is invoked in
824       * multishell mode.
825       * @return The maximum number of instances to buffer.
826       */
827      public int getMultiShellMaxBuffer() {
828        return this.multiShellMaxBuffer;
829      }
830      
831      /**
832       * Returns the MultiShell AutoSend time interval, such as 0.10.
833       * @return The multishell autosend interval.
834       */
835      public double getMultiShellAutoSendTimeInterval() {
836        return this.multiShellAutoSendTimeInterval;
837      }
838    
839    
840      /**
841       * Returns true if offline cache data saving is enabled.
842       * @return True if offline caching enabled.
843       */
844      public boolean isOfflineCacheEnabled() {
845        return this.offlineCacheEnabled;
846      }
847      
848      
849      /**
850       * Returns true if offline data recovery is enabled.
851       * @return True if offline data recovery enabled.
852       */
853      public boolean isOfflineRecoveryEnabled () {
854        return this.offlineRecoveryEnabled;
855      }
856      
857      /**
858       * Returns the set of SensorProperties as a multi-line string.
859       * Does not print out the value associated with SENSORSHELL_PASSWORD_KEY. 
860       * @return The Sensor property keys and values. 
861       */
862      @Override
863      public String toString() {
864        String cr = System.getProperty("line.separator");
865        StringBuffer buff = new StringBuffer(100);
866        // It turns out to be much, much more usable to get these properties alphabetized.
867        TreeMap<String, String> map = new TreeMap<String, String>();
868        for (Object key : this.sensorProps.keySet()) {
869          map.put(key.toString(), this.sensorProps.get(key).toString());
870        }
871        // Now print them out in alphabetical order.
872        buff.append("SensorProperties");
873        for (Entry<String, String> entry : map.entrySet()) {
874          buff.append(cr);
875          buff.append("  ");
876          buff.append(entry.getKey());
877          buff.append(" : "); 
878          buff.append((entry.getKey().equals(SENSORSHELL_SENSORBASE_PASSWORD_KEY)) ? 
879              "<password hidden>" : entry.getValue());
880        }
881        buff.append(cr);
882        buff.append("  sensorshell.properties file location: ");
883        buff.append(this.sensorShellPropertiesFile.getAbsolutePath());
884        return buff.toString();
885      }
886     
887      /**
888       * Sets the autosend time interval and autosend max buffer size to the multishell versions.
889       * Invoked by the multishell just before instantiating its child SingleSensorShells so that
890       * they are set up with the appropriate multishell time interval. 
891       */
892      public void switchToMultiShellMode() {
893        this.sensorProps.setProperty(SENSORSHELL_AUTOSEND_TIMEINTERVAL_KEY, 
894            String.valueOf(this.getMultiShellAutoSendTimeInterval()));
895        this.autosendTimeInterval = this.multiShellAutoSendTimeInterval;
896        this.sensorProps.setProperty(SENSORSHELL_AUTOSEND_MAXBUFFER_KEY, 
897            String.valueOf(this.getMultiShellMaxBuffer()));
898        this.autosendMaxBuffer = this.getMultiShellMaxBuffer();
899      }
900      
901      /**
902       * Returns a new SensorShellProperties instance customized for offline data recovery.
903       * This means that multishell, autosend, and offline recovery/caching are all disabled.
904       * @param properties The original properties.
905       * @return The new SensorShellProperties instance. 
906       * @throws SensorShellException If something goes wrong. 
907       */
908    
909      public static SensorShellProperties getOfflineMode(SensorShellProperties properties) 
910      throws SensorShellException {
911        String falseStr = "false";
912        Properties props = new Properties();
913        props.setProperty(SENSORSHELL_MULTISHELL_ENABLED_KEY, falseStr);
914        props.setProperty(SENSORSHELL_OFFLINE_CACHE_ENABLED_KEY, falseStr);
915        props.setProperty(SENSORSHELL_OFFLINE_RECOVERY_ENABLED_KEY, falseStr);
916        props.setProperty(SENSORSHELL_AUTOSEND_TIMEINTERVAL_KEY, "0.0");
917        return new SensorShellProperties(props, true);
918      }
919    }
920