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    }