001    package org.hackystat.sensorshell.command;
002    
003    import java.util.Date;
004    import java.util.Map;
005    import java.util.logging.Level;
006    
007    import javax.xml.datatype.XMLGregorianCalendar;
008    
009    import org.hackystat.sensorbase.client.SensorBaseClient;
010    import org.hackystat.sensorbase.client.SensorBaseClientException;
011    import org.hackystat.utilities.stacktrace.StackTrace;
012    import org.hackystat.utilities.tstamp.Tstamp;
013    import org.hackystat.sensorbase.resource.sensordata.jaxb.Properties;
014    import org.hackystat.sensorbase.resource.sensordata.jaxb.Property;
015    import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
016    import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDatas;
017    import org.hackystat.sensorshell.SensorShellException;
018    import org.hackystat.sensorshell.SensorShellProperties;
019    import org.hackystat.sensorshell.SingleSensorShell;
020    
021    /**
022     * Implements the SensorData commands, of which there is "add", "send", and "statechange".
023     * @author Philip Johnson
024     */
025    public class SensorDataCommand extends Command { 
026        
027      private static final String RESOURCE = "Resource";
028      /** The list of unsent SensorData instances. */
029      private SensorDatas sensorDatas = new SensorDatas();
030      /** The Ping Command. */
031      private PingCommand pingCommand;
032      /** The sensorbase client. */
033      private SensorBaseClient client;
034      /** Holds the Resource value from the last StateChange event. */
035      private String lastStateChangeResource = "";
036      /** Holds the bufferSize value from the last StateChange event. */
037      private long lastStateChangeResourceCheckSum = 0;
038      /** Holds the total number of sensor data sent to the server. */
039      private long totalSent = 0;
040      
041      
042      /**
043       * Creates the SensorDataCommand. 
044       * @param shell The sensorshell. 
045       * @param properties The sensorproperties.
046       * @param pingCommand The Ping Command. 
047       * @param client The SensorBase client.
048       */
049      public SensorDataCommand(SingleSensorShell shell, SensorShellProperties properties, 
050          PingCommand pingCommand, SensorBaseClient client) {
051        super(shell, properties);
052        this.pingCommand = pingCommand;
053        this.client = client;
054      }
055      
056      /**
057       * Sends accumulated data, including offline and current data from the AddCommand.
058       * If server not pingable, then the offline data is saved for a later attempt.
059       * @return The number of sensor data instances that were sent. 
060       * @throws SensorShellException If problems occur sending the data. 
061       */
062      public int send() throws SensorShellException {
063        
064        int numDataSent = 0;
065    
066        // Return right away if there is no data to send
067        if (sensorDatas.getSensorData().isEmpty()) {
068          return 0;
069        }
070        // Indicate we're sending if in interactive mode.
071        if (!this.shell.isInteractive()) {
072          this.shell.getLogger().info("#> send" + cr);
073        }
074        
075        // Do a ping to see that we can connect to the server. 
076        if (this.pingCommand.isPingable()) {
077          // We can connect, and there is data, so attempt to send.
078          try {
079            this.shell.println("Attempting to send " + sensorDatas.getSensorData().size() 
080                + " sensor data instances. Available memory (bytes): " + getAvailableMemory());
081            long startTime = new Date().getTime(); 
082            this.client.putSensorDataBatch(sensorDatas);
083            this.shell.println("Successful send to " + this.properties.getSensorBaseHost() +
084                " Elapsed time: " + (new Date().getTime() - startTime) + " ms.");
085            numDataSent = sensorDatas.getSensorData().size();
086            totalSent += numDataSent;
087            this.sensorDatas.getSensorData().clear();
088            return numDataSent;
089          }
090          catch (SensorBaseClientException e) {
091            this.shell.println("Error sending data: " + e);
092            this.shell.println(StackTrace.toString(e));
093            this.sensorDatas.getSensorData().clear();
094            throw new SensorShellException("Could not send data: error in SensorBaseClient", e);
095          }
096        }
097    
098        // If we got here, then the server was not available.
099        if (this.properties.isOfflineCacheEnabled()) {
100          this.shell.println("Server " + this.properties.getSensorBaseHost() + " not available." + 
101              " Storing sensor data offline.");
102          this.shell.getOfflineManager().store(this.sensorDatas);
103          this.sensorDatas.getSensorData().clear();
104          return 0;
105        }
106        else {
107          String msg = "Server not available and offline storage disabled. Sensor Data lost.";
108          this.shell.println(msg);
109          this.sensorDatas.getSensorData().clear();
110          throw new SensorShellException(msg);
111        }
112      }
113      
114      /**
115       * Returns the data instance in a formatted string.
116       * @param data The sensor data instance. 
117       * @return A string displaying the instance.
118       */
119      private String formatSensorData(SensorData data) {
120        StringBuffer buffer = new StringBuffer(75);
121        buffer.append('<');
122        buffer.append(data.getTimestamp());
123        buffer.append(' ');
124        buffer.append(data.getSensorDataType());
125        buffer.append(' ');
126        buffer.append(data.getOwner());
127        buffer.append(' ');
128        buffer.append(data.getTool());
129        buffer.append(' ');
130        buffer.append(data.getResource());
131        buffer.append(' ');
132        buffer.append(data.getRuntime());
133        buffer.append(' ');
134        for (Property property : data.getProperties().getProperty()) {
135          buffer.append(property.getKey());
136          buffer.append('=');
137          buffer.append(property.getValue());
138          buffer.append(' ');
139        }
140        buffer.append('>');
141        return buffer.toString();
142      }
143     
144      
145      /**
146       * Given a Map containing key-value pairs corresponding to SensorData fields and properties,
147       * constructs a SensorData instance and stores it for subsequent sending to the SensorBase.
148       * @param keyValMap The map of key-value pairs. 
149       * @throws SensorShellException If problems occur sending the data.  
150       */
151      public void add(Map<String, String> keyValMap) throws SensorShellException {
152        // Begin by creating the sensor data instance. 
153        try {
154        SensorData data = new SensorData();
155        XMLGregorianCalendar tstamp = Tstamp.makeTimestamp();
156        data.setOwner(getMap(keyValMap, "Owner", this.properties.getSensorBaseUser()));
157        data.setResource(getMap(keyValMap, RESOURCE, ""));
158        data.setRuntime(Tstamp.makeTimestamp(getMap(keyValMap, "Runtime", tstamp.toString())));
159        data.setSensorDataType(getMap(keyValMap, "SensorDataType", ""));
160        data.setTimestamp(Tstamp.makeTimestamp(getMap(keyValMap, "Timestamp", tstamp.toString())));
161        data.setTool(getMap(keyValMap, "Tool", "unknown"));
162        data.setProperties(new Properties());
163        // Add all non-standard key-val pairs to the property list. 
164        for (Map.Entry<String, String> entry : keyValMap.entrySet()) {
165          Property property = new Property();
166          String key = entry.getKey();
167          if (isProperty(key)) {
168            property.setKey(key);
169            property.setValue(entry.getValue());
170            data.getProperties().getProperty().add(property);
171          }
172        }
173        add(data);
174        }
175        catch (Exception e) {
176          throw new SensorShellException("Error adding sensor data instance.", e);
177        }
178      }
179      
180      /**
181       * Adds the SensorData instance, invoking send if the max buffer size has been exceeded.
182       * @param data The SensorData instance to be added.
183       * @throws SensorShellException If problems occur sending the data. 
184       */
185      public void add(SensorData data) throws SensorShellException {
186        sensorDatas.getSensorData().add(data);
187        if (this.shell.getLogger().isLoggable(Level.FINE)) {
188          this.shell.println("Adding: " + formatSensorData(data));
189        }
190        // If that makes the buffer size too big, then send this data. 
191        if (sensorDatas.getSensorData().size() > properties.getAutoSendMaxBuffer()) {
192          this.shell.println("Invoking send(); buffer size > " + properties.getAutoSendMaxBuffer());
193          try {
194            this.send();
195          }
196          catch (SensorShellException e) {
197            this.shell.println("Exception during send(): " + e);
198          }
199        }
200      }
201      
202      /**
203       * Provides an easy way for sensors to implement "StateChange" behavior.  From the sensor side,
204       * StateChange can be implemented by creating a timer process that wakes up at regular 
205       * intervals and gets the file (resource) that the current active buffer is attached to, plus
206       * a "checksum" (typically the size of the resource in characters or bytes or whatever).  
207       * Then, the timer process simply creates the
208       * appropriate keyValMap as if for an 'add' event, and provides it to this method along with 
209       * the buffer size.  This method will check the passed buffer size and the Resource field 
210       * against the values for these two fields passed in the last call of this method, and if 
211       * either one has changed, then the "add" method is called with the keyValMap.  
212       * @param resourceCheckSum Indicates the state of the resource, typically via its size. 
213       * @param keyValMap The map of key-value pairs. 
214       * @throws Exception If problems occur while invoking the 'add' command. 
215       */
216      public void statechange(long resourceCheckSum, Map<String, String> keyValMap) throws Exception {
217        // Get the Resource attribute, default it to "" if not found.
218        String resource = (keyValMap.get(RESOURCE) == null) ? "" : keyValMap.get(RESOURCE);
219        // Do an add if the resource or buffer size has changed.
220        if (!this.lastStateChangeResource.equals(resource)) { //NOPMD
221          this.shell.println("Invoking add: Resource has changed to: " + resource);
222          this.add(keyValMap);
223        }
224        else if (this.lastStateChangeResourceCheckSum != resourceCheckSum) { //NOPMD
225          this.shell.println("Invoking add: CheckSum has changed to: " + resourceCheckSum);
226          this.add(keyValMap);
227        }
228        else {
229          this.shell.println("No change in resource: " + resource + ", checksum: " + resourceCheckSum);
230        }
231        // Always update the 'last' values.
232        this.lastStateChangeResourceCheckSum = resourceCheckSum;
233        this.lastStateChangeResource = resource;
234      }
235    
236      /**
237       * Returns the value associated with key in keyValMap, or the default if the key is not present.
238       * @param keyValMap The map
239       * @param key The key
240       * @param defaultValue The value to return if the key has no mapping.
241       * @return The value to be used.
242       */
243      private String getMap(Map<String, String> keyValMap, String key, String defaultValue) {
244        return (keyValMap.get(key) == null) ? defaultValue : keyValMap.get(key);
245      }
246      
247      /**
248       * Returns true if the passed key is not one of the standard Sensor Data fields including
249       * "Timestamp", "Runtime", "Tool", "Resource", "Owner", or "SensorDataType".
250       * @param key The key.
251       * @return True if the passed key indicates a property, not a standard sensor data field.
252       */
253      private boolean isProperty(String key) {
254        return 
255            (!"Timestamp".equals(key)) &&
256            (!"Runtime".equals(key)) &&
257            (!"Tool".equals(key)) &&
258            (!RESOURCE.equals(key)) &&
259            (!"Owner".equals(key)) &&
260            (!"SensorDataType".equals(key));
261      }
262    
263      /**
264       * Returns the total number of sensor data instances sent so far. 
265       * @return The total sent so far by this instance of SensorDataCommand. 
266       */
267      public long getTotalSent() {
268        return this.totalSent;
269      }
270      
271      /**
272       * Returns true if this SensorDataCommand instance has remaining unsent data. 
273       * @return True if there is data remaining to be sent. 
274       */
275      public boolean hasUnsentData() {
276        return ((this.sensorDatas.getSensorData() != null) &&
277            !this.sensorDatas.getSensorData().isEmpty());
278      }
279      
280      /**
281       * Helper method to return the available memory in bytes.
282       * @return The available memory.
283       */
284      private long getAvailableMemory() {
285        Runtime runtime = Runtime.getRuntime();
286        long maxMemory = runtime.maxMemory();
287        long allocatedMemory = runtime.totalMemory();
288        long freeMemory = runtime.freeMemory();
289        return freeMemory + (maxMemory - allocatedMemory);
290      }
291    }