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 }