001 package org.hackystat.sensorshell; 002 003 import java.io.File; 004 import java.io.FileInputStream; 005 import java.io.FileOutputStream; 006 import java.text.SimpleDateFormat; 007 import java.util.Date; 008 import java.util.Locale; 009 import javax.xml.bind.JAXBContext; 010 import javax.xml.bind.Marshaller; 011 import javax.xml.bind.Unmarshaller; 012 import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData; 013 import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDatas; 014 import org.hackystat.utilities.home.HackystatUserHome; 015 import org.hackystat.utilities.stacktrace.StackTrace; 016 017 /** 018 * Provides a facility for: (a) persisting buffered SensorData instances locally when the SensorBase 019 * host is not available and (b) recovering them during a subsequent invocation of SensorShell. 020 * 021 * @author Philip Johnson 022 */ 023 public class OfflineManager { 024 025 /** The directory where offline data is stored. */ 026 private File offlineDir; 027 028 /** The jaxb context. */ 029 private JAXBContext jaxbContext; 030 031 /** Holds the sensorShellProperties instance from the parent sensor shell. */ 032 private SensorShellProperties properties; 033 034 /** The shell that created this offline manager. **/ 035 private SingleSensorShell parentShell; 036 037 /** The tool that was created the parent shell. */ 038 private String tool; 039 040 /** Whether or not data has been stored offline. */ 041 boolean hasOfflineData = false; 042 043 /** 044 * Creates an OfflineManager given the parent shell and the tool. 045 * @param shell The parent shell. 046 * @param tool The tool. 047 */ 048 public OfflineManager(SingleSensorShell shell, String tool) { 049 this.parentShell = shell; 050 this.properties = shell.getProperties(); 051 this.tool = tool; 052 this.offlineDir = new File(HackystatUserHome.getHome(), "/.hackystat/sensorshell/offline/"); 053 boolean dirOk = this.offlineDir.mkdirs(); 054 if (!dirOk && !this.offlineDir.exists()) { 055 throw new RuntimeException("mkdirs failed"); 056 } 057 try { 058 this.jaxbContext = 059 JAXBContext.newInstance( 060 org.hackystat.sensorbase.resource.sensordata.jaxb.ObjectFactory.class); 061 } 062 catch (Exception e) { 063 throw new RuntimeException("Could not create JAXB context.", e); 064 } 065 } 066 067 /** 068 * Stores a SensorDatas instance to a serialized file in the offline directory. 069 * Does nothing if there are no sensordata instances in the SensorDatas instance. 070 * @param sensorDatas The SensorDatas instance to be stored. 071 */ 072 public void store(SensorDatas sensorDatas) { 073 if (sensorDatas.getSensorData().size() > 0) { 074 SimpleDateFormat fileTimestampFormat = 075 new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss.SSS", Locale.US); 076 String fileStampString = fileTimestampFormat.format(new Date()); 077 File outFile = new File(this.offlineDir, fileStampString + ".xml"); 078 try { 079 Marshaller marshaller = jaxbContext.createMarshaller(); 080 marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); 081 marshaller.marshal(sensorDatas, new FileOutputStream(outFile)); 082 parentShell.println("Stored " + sensorDatas.getSensorData().size() + 083 " sensor data instances in: " + outFile.getAbsolutePath()); 084 this.hasOfflineData = true; 085 } 086 catch (Exception e) { 087 parentShell.println("Error writing the offline file " + outFile.getName() + " " + e); 088 } 089 } 090 } 091 092 /** 093 * Returns true if this offline manager has successfully stored any data offline. 094 * @return True if offline data has been stored. 095 */ 096 public boolean hasOfflineData() { 097 return this.hasOfflineData; 098 } 099 100 101 /** 102 * Attempts to resend any previously stored SensorDatas instances from their serialized files. 103 * Creates a new sensorshell instance to do the sending. 104 * Each SensorDatas instance is deserialized, then each SensorData instance inside is 105 * sent to the SensorShell. This gives the SensorShell an opportunity to 106 * send batches off at whatever interval it chooses. 107 * All serialized files are deleted after being processed if successful. 108 * @throws SensorShellException If problems occur sending the recovered data. 109 */ 110 public void recover() throws SensorShellException { 111 // Return immediately if there are no offline files to process. 112 File[] xmlFiles = this.offlineDir.listFiles(new ExtensionFileFilter(".xml")); 113 if (xmlFiles.length == 0) { 114 return; 115 } 116 // Tell the parent shell log that we're going to try to do offline recovery. 117 parentShell.println("Invoking offline recovery on " + xmlFiles.length + " files."); 118 119 // Create a new properties instance with offline recovery/storage disabled. 120 SensorShellProperties props = SensorShellProperties.getOfflineMode(this.properties); 121 // Provide a separate log file for this offline recovery. 122 String offlineTool = this.tool + "-offline-recovery"; 123 // Create the offline sensor shell to be used for sending this data. 124 SingleSensorShell shell = new SingleSensorShell(props, false, offlineTool); 125 shell.println("Invoking offline recovery on " + xmlFiles.length + " files."); 126 FileInputStream fileStream = null; 127 128 // For each offline file to recover 129 for (int i = 0; i < xmlFiles.length; i++) { 130 try { 131 // Reconstruct the SensorDatas instances from the serialized files. 132 shell.println("Recovering offline data from: " + xmlFiles[i].getName()); 133 fileStream = new FileInputStream(xmlFiles[i]); 134 Unmarshaller unmarshaller = this.jaxbContext.createUnmarshaller(); 135 SensorDatas sensorDatas = (SensorDatas)unmarshaller.unmarshal(fileStream); 136 shell.println("Found " + sensorDatas.getSensorData().size() + " instances."); 137 for (SensorData data : sensorDatas.getSensorData()) { 138 shell.add(data); 139 } 140 // Try to send the data. 141 shell.println("About to send data"); 142 int numSent = shell.send(); 143 shell.println("Successfully sent: " + numSent + " instances."); 144 // If all the data was successfully sent, then we delete the file. 145 if (numSent == sensorDatas.getSensorData().size()) { 146 boolean isDeleted = xmlFiles[i].delete(); 147 shell.println("Trying to delete " + xmlFiles[i].getName() + ". Success: " + isDeleted); 148 } 149 else { 150 shell.println("Did not send all instances. " + xmlFiles[i] + " not deleted."); 151 } 152 fileStream.close(); 153 } 154 catch (Exception e) { 155 shell.println("Error recovering data from: " + xmlFiles[i] + " " + StackTrace.toString(e)); 156 try { 157 fileStream.close(); 158 } 159 catch (Exception f) { 160 shell.println("Failed to close: " + fileStream.toString() + " " + e); 161 } 162 } 163 } 164 shell.quit(); 165 } 166 }