001    package org.hackystat.sensorbase.resource.sensordata;
002    
003    import static org.hackystat.sensorbase.server.ServerProperties.XML_DIR_KEY;
004    
005    import java.io.File;
006    import java.io.StringReader;
007    import java.io.StringWriter;
008    import java.util.List;
009    import javax.xml.bind.JAXBContext;
010    import javax.xml.bind.Marshaller;
011    import javax.xml.bind.Unmarshaller;
012    import javax.xml.datatype.XMLGregorianCalendar;
013    import javax.xml.parsers.DocumentBuilder;
014    import javax.xml.parsers.DocumentBuilderFactory;
015    import javax.xml.transform.Transformer;
016    import javax.xml.transform.TransformerFactory;
017    import javax.xml.transform.dom.DOMSource;
018    import javax.xml.transform.stream.StreamResult;
019    
020    import org.hackystat.sensorbase.db.DbManager;
021    import org.hackystat.utilities.stacktrace.StackTrace;
022    import org.hackystat.utilities.tstamp.Tstamp;
023    import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
024    import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
025    import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
026    import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDatas;
027    import org.hackystat.sensorbase.resource.users.UserManager;
028    import org.hackystat.sensorbase.resource.users.jaxb.User;
029    import org.hackystat.sensorbase.server.Server;
030    import org.w3c.dom.Document;
031    
032    /**
033     * Provides a manager for the Sensor Data resource.
034     * As with all of the Resource managers the methods in this class can be grouped into 
035     * three general categories:
036     * <ul>
037     * <li> URI/Name conversion methods (convert*): these methods translate between the URI and 
038     * Name-based representations of SensorDataType and User resources. 
039     * <li> Database access methods (get*, has*, put*):  these methods communicate with the underlying 
040     * storage system. 
041     * <li> XML/Java translation methods (make*): these methods translate between the XML String
042     * representation of a resource and its Java class instance representation. 
043     * </ul>
044     * <p>  
045     * See https://jaxb.dev.java.net/guide/Performance_and_thread_safety.html for info 
046     * on JAXB performance and thread safety.
047     * @author Philip Johnson
048     */
049    public class SensorDataManager {
050      
051      /** Holds the class-wide JAXBContext, which is thread-safe. */
052      private JAXBContext jaxbContext;
053      
054      /** The Server associated with this SensorDataManager. */
055      Server server; 
056      
057      /** The DbManager associated with this server. */
058      DbManager dbManager;
059      
060      /** The http string identifier. */
061      private static final String http = "http";
062      
063      /** 
064       * The constructor for SensorDataManagers. 
065       * There is one SensorDataManager per Server. 
066       * @param server The Server instance associated with this SensorDataManager. 
067       */
068      public SensorDataManager(Server server) {
069        this.server = server;
070        this.dbManager = (DbManager)this.server.getContext().getAttributes().get("DbManager");
071        UserManager userManager = (UserManager)server.getContext().getAttributes().get("UserManager");
072        try {
073          this.jaxbContext  = 
074            JAXBContext.newInstance(
075                org.hackystat.sensorbase.resource.sensordata.jaxb.ObjectFactory.class);
076          loadDefaultSensorData(userManager); // NOPMD (Incorrect overridable method warning)
077        }
078        catch (Exception e) {
079          String msg = "Exception during SensorDataManager initialization processing";
080          server.getLogger().warning(msg + "\n" + StackTrace.toString(e));
081          throw new RuntimeException(msg, e);
082        }
083      }
084    
085    
086      /**
087       * Loads the sensor data from the sensordata defaults file. 
088       * @param userManager The User Manager.
089       * @throws Exception If problems occur.
090       */
091      private void loadDefaultSensorData(UserManager userManager) throws Exception {
092        // Get the default Sensor Data definitions from the XML defaults file. 
093        File defaultsFile = findDefaultsFile();
094        // Initialize the SDTs if we've found a default file. 
095        if (defaultsFile.exists()) {
096          server.getLogger().info("Loading SensorData defaults: " 
097              + defaultsFile.getPath());
098          Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
099          SensorDatas sensorDatas = (SensorDatas) unmarshaller.unmarshal(defaultsFile);
100          // Initialize the database.
101          for (SensorData data : sensorDatas.getSensorData()) {
102            data.setLastMod(Tstamp.makeTimestamp());
103            String email = convertOwnerToEmail(data.getOwner());
104            if (!userManager.isUser(email)) {
105              throw new IllegalArgumentException("Owner is not a defined User: " + email);
106            }
107            this.dbManager.storeSensorData(data, this.makeSensorData(data), 
108                this.makeSensorDataRefString(data));
109          }
110        }
111      }
112      
113      /**
114       * Checks the ServerProperties for the XML_DIR property.
115       * If this property is null, returns the File for ./xml/defaults/sensordatatypes.defaults.xml.
116       * @return The File instance (which might not point to an existing file.)
117       */
118      private File findDefaultsFile() {
119        String defaultsPath = "/defaults/sensordata.defaults.xml";
120        String xmlDir = server.getServerProperties().get(XML_DIR_KEY);
121        return (xmlDir == null) ?
122            new File (System.getProperty("user.dir") + "/xml" + defaultsPath) :
123              new File (xmlDir + defaultsPath);
124      }
125      
126    
127      /**
128       * Converts an "Owner" string to an email address.
129       * The owner string might be a URI (starting with http) or an email address. 
130       * @param owner The owner string. 
131       * @return The email address corresponding to the owner string. 
132       */
133      public String convertOwnerToEmail(String owner) {
134        if (owner.startsWith(http)) {
135          int lastSlash = owner.lastIndexOf('/');
136          if (lastSlash < 0) {
137            throw new IllegalArgumentException("Could not convert owner to URI");
138          }
139          return owner.substring(lastSlash + 1); 
140        }
141        // Otherwise owner is already the email. 
142        return owner;
143      }
144      
145      /**
146       * Returns the owner string as a URI.
147       * The owner string could either be an email address or the URI. 
148       * @param owner The owner string. 
149       * @return The URI corresponding to the owner string. 
150       */
151      public String convertOwnerToUri(String owner) {
152        return (owner.startsWith(http)) ? owner :
153          server.getServerProperties().getFullHost() + "users/" + owner;
154      }
155      
156      /**
157       * Converts an "sdt" string to the sdt name.
158       * The sdt string might be a URI (starting with http) or the sdt name.
159       * @param sdt The sdt string. 
160       * @return The sdt name corresponding to the sdt string. 
161       */
162      public String convertSdtToName(String sdt) {
163        if (sdt.startsWith(http)) {
164          int lastSlash = sdt.lastIndexOf('/');
165          if (lastSlash < 0) {
166            throw new IllegalArgumentException("Could not convert sdt to name");
167          }
168          return sdt.substring(lastSlash + 1); 
169        }
170        // Otherwise sdt is already the name.
171        return sdt;
172      }
173      
174      /**
175       * Returns the sdt string as a URI.
176       * The sdt string could either be an sdt name or a URI. 
177       * @param sdt The sdt string. 
178       * @return The URI corresponding to the sdt string. 
179       */
180      public String convertSdtToUri(String sdt) {
181        return (sdt.startsWith(http)) ? sdt :
182          server.getServerProperties().getFullHost() + "sensordatatypes/" + sdt;
183      }  
184    
185      
186      /**
187       * Returns the XML SensorDataIndex for all sensor data.
188       * @return The XML String providing an index of all relevent sensor data resources.
189       */
190      public String getSensorDataIndex() {
191        return dbManager.getSensorDataIndex();
192      }
193      
194      /**
195       * Returns the XML SensorDataIndex for all sensor data for this user. 
196       * @param user The User whose sensor data is to be returned. 
197       * @return The XML String providing an index of all relevent sensor data resources.
198       */
199      public String getSensorDataIndex(User user) {
200        return dbManager.getSensorDataIndex(user);
201      }
202      
203      /**
204       * Returns the XML SensorDataIndex for all sensor data for this user and sensor data type.
205       * @param user The User whose sensor data is to be returned. 
206       * @param sdtName The sensor data type name.
207       * @return The XML String providing an index of all relevent sensor data resources.
208       */
209      public String getSensorDataIndex(User user, String sdtName) {
210        return this.dbManager.getSensorDataIndex(user, sdtName);
211      }
212      
213      /**
214       * Returns the XML SensorDataIndex for all sensor data matching these users, start/end time, and 
215       * whose resource string matches at least one in the list of UriPatterns. 
216       * Client must guarantee that startTime and endTime are within Project dates, and that 
217       * startIndex and maxInstances are non-negative.
218       * @param users The users. 
219       * @param startTime The start time. 
220       * @param endTime The end time. 
221       * @param uriPatterns A list of UriPatterns. 
222       * @param startIndex The starting index.
223       * @param maxInstances The maximum number of instances to return.
224       * @return The XML SensorDataIndex string corresponding to the matching sensor data. 
225       */
226      public String getSensorDataIndex(List<User> users, XMLGregorianCalendar startTime, 
227          XMLGregorianCalendar endTime, List<String> uriPatterns, int startIndex, int maxInstances) {
228        return this.dbManager.getSensorDataIndex(users, startTime, endTime, uriPatterns, startIndex,
229            maxInstances);
230      }  
231      
232      /**
233       * Returns the XML SensorDataIndex for all sensor data matching these users, start/end time, and 
234       * whose resource string matches at least one in the list of UriPatterns. 
235       * @param users The users. 
236       * @param startTime The start time. 
237       * @param endTime The end time. 
238       * @param uriPatterns A list of UriPatterns. 
239       * @param sdt The sensordatatype of interest, or null if sensordata from all SDTs should be
240       * retrieved.
241       * @param tool The tool of interest. 
242       * @return The XML SensorDataIndex string corresponding to the matching sensor data. 
243       */
244      public String getSensorDataIndex(List<User> users, XMLGregorianCalendar startTime, 
245          XMLGregorianCalendar endTime, List<String> uriPatterns, String sdt, String tool) {
246        return this.dbManager.getSensorDataIndex(users, startTime, endTime, uriPatterns, sdt, tool);
247      }  
248      
249      /**
250       * Returns the XML SensorDataIndex for all sensor data matching these users, start/end time, and 
251       * whose resource string matches at least one in the list of UriPatterns. 
252       * @param users The users. 
253       * @param startTime The start time. 
254       * @param endTime The end time. 
255       * @param uriPatterns A list of UriPatterns. 
256       * @param sdt The sensordatatype of interest, or null if sensordata from all SDTs should be
257       * retrieved.
258       * @return The XML SensorDataIndex string corresponding to the matching sensor data. 
259       */
260      public String getSensorDataIndex(List<User> users, XMLGregorianCalendar startTime, 
261          XMLGregorianCalendar endTime, List<String> uriPatterns, String sdt) {
262        return this.dbManager.getSensorDataIndex(users, startTime, endTime, uriPatterns, sdt);
263      }  
264      
265      /**
266       * Returns the XML SensorDataIndex for all sensor data for the given user that arrived
267       * at the server between the given timestamps.  This method uses the LastMod timestamp
268       * rather than the "regular" timestamp, and is used for real-time monitoring of data
269       * arriving at the server. 
270       * @param user The user whose data is being monitored.
271       * @param lastModStartTime  The lastMod start time of interest. 
272       * @param lastModEndTime  The lastMod end time of interest. 
273       * @return The XML SensorDataIndex for the recently arrived data based upon the timestamps.
274       */
275      public String getSensorDataIndexLastMod(User user, XMLGregorianCalendar lastModStartTime, 
276          XMLGregorianCalendar lastModEndTime) {
277        return this.dbManager.getSensorDataIndexLastMod(user, lastModStartTime, lastModEndTime);
278      }
279      
280      
281      /**
282       * Updates the Manager with this sensor data. Any old definition is overwritten for
283       * this user and timestamp.
284       * If runtime is not specified, it is defaulted to the timestamp.
285       * If tool, resource, or SDT are not specified, they default to "".
286       * @param data The sensor data. 
287       */
288      public void putSensorData(SensorData data) {
289        if (data.getRuntime() == null) {
290          data.setRuntime(data.getTimestamp());
291        }
292        if (data.getTool() == null) {
293          data.setTool("");
294        }
295        if (data.getResource() == null) {
296          data.setResource("");
297        }
298        if (data.getSensorDataType() == null) { 
299          data.setSensorDataType("");
300        }
301        try {
302          data.setLastMod(Tstamp.makeTimestamp());
303          this.dbManager.storeSensorData(data, this.makeSensorData(data),
304              this.makeSensorDataRefString(data));
305          server.getLogger().info("Put: " + data.getTimestamp() + " " + data.getOwner() + 
306              " " + data.getTool() + " " + data.getSensorDataType() + " " + data.getResource());
307        }
308        catch (Exception e) {
309          server.getLogger().warning("Failed to put sensor data " + StackTrace.toString(e));
310        }
311      }
312      
313      /**
314       * Returns true if the passed [user, timestamp] has sensor data defined for it.
315       * Generally, you will want to use getSensorData(user, timestamp) and check for the 
316       * return value == null to avoid a redundant DB call. 
317       * @param user The user.
318       * @param timestamp The timestamp
319       * @return True if there is any sensor data for this [user, timestamp].
320       */
321      public boolean hasSensorData(User user, XMLGregorianCalendar timestamp) {
322        return this.dbManager.hasSensorData(user, timestamp);
323      }
324      
325      /**
326       * Returns the SensorData XML String corresponding to [user, timestamp], or null if not found.
327       * @param user The user 
328       * @param timestamp The timestamp. 
329       * @return The SensorData XML String, or null if not found. 
330       */
331      public String getSensorData(User user, XMLGregorianCalendar timestamp) {
332        return this.dbManager.getSensorData(user, timestamp);
333      }
334      
335      /**
336       * Ensures that sensor data with the given user and timestamp is no longer
337       * present in this manager.
338       * @param user The user.
339       * @param timestamp The timestamp associated with this sensor data.
340       */
341      public void deleteData(User user, XMLGregorianCalendar timestamp) {
342        this.dbManager.deleteSensorData(user, timestamp);
343      }
344      
345      /**
346       * Ensures that sensor data with the given user is no longer present in this manager.
347       * @param user The user.
348       */
349      public void deleteData(User user) {
350        this.dbManager.deleteSensorData(user);
351      }
352      
353    
354      /**
355       * Takes an XML Document representing a SensorDataIndex and converts it to an instance. 
356       *
357       * @param xmlString The XML string representing a SensorDataIndex. 
358       * @return The corresponding SensorDataIndex instance. 
359       * @throws Exception If problems occur during unmarshalling. 
360       */
361      public SensorDataIndex makeSensorDataIndex(String xmlString) throws Exception {
362        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
363        return (SensorDataIndex) unmarshaller.unmarshal(new StringReader(xmlString));
364      }
365      
366      /**
367       * Takes a String encoding of a SensorData in XML format and converts it to an instance. 
368       * 
369       * @param xmlString The XML string representing a SensorData.
370       * @return The corresponding SensorData instance. 
371       * @throws Exception If problems occur during unmarshalling.
372       */
373      public SensorData makeSensorData(String xmlString) throws Exception {
374        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
375        return (SensorData)unmarshaller.unmarshal(new StringReader(xmlString));
376      }
377      
378      /**
379       * Takes a String encoding of a SensorDatas in XML format and converts it to an instance. 
380       * 
381       * @param xmlString The XML string representing a SensorData.
382       * @return The corresponding SensorData instance. 
383       * @throws Exception If problems occur during unmarshalling.
384       */
385      public SensorDatas makeSensorDatas(String xmlString) throws Exception {
386        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
387        return (SensorDatas)unmarshaller.unmarshal(new StringReader(xmlString));
388      }
389      
390      /**
391       * Returns the passed SensorData instance as a String encoding of its XML representation.
392       * Final because it's called in constructor.
393       * @param data The SensorData instance. 
394       * @return The XML String representation.
395       * @throws Exception If problems occur during translation. 
396       */
397      public final String makeSensorData (SensorData data) throws Exception {
398        Marshaller marshaller = jaxbContext.createMarshaller(); 
399        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
400        dbf.setNamespaceAware(true);
401        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
402        Document doc = documentBuilder.newDocument();
403        marshaller.marshal(data, doc);
404        DOMSource domSource = new DOMSource(doc);
405        StringWriter writer = new StringWriter();
406        StreamResult result = new StreamResult(writer);
407        TransformerFactory tf = TransformerFactory.newInstance();
408        Transformer transformer = tf.newTransformer();
409        transformer.transform(domSource, result);
410        String xmlString = writer.toString();
411        // Now remove the processing instruction.  This approach seems like a total hack.
412        xmlString = xmlString.substring(xmlString.indexOf('>') + 1);
413        return xmlString;
414      }
415    
416      /**
417       * Returns the passed SensorData instance as a String encoding of its XML representation 
418       * as a SensorDataRef object.
419       * Final because it's called in constructor.
420       * @param data The SensorData instance. 
421       * @return The XML String representation of it as a SensorDataRef
422       * @throws Exception If problems occur during translation. 
423       */
424      public final String makeSensorDataRefString (SensorData data) throws Exception {
425        SensorDataRef ref = makeSensorDataRef(data);
426        Marshaller marshaller = jaxbContext.createMarshaller(); 
427        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
428        dbf.setNamespaceAware(true);
429        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
430        Document doc = documentBuilder.newDocument();
431        marshaller.marshal(ref, doc);
432        DOMSource domSource = new DOMSource(doc);
433        StringWriter writer = new StringWriter();
434        StreamResult result = new StreamResult(writer);
435        TransformerFactory tf = TransformerFactory.newInstance();
436        Transformer transformer = tf.newTransformer();
437        transformer.transform(domSource, result);
438        String xmlString = writer.toString();
439        // Now remove the processing instruction.  This approach seems like a total hack.
440        xmlString = xmlString.substring(xmlString.indexOf('>') + 1);
441        return xmlString;
442      }
443      
444      /**
445       * Returns a SensorDataRef instance constructed from a SensorData instance.
446       * @param data The sensor data instance. 
447       * @return A SensorDataRef instance. 
448       */
449      public SensorDataRef makeSensorDataRef(SensorData data) {
450        SensorDataRef ref = new SensorDataRef();
451        String email = convertOwnerToEmail(data.getOwner());
452        String sdt = convertSdtToName(data.getSensorDataType());
453        XMLGregorianCalendar timestamp = data.getTimestamp();
454        ref.setOwner(email);
455        ref.setSensorDataType(sdt);
456        ref.setTimestamp(timestamp);
457        ref.setTool(data.getTool());
458        ref.setHref(this.server.getHostName() + "sensordata/" + email + "/" +  timestamp.toString()); 
459        ref.setLastMod(data.getLastMod());
460        return ref;
461      }
462     
463    }