001    package org.hackystat.sensorbase.client;
002    
003    import java.io.StringReader;
004    import java.io.StringWriter;
005    import java.util.Date;
006    import java.util.GregorianCalendar;
007    import java.util.HashMap;
008    import java.util.Map;
009    
010    import javax.xml.bind.JAXBContext;
011    import javax.xml.bind.Marshaller;
012    import javax.xml.bind.Unmarshaller;
013    import javax.xml.datatype.XMLGregorianCalendar;
014    import javax.xml.parsers.DocumentBuilder;
015    import javax.xml.parsers.DocumentBuilderFactory;
016    import javax.xml.transform.Transformer;
017    import javax.xml.transform.TransformerFactory;
018    import javax.xml.transform.dom.DOMSource;
019    import javax.xml.transform.stream.StreamResult;
020    
021    import org.hackystat.sensorbase.resource.projects.jaxb.Invitations;
022    import org.hackystat.sensorbase.resource.projects.jaxb.MultiDayProjectSummary;
023    import org.hackystat.sensorbase.resource.projects.jaxb.Project;
024    import org.hackystat.sensorbase.resource.projects.jaxb.ProjectIndex;
025    import org.hackystat.sensorbase.resource.projects.jaxb.ProjectRef;
026    import org.hackystat.sensorbase.resource.projects.jaxb.ProjectSummary;
027    import org.hackystat.sensorbase.resource.sensorbase.SensorBaseResource;
028    import org.hackystat.sensorbase.resource.sensordata.jaxb.Property;
029    import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
030    import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
031    import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
032    import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDatas;
033    import org.hackystat.sensorbase.resource.sensordatatypes.jaxb.SensorDataType;
034    import org.hackystat.sensorbase.resource.sensordatatypes.jaxb.SensorDataTypeIndex;
035    import org.hackystat.sensorbase.resource.sensordatatypes.jaxb.SensorDataTypeRef;
036    import org.hackystat.sensorbase.resource.users.jaxb.Properties;
037    import org.hackystat.sensorbase.resource.users.jaxb.User;
038    import org.hackystat.sensorbase.resource.users.jaxb.UserIndex;
039    import org.hackystat.sensorbase.resource.users.jaxb.UserRef;
040    import org.hackystat.utilities.logger.RestletLoggerUtil;
041    import org.hackystat.utilities.tstamp.Tstamp;
042    import org.hackystat.utilities.uricache.UriCache;
043    import org.restlet.Client;
044    import org.restlet.data.ChallengeResponse;
045    import org.restlet.data.ChallengeScheme;
046    import org.restlet.data.Form;
047    import org.restlet.data.MediaType;
048    import org.restlet.data.Method;
049    import org.restlet.data.Preference;
050    import org.restlet.data.Protocol;
051    import org.restlet.data.Reference;
052    import org.restlet.data.Request;
053    import org.restlet.data.Response;
054    import org.restlet.data.Status;
055    import org.restlet.resource.Representation;
056    import org.w3c.dom.Document;
057    
058    /**
059     * Provides a high-level interface for Clients wishing to communicate with a SensorBase.
060     * 
061     * @author Philip Johnson
062     * 
063     */
064    public class SensorBaseClient {
065      
066      /** The possible responses to a Project invitation. */
067      public enum InvitationReply { ACCEPT, DECLINE }
068    
069      /** Holds the userEmail to be associated with this client. */
070      private String userEmail;
071      /** Holds the password to be associated with this client. */
072      private String password;
073      /** The SensorBase host, such as "http://localhost:9876/sensorbase". */
074      private String sensorBaseHost;
075      /** The Restlet Client instance used to communicate with the server. */
076      private Client client;
077      /** SDT JAXBContext. */
078      private static final JAXBContext sdtJAXB;
079      /** Users JAXBContext. */
080      private static final JAXBContext userJAXB;
081      /** SensorData JAXBContext. */
082      private static final JAXBContext sensordataJAXB;
083      /** Project JAXBContext. */
084      private static final JAXBContext projectJAXB;
085      /** The http authentication approach. */
086      private ChallengeScheme scheme = ChallengeScheme.HTTP_BASIC;
087      /** The preferred representation type. */
088      private Preference<MediaType> xmlMedia = new Preference<MediaType>(MediaType.TEXT_XML);
089      /** For PMD. */
090      private String sensordataUri = "sensordata/";
091      /** For PMD. */
092      private String projectsUri = "projects/";
093      /** For PMD. */
094      private String andEndTime = "&endTime=";
095      /** For PMD. */
096      //private static final String sensorbaseclient = "sensorbaseclient";
097      
098      /** To facilitate debugging of problems using this system. */
099      private boolean isTraceEnabled = false;
100    
101      /** An associated UriCache to improve responsiveness. */
102      private UriCache uriCache = null;
103      
104      /** Indicates whether or not cache is enabled. */
105      private boolean isCacheEnabled = false;
106      
107      /** Timestamp of last time we tried to contact a server and failed, since this is expensive. */
108      private static Map<String, Long> lastHostNotAvailable = new HashMap<String, Long>();
109      
110      /** The System property key used to retrieve the default timeout value in milliseconds. */
111      public static final String SENSORBASECLIENT_TIMEOUT_KEY = "sensorbaseclient.timeout";
112    
113      // JAXBContexts are thread safe, so we can share them across all instances and threads.
114      // https://jaxb.dev.java.net/guide/Performance_and_thread_safety.html
115      static {
116        try {
117          sdtJAXB = JAXBContext
118              .newInstance(org.hackystat.sensorbase.resource.sensordatatypes.jaxb.ObjectFactory.class);
119          userJAXB = JAXBContext
120              .newInstance(org.hackystat.sensorbase.resource.users.jaxb.ObjectFactory.class);
121          sensordataJAXB = JAXBContext
122              .newInstance(org.hackystat.sensorbase.resource.sensordata.jaxb.ObjectFactory.class);
123          projectJAXB = JAXBContext
124              .newInstance(org.hackystat.sensorbase.resource.projects.jaxb.ObjectFactory.class);
125        }
126        catch (Exception e) {
127          throw new RuntimeException("Couldn't create JAXB context instances.", e);
128        }
129      }
130    
131      /**
132       * Initializes a new SensorBaseClient, given the host, userEmail, and password.
133       * 
134       * @param host The host, such as 'http://localhost:9876/sensorbase'.
135       * @param email The user's email that we will use for authentication.
136       * @param password The password we will use for authentication.
137       */
138      public SensorBaseClient(String host, String email, String password) {
139        validateArg(host);
140        validateArg(email);
141        validateArg(password);
142        // Restlet logger leaves too many .lck files around, so disable it until we figure it out.
143        RestletLoggerUtil.disableLogging();
144        this.userEmail = email;
145        this.password = password;
146        this.sensorBaseHost = host;
147        if (!this.sensorBaseHost.endsWith("/")) {
148          this.sensorBaseHost = this.sensorBaseHost + "/";
149        }
150        if (this.isTraceEnabled) {
151          System.out.println("SensorBaseClient Tracing: INITIALIZE " + "host='" + host + "', email='"
152              + email + "', password='" + password + "'");
153        }
154        this.client = new Client(Protocol.HTTP);
155        setTimeout(getDefaultTimeout());
156      }
157      
158      /**
159       * Attempts to provide a timeout value for this SensorBaseClient.  
160       * @param milliseconds The number of milliseconds to wait before timing out. 
161       */
162      public final synchronized void setTimeout(int milliseconds) {
163        setClientTimeout(this.client, milliseconds);
164      }
165      
166      /**
167       * Returns the default timeout in milliseconds. 
168       * The default timeout is set to 2000 ms, but clients can change this by creating a 
169       * System property called sensorbaseclient.timeout and set it to a String indicating
170       * the number of milliseconds.  
171       * @return The default timeout.
172       */
173      private static int getDefaultTimeout() {
174        String systemTimeout = System.getProperty(SENSORBASECLIENT_TIMEOUT_KEY, "2000");
175        int timeout = 2000;
176        try {
177          timeout = Integer.parseInt(systemTimeout);
178        }
179        catch (Exception e) {
180          timeout = 2000;
181        }
182        return timeout;
183      }
184    
185      /**
186       * When passed true, future HTTP calls using this client instance will print out information on
187       * the request and response.
188       * 
189       * @param enable If true, trace output will be generated.
190       */
191      public synchronized void enableHttpTracing(boolean enable) {
192        this.isTraceEnabled = enable;
193      }
194    
195      /**
196       * Authenticates this user and password with the server.
197       * 
198       * @return This SensorBaseClient instance.
199       * @throws SensorBaseClientException If authentication is not successful.
200       */
201      public synchronized SensorBaseClient authenticate() throws SensorBaseClientException {
202        String uri = "ping?user=" + this.userEmail + "&password=" + this.password;
203        Response response = makeRequest(Method.GET, uri, null);
204        if (!response.getStatus().isSuccess()) {
205          throw new SensorBaseClientException(response.getStatus());
206        }
207        String responseString;
208        try {
209          responseString = response.getEntity().getText();
210        }
211        catch (Exception e) {
212          throw new SensorBaseClientException("Bad response", e);
213        }
214        if (!"SensorBase authenticated".equals(responseString)) {
215          throw new SensorBaseClientException("Authentication failed");
216        }
217        return this;
218      }
219    
220      /**
221       * Provides an easy way to construct SensorData instances. The keyValMap is processed and the
222       * key-value pairs are processed in the following way.
223       * <ul>
224       * <li> ["Owner", email] (if not supplied, defaults to this client's email)
225       * <li> ["Timestamp", timestamp string] (if not supplied, defaults to the current time.)
226       * <li> ["Runtime", timestamp string] (if not supplied, defaults to Timestamp.)
227       * <li> ["Resource", resource string] (if not supplied, defaults to "")
228       * <li> ["SensorDataType", sdt string] (if not supplied, defaults to "")
229       * <li> ["Tool", tool string] (if not supplied, defaults to "")
230       * <li> Any other key-val pairs are extracted and put into the SensorData properties.
231       * </ul>
232       * Throws an exception if Timestamp or Runtime are supplied but can't be parsed into an
233       * XMLGregorianCalendar instance.
234       * 
235       * @param keyValMap The map of key-value pairs corresponding to SensorData fields and properties.
236       * @return A SensorData instance.
237       * @throws SensorBaseClientException If errors occur parsing the contents of the keyValMap.
238       */
239      public synchronized SensorData makeSensorData(Map<String, String> keyValMap)
240          throws SensorBaseClientException {
241        // Begin by creating the sensor data instance.
242        SensorData data = new SensorData();
243        XMLGregorianCalendar defaultTstamp = Tstamp.makeTimestamp();
244        XMLGregorianCalendar tstamp = null;
245        try {
246          tstamp = Tstamp.makeTimestamp(takeFromMap(keyValMap, "Timestamp", defaultTstamp.toString()));
247        }
248        catch (Exception e) {
249          throw new SensorBaseClientException("Error parsing tstamp", e);
250        }
251        XMLGregorianCalendar runtime = null;
252        try {
253          runtime = Tstamp.makeTimestamp(takeFromMap(keyValMap, "Runtime", defaultTstamp.toString()));
254        }
255        catch (Exception e) {
256          throw new SensorBaseClientException("Error parsing runtime", e);
257        }
258        data.setOwner(takeFromMap(keyValMap, "Owner", userEmail));
259        data.setResource(takeFromMap(keyValMap, "Resource", ""));
260        data.setRuntime(runtime);
261        data.setSensorDataType(takeFromMap(keyValMap, "SensorDataType", ""));
262        data.setTimestamp(tstamp);
263        data.setTool(takeFromMap(keyValMap, "Tool", "unknown"));
264        // Add all remaining key-val pairs to the property list.
265        data.setProperties(new org.hackystat.sensorbase.resource.sensordata.jaxb.Properties());
266        for (Map.Entry<String, String> entry : keyValMap.entrySet()) {
267          Property property = new Property();
268          property.setKey(entry.getKey());
269          property.setValue(entry.getValue());
270          data.getProperties().getProperty().add(property);
271        }
272        return data;
273      }
274    
275      /**
276       * Returns the value associated with key in keyValMap, or the default, and also removes the
277       * mapping associated with key from the keyValMap.
278       * 
279       * @param keyValMap The map
280       * @param key The key
281       * @param defaultValue The value to return if the key has no mapping.
282       * @return The value to be used.
283       */
284      private String takeFromMap(Map<String, String> keyValMap, String key, String defaultValue) {
285        String value = (keyValMap.get(key) == null) ? defaultValue : keyValMap.get(key);
286        keyValMap.remove(key);
287        return value;
288      }
289    
290      /**
291       * Returns the index of SensorDataTypes from this server.
292       * 
293       * @return The SensorDataTypeIndex instance.
294       * @throws SensorBaseClientException If the server does not return the Index or returns an index
295       *         that cannot be marshalled into Java SensorDataTypeIndex instance.
296       */
297      public synchronized SensorDataTypeIndex getSensorDataTypeIndex() 
298                                                          throws SensorBaseClientException {
299        Response response = makeRequest(Method.GET, "sensordatatypes", null);
300        SensorDataTypeIndex index;
301        if (!response.getStatus().isSuccess()) {
302          throw new SensorBaseClientException(response.getStatus());
303        }
304        try {
305          String xmlData = response.getEntity().getText();
306          index = makeSensorDataTypeIndex(xmlData);
307        }
308        catch (Exception e) {
309          throw new SensorBaseClientException(response.getStatus(), e);
310        }
311        return index;
312      }
313    
314      /**
315       * Returns the named SensorDataType from this server.
316       * 
317       * @param sdtName The SDT name.
318       * @return The SensorDataType instance.
319       * @throws SensorBaseClientException If the server does not return the SDT or returns something
320       *         that cannot be marshalled into Java SensorDataType instance.
321       */
322      public synchronized SensorDataType getSensorDataType(String sdtName)
323          throws SensorBaseClientException {
324        Response response = makeRequest(Method.GET, "sensordatatypes/" + sdtName, null);
325        SensorDataType sdt;
326        if (!response.getStatus().isSuccess()) {
327          throw new SensorBaseClientException(response.getStatus());
328        }
329        try {
330          String xmlData = response.getEntity().getText();
331          sdt = makeSensorDataType(xmlData);
332        }
333        catch (Exception e) {
334          throw new SensorBaseClientException(response.getStatus(), e);
335        }
336        return sdt;
337      }
338    
339      /**
340       * Returns the named SensorDataType associated with the SensorDataTypeRef.
341       * 
342       * @param ref The SensorDataTypeRef instance
343       * @return The SensorDataType instance.
344       * @throws SensorBaseClientException If the server does not return the SDT or returns something
345       *         that cannot be marshalled into Java SensorDataType instance.
346       */
347      public synchronized SensorDataType getSensorDataType(SensorDataTypeRef ref)
348          throws SensorBaseClientException {
349        Response response = getUri(ref.getHref());
350        SensorDataType sdt;
351        if (!response.getStatus().isSuccess()) {
352          throw new SensorBaseClientException(response.getStatus());
353        }
354        try {
355          String xmlData = response.getEntity().getText();
356          sdt = makeSensorDataType(xmlData);
357        }
358        catch (Exception e) {
359          throw new SensorBaseClientException(response.getStatus(), e);
360        }
361        return sdt;
362      }
363    
364      /**
365       * Creates the passed SDT on the server. This is an admin-only operation.
366       * 
367       * @param sdt The SDT to create.
368       * @throws SensorBaseClientException If the user is not the admin or if there is some problem with
369       *         the SDT instance.
370       */
371      public synchronized void putSensorDataType(SensorDataType sdt) throws SensorBaseClientException {
372        try {
373          String xmlData = makeSensorDataType(sdt);
374          Representation representation = SensorBaseResource.getStringRepresentation(xmlData);
375          String uri = "sensordatatypes/" + sdt.getName();
376          Response response = makeRequest(Method.PUT, uri, representation);
377          if (!response.getStatus().isSuccess()) {
378            throw new SensorBaseClientException(response.getStatus());
379          }
380        }
381        // Allow SensorBaseClientExceptions to be thrown out of this method.
382        catch (SensorBaseClientException f) {
383          throw f;
384        }
385        // All other exceptions are caught and rethrown.
386        catch (Exception e) {
387          throw new SensorBaseClientException("Error marshalling SDT", e);
388        }
389      }
390    
391      /**
392       * Deletes the SDT given its name.
393       * 
394       * @param sdtName The name of the SDT to delete.
395       * @throws SensorBaseClientException If the server does not indicate success.
396       */
397      public synchronized void deleteSensorDataType(String sdtName) throws SensorBaseClientException {
398        Response response = makeRequest(Method.DELETE, "sensordatatypes/" + sdtName, null);
399        if (!response.getStatus().isSuccess()) {
400          throw new SensorBaseClientException(response.getStatus());
401        }
402      }
403    
404      /**
405       * Returns the index of Users from this server. This is an admin-only operation.
406       * 
407       * @return The UserIndex instance.
408       * @throws SensorBaseClientException If the server does not return the Index or returns an index
409       *         that cannot be marshalled into Java UserIndex instance.
410       */
411      public synchronized UserIndex getUserIndex() throws SensorBaseClientException {
412        Response response = makeRequest(Method.GET, "users", null);
413        UserIndex index;
414        if (!response.getStatus().isSuccess()) {
415          throw new SensorBaseClientException(response.getStatus());
416        }
417        try {
418          String xmlData = response.getEntity().getText();
419          index = makeUserIndex(xmlData);
420        }
421        catch (Exception e) {
422          throw new SensorBaseClientException(response.getStatus(), e);
423        }
424        return index;
425      }
426      
427      /**
428       * Returns the User instance associated with this SensorBaseClient instance.
429       * Requires a HTTP call to the SensorBase.
430       * @return The User instance for the user associated with this SensorBaseClient.
431       * @throws SensorBaseClientException If problems occur.
432       */
433      public synchronized User getUser() throws SensorBaseClientException {
434        return getUser(this.userEmail);
435      }
436    
437      /**
438       * Returns the named User from this server.
439       * 
440       * @param email The user email.
441       * @return The User.
442       * @throws SensorBaseClientException If the server does not return the SDT or returns something
443       *         that cannot be marshalled into Java User instance.
444       */
445      public synchronized User getUser(String email) throws SensorBaseClientException {
446        Response response = makeRequest(Method.GET, "users/" + userEmail, null);
447        User user;
448        if (!response.getStatus().isSuccess()) {
449          throw new SensorBaseClientException(response.getStatus());
450        }
451        try {
452          String xmlData = response.getEntity().getText();
453          user = makeUser(xmlData);
454        }
455        catch (Exception e) {
456          throw new SensorBaseClientException(response.getStatus(), e);
457        }
458        return user;
459      }
460    
461      /**
462       * Returns the named User associated with the UserRef.
463       * 
464       * @param ref The UserRef instance
465       * @return The User instance.
466       * @throws SensorBaseClientException If the server does not return the user or returns something
467       *         that cannot be marshalled into Java User instance.
468       */
469      public synchronized User getUser(UserRef ref) throws SensorBaseClientException {
470        Response response = getUri(ref.getHref());
471        User user;
472        if (!response.getStatus().isSuccess()) {
473          throw new SensorBaseClientException(response.getStatus());
474        }
475        try {
476          String xmlData = response.getEntity().getText();
477          user = makeUser(xmlData);
478        }
479        catch (Exception e) {
480          throw new SensorBaseClientException(response.getStatus(), e);
481        }
482        return user;
483      }
484    
485      /**
486       * Deletes the User given their email.
487       * 
488       * @param email The email of the User to delete.
489       * @throws SensorBaseClientException If the server does not indicate success.
490       */
491      public synchronized void deleteUser(String email) throws SensorBaseClientException {
492        Response response = makeRequest(Method.DELETE, "users/" + email, null);
493        if (!response.getStatus().isSuccess()) {
494          throw new SensorBaseClientException(response.getStatus());
495        }
496      }
497    
498      /**
499       * Updates the specified user's properties.
500       * 
501       * @param email The email of the User whose properties are to be deleted.
502       * @param properties The properties to post.
503       * @throws SensorBaseClientException If the server does not indicate success.
504       */
505      public synchronized void updateUserProperties(String email, Properties properties)
506          throws SensorBaseClientException {
507        String xmlData;
508        try {
509          xmlData = makeProperties(properties);
510        }
511        catch (Exception e) {
512          throw new SensorBaseClientException("Failed to marshall Properties instance.", e);
513        }
514        Representation representation = SensorBaseResource.getStringRepresentation(xmlData);
515        Response response = makeRequest(Method.POST, "users/" + email, representation);
516        if (!response.getStatus().isSuccess()) {
517          throw new SensorBaseClientException(response.getStatus());
518        }
519      }
520    
521      /**
522       * GETs the URI string and returns the Restlet Response if the server indicates success.
523       * 
524       * @param uriString The URI String, such as "http://localhost:9876/sensorbase/sensordatatypes".
525       * @return The response instance if the GET request succeeded.
526       * @throws SensorBaseClientException If the server indicates that a problem occurred.
527       */
528      public synchronized Response getUri(String uriString) throws SensorBaseClientException {
529        Reference reference = new Reference(uriString);
530        Request request = new Request(Method.GET, reference);
531        request.getClientInfo().getAcceptedMediaTypes().add(xmlMedia);
532        ChallengeResponse authentication = new ChallengeResponse(scheme, this.userEmail, this.password);
533        request.setChallengeResponse(authentication);
534        if (this.isTraceEnabled) {
535          System.out.println("SensorBaseClient Tracing: GET " + reference);
536        }
537        Response response = this.client.handle(request);
538        if (this.isTraceEnabled) {
539          Status status = response.getStatus();
540          System.out.println("  => " + status.getCode() + " " + status.getDescription());
541        }
542        if (!response.getStatus().isSuccess()) {
543          throw new SensorBaseClientException(response.getStatus());
544        }
545        return response;
546      }
547    
548      /**
549       * Returns the index of SensorData from this server. This is an admin-only operation.
550       * 
551       * @return The SensorDataIndex instance.
552       * @throws SensorBaseClientException If the server does not return the Index or returns an index
553       *         that cannot be marshalled into Java SensorDataIndex instance.
554       */
555      public synchronized SensorDataIndex getSensorDataIndex() throws SensorBaseClientException {
556        Response response = makeRequest(Method.GET, "sensordata", null);
557        SensorDataIndex index;
558        if (!response.getStatus().isSuccess()) {
559          throw new SensorBaseClientException(response.getStatus());
560        }
561        try {
562          String xmlData = response.getEntity().getText();
563          index = makeSensorDataIndex(xmlData);
564        }
565        catch (Exception e) {
566          throw new SensorBaseClientException(response.getStatus(), e);
567        }
568        setSensorDataIndexLastMod(index);
569        return index;
570      }
571    
572      /**
573       * Computes the lastMod value for this index. Iterates through the individual refs to find their
574       * lastMod values, and stores the most recent lastMod value as the result. If the index is empty,
575       * then a default lastMod of 1000-01-01 is returned. We hope that indexes are cached so that this
576       * is not done a lot.
577       * 
578       * @param index The index.
579       * @throws SensorBaseClientException If problems occur parsing the lastMod fields.
580       */
581      private void setSensorDataIndexLastMod(SensorDataIndex index) throws SensorBaseClientException {
582        try {
583          index.setLastMod(Tstamp.makeTimestamp("1000-01-01"));
584          for (SensorDataRef ref : index.getSensorDataRef()) {
585            XMLGregorianCalendar lastMod = ref.getLastMod();
586            if ((!(lastMod == null)) && Tstamp.greaterThan(lastMod, index.getLastMod())) {
587              index.setLastMod(lastMod);
588            }
589          }
590        }
591        catch (Exception e) {
592          throw new SensorBaseClientException("Error setting LastMod for index: " + index, e);
593        }
594      }
595    
596      /**
597       * Returns the index of SensorData for this user from this server.
598       * 
599       * @param email The user email.
600       * @return The SensorDataIndex instance.
601       * @throws SensorBaseClientException If the server does not return the Index or returns an index
602       *         that cannot be marshalled into Java SensorDataIndex instance.
603       */
604      public synchronized SensorDataIndex getSensorDataIndex(String email)
605          throws SensorBaseClientException {
606        Response response = makeRequest(Method.GET, sensordataUri + email, null);
607        SensorDataIndex index;
608        if (!response.getStatus().isSuccess()) {
609          throw new SensorBaseClientException(response.getStatus());
610        }
611        try {
612          String xmlData = response.getEntity().getText();
613          index = makeSensorDataIndex(xmlData);
614        }
615        catch (Exception e) {
616          throw new SensorBaseClientException(response.getStatus(), e);
617        }
618        setSensorDataIndexLastMod(index);
619        return index;
620      }
621    
622      /**
623       * Returns the index of SensorData for this user from this server with the specified SDT.
624       * 
625       * @param email The user email.
626       * @param sdtName The name of the SDT whose SensorData is to be returned.
627       * @return The SensorDataIndex instance.
628       * @throws SensorBaseClientException If the server does not return the Index or returns an index
629       *         that cannot be marshalled into Java SensorDataIndex instance.
630       */
631      public synchronized SensorDataIndex getSensorDataIndex(String email, String sdtName)
632          throws SensorBaseClientException {
633        Response response = makeRequest(Method.GET, sensordataUri + email + "?sdt=" + sdtName, null);
634        SensorDataIndex index;
635        if (!response.getStatus().isSuccess()) {
636          throw new SensorBaseClientException(response.getStatus());
637        }
638        try {
639          String xmlData = response.getEntity().getText();
640          index = makeSensorDataIndex(xmlData);
641        }
642        catch (Exception e) {
643          throw new SensorBaseClientException(response.getStatus(), e);
644        }
645        return index;
646      }
647    
648      /**
649       * Returns an index to SensorData for the specified user of all sensor data that have arrived at
650       * the server since the specified start and end times. Uses the LastMod field to determine what
651       * data will be retrieved.
652       * <p>
653       * Note that data could be sent recently with a Timestamp (as opposed to LastMod field) from far
654       * back in the past, and the index will include references to such data. This method thus differs
655       * from all other SensorDataIndex-returning methods, because the others compare passed timestamp
656       * values to the Timestamp associated with the moment at which a sensor data instance is created,
657       * not the moment it ends up being received by the server.
658       * <p>
659       * This method is intended for use by user interface facilities such as the SensorDataViewer that
660       * wish to monitor the arrival of data at the SensorBase.
661       * 
662       * @param email The user email.
663       * @param lastModStartTime A timestamp used to determine the start time of data to get.
664       * @param lastModEndTime A timestamp used to determine the end time of data to get.
665       * @return The SensorDataIndex instance.
666       * @throws SensorBaseClientException If the server does not return the Index or returns an index
667       *         that cannot be marshalled into Java SensorDataIndex instance.
668       */
669      public synchronized SensorDataIndex getSensorDataIndexLastMod(String email,
670          XMLGregorianCalendar lastModStartTime, XMLGregorianCalendar lastModEndTime)
671          throws SensorBaseClientException {
672        Response response = makeRequest(Method.GET, sensordataUri + email + "?lastModStartTime="
673            + lastModStartTime + "&lastModEndTime=" + lastModEndTime, null);
674        SensorDataIndex index;
675        if (!response.getStatus().isSuccess()) {
676          throw new SensorBaseClientException(response.getStatus());
677        }
678        try {
679          String xmlData = response.getEntity().getText();
680          index = makeSensorDataIndex(xmlData);
681        }
682        catch (Exception e) {
683          throw new SensorBaseClientException(response.getStatus(), e);
684        }
685        return index;
686      }
687    
688      /**
689       * Returns the SensorData for this user from this server with the specified timestamp.
690       * Uses the cache if enabled.
691       * 
692       * @param email The user email.
693       * @param timestamp The timestamp.
694       * @return The SensorData instance.
695       * @throws SensorBaseClientException If the server does not return the success code or returns a
696       *         String that cannot be marshalled into Java SensorData instance.
697       */
698      public synchronized SensorData getSensorData(String email, XMLGregorianCalendar timestamp)
699          throws SensorBaseClientException {
700        SensorData data;
701        String uri = sensordataUri + email + "/" + timestamp;
702        // Check the cache, and return the sensor data instance from it if available. 
703        if (this.isCacheEnabled) {
704          data = (SensorData)this.uriCache.get(uri);
705          if (data != null) {
706            return data;
707          }
708        }
709        // If not in the cache, request it from the sensorbase service.
710        Response response = makeRequest(Method.GET, uri, null);
711        if (!response.getStatus().isSuccess()) {
712          throw new SensorBaseClientException(response.getStatus());
713        }
714        try {
715          String xmlData = response.getEntity().getText();
716          data = makeSensorData(xmlData);
717          // Add it to the cache if we're using one.
718          if (this.isCacheEnabled) {
719            this.uriCache.put(uri, data);
720          }
721        }
722        catch (Exception e) {
723          throw new SensorBaseClientException(response.getStatus(), e);
724        }
725        return data;
726      }
727    
728      /**
729       * Returns the SensorData for this user from this server given the passed uriString suffix.
730       * 
731       * @param uriString A string that when prefixed with the sensorbase host should return 
732       * a SensorData instance in XML format.  A string such as "http://localhost:9876/sensorbase"
733       * will be prefixed to the uriString. 
734       * @return The SensorData instance.
735       * @throws SensorBaseClientException If the server does not return the success code or returns a
736       *         String that cannot be marshalled into Java SensorData instance.
737       */
738      public synchronized SensorData getSensorData(String uriString) throws SensorBaseClientException {
739        SensorData data;
740        // Check the cache, and return the sensor data instance from it if available. 
741        if (this.isCacheEnabled) {
742          data = (SensorData)this.uriCache.get(uriString);
743          if (data != null) {
744            return data;
745          }
746        }
747        // Otherwise get it from the sensorbase.
748        Response response = makeRequest(Method.GET, uriString, null);
749        if (!response.getStatus().isSuccess()) {
750          throw new SensorBaseClientException(response.getStatus());
751        }
752        try {
753          String xmlData = response.getEntity().getText();
754          data = makeSensorData(xmlData);
755          // Add it to the cache if we're using one.
756          if (this.isCacheEnabled) {
757            this.uriCache.put(uriString, data);
758          }
759        }
760        catch (Exception e) {
761          throw new SensorBaseClientException(response.getStatus(), e);
762        }
763        return data;
764      }
765    
766      /**
767       * Returns the named SensorData associated with the SensorDataRef.
768       * 
769       * @param ref The SensorDataRef instance
770       * @return The SensorData instance.
771       * @throws SensorBaseClientException If the server does not return the data or returns something
772       *         that cannot be marshalled into Java SensorData instance.
773       */
774      public synchronized SensorData getSensorData(SensorDataRef ref) throws SensorBaseClientException {
775        SensorData data;
776        String uri = ref.getHref();
777        // Check the cache, and return the sensor data instance from it if available. 
778        if (this.isCacheEnabled) {
779          data = (SensorData)this.uriCache.get(uri);
780          if (data != null) {
781            return data;
782          }
783        }
784        Response response = getUri(uri);
785        if (!response.getStatus().isSuccess()) {
786          throw new SensorBaseClientException(response.getStatus());
787        }
788        try {
789          String xmlData = response.getEntity().getText();
790          data = makeSensorData(xmlData);
791          // Add it to the cache if we're using one.
792          if (this.isCacheEnabled) {
793            this.uriCache.put(uri, data);
794          }
795        }
796        catch (Exception e) {
797          throw new SensorBaseClientException(response.getStatus(), e);
798        }
799        return data;
800      }
801    
802    
803      /**
804       * Creates the passed SensorData on the server.
805       * 
806       * @param data The sensor data to create.
807       * @throws SensorBaseClientException If problems occur posting this data.
808       */
809      public synchronized void putSensorData(SensorData data) throws SensorBaseClientException {
810        try {
811          String xmlData = makeSensorData(data);
812          Representation representation = SensorBaseResource.getStringRepresentation(xmlData);
813          String uri = sensordataUri + data.getOwner() + "/" + data.getTimestamp();
814          Response response = makeRequest(Method.PUT, uri, representation);
815          if (!response.getStatus().isSuccess()) {
816            throw new SensorBaseClientException(response.getStatus());
817          }
818        }
819        // Allow SensorBaseClientExceptions to be thrown out of this method.
820        catch (SensorBaseClientException f) {
821          throw f;
822        }
823        // All other exceptions are caught and rethrown.
824        catch (Exception e) {
825          throw new SensorBaseClientException("Error marshalling sensor data", e);
826        }
827      }
828    
829      /**
830       * Creates the passed batch of SensorData on the server. Assumes that all of them have the same
831       * owner field, and that batch is non-empty.
832       * 
833       * @param data The sensor data batch to create, represented as a SensorDatas instance.
834       * @throws SensorBaseClientException If problems occur posting this data.
835       */
836      public synchronized void putSensorDataBatch(SensorDatas data) throws SensorBaseClientException {
837        try {
838          String xmlData = makeSensorDatas(data);
839          String owner = data.getSensorData().get(0).getOwner();
840          Representation representation = SensorBaseResource.getStringRepresentation(xmlData);
841          String uri = sensordataUri + owner + "/batch";
842          Response response = makeRequest(Method.PUT, uri, representation);
843          if (!response.getStatus().isSuccess()) {
844            throw new SensorBaseClientException(response.getStatus());
845          }
846        }
847        // Allow SensorBaseClientExceptions to be thrown out of this method.
848        catch (SensorBaseClientException f) {
849          throw f;
850        }
851        // All other exceptions are caught and rethrown.
852        catch (Exception e) {
853          throw new SensorBaseClientException("Error marshalling batch sensor data", e);
854        }
855      }
856    
857      /**
858       * Ensures that the SensorData instance with the specified user and tstamp is not on the server.
859       * Returns success even if the SensorData instance did not exist on the server.
860       * 
861       * @param email The email of the User.
862       * @param timestamp The timestamp of the sensor data.
863       * @throws SensorBaseClientException If the server does not indicate success.
864       */
865      public synchronized void deleteSensorData(String email, XMLGregorianCalendar timestamp)
866          throws SensorBaseClientException {
867        Response response = makeRequest(Method.DELETE, sensordataUri + email + "/" + timestamp, null);
868        if (!response.getStatus().isSuccess()) {
869          throw new SensorBaseClientException(response.getStatus());
870        }
871      }
872    
873      /**
874       * Deletes all sensor data associated with the specified user. 
875       * Note that the user must be a test user.
876       * Returns success even if the user had no sensor data.  
877       * 
878       * @param email The email of the User.
879       * @throws SensorBaseClientException If the server does not indicate success.
880       */
881      public synchronized void deleteSensorData(String email) throws SensorBaseClientException {
882        Response response = makeRequest(Method.DELETE, sensordataUri + email, null);
883        if (!response.getStatus().isSuccess()) {
884          throw new SensorBaseClientException(response.getStatus());
885        }
886      }
887    
888      /**
889       * Returns the index of all Projects from this server. This is an admin-only operation.
890       * 
891       * @return The ProjectIndex instance.
892       * @throws SensorBaseClientException If the server does not return the Index or returns an index
893       *         that cannot be marshalled into Java ProjectIndex instance.
894       */
895      public synchronized ProjectIndex getProjectIndex() throws SensorBaseClientException {
896        Response response = makeRequest(Method.GET, projectsUri, null);
897        ProjectIndex index;
898        if (!response.getStatus().isSuccess()) {
899          throw new SensorBaseClientException(response.getStatus());
900        }
901        try {
902          String xmlData = response.getEntity().getText();
903          index = makeProjectIndex(xmlData);
904        }
905        catch (Exception e) {
906          throw new SensorBaseClientException(response.getStatus(), e);
907        }
908        return index;
909      }
910    
911      /**
912       * Returns the index of all Projects from this server associated with this user.
913       * This includes the projects that this user owns, that this user is a member of,
914       * and that this user has been invited to participate in as a member (but has not
915       * yet accepted or declined.)
916       * 
917       * @param email The user email.
918       * @return The ProjectIndex instance.
919       * @throws SensorBaseClientException If the server does not return the Index or returns an index
920       *         that cannot be marshalled into Java ProjectIndex instance.
921       */
922      public synchronized ProjectIndex getProjectIndex(String email)
923          throws SensorBaseClientException {
924        Response response = makeRequest(Method.GET, projectsUri + email, null);
925        ProjectIndex index;
926        if (!response.getStatus().isSuccess()) {
927          throw new SensorBaseClientException(response.getStatus());
928        }
929        try {
930          String xmlData = response.getEntity().getText();
931          index = makeProjectIndex(xmlData);
932        }
933        catch (Exception e) {
934          throw new SensorBaseClientException(response.getStatus(), e);
935        }
936        return index;
937      }
938      
939    
940      /**
941       * Returns the Project from this server.
942       * 
943       * @param email The user email.
944       * @param projectName The project name.
945       * @return The Project
946       * @throws SensorBaseClientException If the server does not return success or returns something
947       *         that cannot be marshalled into Java Project instance.
948       */
949      public synchronized Project getProject(String email, String projectName)
950          throws SensorBaseClientException {
951        Response response = makeRequest(Method.GET, projectsUri + email + "/" + projectName, null);
952        Project project;
953        if (!response.getStatus().isSuccess()) {
954          throw new SensorBaseClientException(response.getStatus());
955        }
956        try {
957          String xmlData = response.getEntity().getText();
958          project = makeProject(xmlData);
959        }
960        catch (Exception e) {
961          throw new SensorBaseClientException(response.getStatus(), e);
962        }
963        return project;
964      }
965      
966      /**
967       * True if the current user is the owner, member, or spectator of the specified project.
968       * If caching is enabled, then we cache the retrieved project definition for 
969       * 3 minutes when this client is an owner, member, or spectator so that subsequent retrievals
970       * don't require an http access.  
971       * 
972       * @param email The email address of the project owner. 
973       * @param projectName The name of the project. 
974       * @return True if this user is now a member of the 
975       */
976      public synchronized boolean inProject(String email, String projectName) {
977        String key = email + '/' + projectName;
978        double maxLifeHours = 0.05;
979        if (this.isCacheEnabled && this.uriCache.get(key) != null) {
980          return true;
981        }
982        // otherwise we attempt to retrieve the project definition.
983        try {
984          Project project = getProject(email, projectName);
985          boolean success = isInProject(project);
986          if (this.isCacheEnabled && success) {
987            this.uriCache.put(key, project, maxLifeHours);
988          }
989          return success;
990        }
991        catch (Exception e) {
992          return false;
993        }
994      }
995      
996      /**
997       * Returns true if this client's user is the owner or member or spectator in the project.
998       * @param project The project.
999       * @return True if the client is an owner, member, or spectator. 
1000       */
1001      private boolean isInProject(Project project) {
1002        if (project.getOwner().equals(this.userEmail)) {
1003          return true;
1004        }
1005        for (String member : project.getMembers().getMember()) {
1006          if (member.equals(this.userEmail)) {
1007            return true;
1008          }
1009         }
1010        for (String spectator : project.getSpectators().getSpectator()) {
1011          if (spectator.equals(this.userEmail)) {
1012            return true;
1013          }
1014         }
1015        return false;
1016      }
1017      
1018      /**
1019       * Invites the user indicated via their email address to the named project owned by this user.
1020       * Has no effect if the user is already an invited member.
1021       * Returns the updated project representation. 
1022       * 
1023       * @param email The user to be invited to this project.  
1024       * @param projectName The project name.
1025       * @return The project representation as a result of the invitation.
1026       * @throws SensorBaseClientException If the server does not return success.
1027       */
1028      public synchronized Project invite(String email, String projectName) 
1029      throws SensorBaseClientException {
1030        
1031        // First, get the project representation.
1032        Project project = this.getProject(this.userEmail, projectName);
1033        // Make sure that the Invitations instance is not null.
1034        if (project.getInvitations() == null) {
1035          project.setInvitations(new Invitations());
1036        }
1037        // If user hasn't already been invited, then invite them.
1038        if (!project.getInvitations().getInvitation().contains(email)) {
1039          project.getInvitations().getInvitation().add(email);
1040          this.putProject(project);
1041        }
1042        // Return the updated project representation from the server. 
1043        return this.getProject(this.userEmail, projectName);
1044      }
1045      
1046      /**
1047       * Accepts the invitation to be a member of the project owned by owner.
1048       * 
1049       * @param owner The owner of the project that this user has been invited into
1050       * @param projectName the name of the project.
1051       * @param reply The reply, either ACCEPT or DECLINE.
1052       * @throws SensorBaseClientException If the server returns an error from this acceptance, for
1053       * example if the user has not actually been invited.
1054       */
1055      public synchronized void reply(String owner, String projectName, InvitationReply reply) 
1056      throws SensorBaseClientException {
1057        Response response = makeRequest(Method.POST, 
1058            projectsUri + owner + "/" + projectName + "/invitation/" + 
1059            reply.toString().toLowerCase(), null);
1060        if (!response.getStatus().isSuccess()) {
1061          throw new SensorBaseClientException(response.getStatus());
1062        }
1063      }
1064      
1065      /**
1066       * Renames the project.
1067       * 
1068       * @param owner The owner of the project to be renamed.
1069       * @param projectName The current name of the project.
1070       * @param newProjectName The new name of the project. 
1071       * @throws SensorBaseClientException If the server returns an error from this acceptance, for
1072       * example if the new project name is not unique. 
1073       */
1074      public synchronized void renameProject(String owner, String projectName, String newProjectName) 
1075      throws SensorBaseClientException {
1076        Response response = makeRequest(Method.POST, 
1077            projectsUri + owner + "/" + projectName + "/rename/" + newProjectName, null); 
1078        if (!response.getStatus().isSuccess()) {
1079          throw new SensorBaseClientException(response.getStatus());
1080        }
1081      }
1082      
1083      
1084    
1085      /**
1086       * Returns the named Project associated with the ProjectRef.
1087       * 
1088       * @param ref The ProjectRef instance
1089       * @return The Project instance.
1090       * @throws SensorBaseClientException If the server does not return the user or returns something
1091       *         that cannot be marshalled into Java Project instance.
1092       */
1093      public synchronized Project getProject(ProjectRef ref) throws SensorBaseClientException {
1094        Response response = getUri(ref.getHref());
1095        Project project;
1096        if (!response.getStatus().isSuccess()) {
1097          throw new SensorBaseClientException(response.getStatus());
1098        }
1099        try {
1100          String xmlData = response.getEntity().getText();
1101          project = makeProject(xmlData);
1102        }
1103        catch (Exception e) {
1104          throw new SensorBaseClientException(response.getStatus(), e);
1105        }
1106        return project;
1107      }
1108    
1109      /**
1110       * Returns a SensorDataIndex representing all the SensorData for this Project.
1111       * 
1112       * @param owner The project owner's email.
1113       * @param projectName The project name.
1114       * @return A SensorDataIndex.
1115       * @throws SensorBaseClientException If the server does not return success or returns something
1116       *         that cannot be marshalled into Java SensorDataIndex instance.
1117       */
1118      public synchronized SensorDataIndex getProjectSensorData(String owner, String projectName)
1119          throws SensorBaseClientException {
1120        Response response = makeRequest(Method.GET, projectsUri + owner + "/" + projectName
1121            + "/sensordata", null);
1122        SensorDataIndex index;
1123        if (!response.getStatus().isSuccess()) {
1124          throw new SensorBaseClientException(response.getStatus());
1125        }
1126        try {
1127          String xmlData = response.getEntity().getText();
1128          index = makeSensorDataIndex(xmlData);
1129        }
1130        catch (Exception e) {
1131          throw new SensorBaseClientException(response.getStatus(), e);
1132        }
1133        return index;
1134      }
1135      
1136     
1137    
1138      /**
1139       * Returns a SensorDataIndex representing the SensorData for the Project during the time interval.
1140       * 
1141       * @param owner The project owner's email.
1142       * @param projectName The project name.
1143       * @param startTime The start time.
1144       * @param endTime The end time.
1145       * @return A SensorDataIndex.
1146       * @throws SensorBaseClientException If the server does not return success or returns something
1147       *         that cannot be marshalled into Java SensorDataIndex instance.
1148       */
1149      public synchronized SensorDataIndex getProjectSensorData(String owner, String projectName,
1150          XMLGregorianCalendar startTime, XMLGregorianCalendar endTime)
1151          throws SensorBaseClientException {
1152        Response response = makeRequest(Method.GET, projectsUri + owner + "/" + projectName
1153            + "/sensordata?startTime=" + startTime + andEndTime + endTime, null);
1154        SensorDataIndex index;
1155        if (!response.getStatus().isSuccess()) {
1156          throw new SensorBaseClientException(response.getStatus());
1157        }
1158        try {
1159          String xmlData = response.getEntity().getText();
1160          index = makeSensorDataIndex(xmlData);
1161        }
1162        catch (Exception e) {
1163          throw new SensorBaseClientException(response.getStatus(), e);
1164        }
1165        return index;
1166      }
1167      
1168      /**
1169       * Returns a SensorDataIndex representing the SensorData with the given SDT for the Project 
1170       * during the time interval.
1171       * 
1172       * @param owner The project owner's email.
1173       * @param projectName The project name.
1174       * @param startTime The start time.
1175       * @param endTime The end time.
1176       * @param sdt The SensorDataType.
1177       * @return A SensorDataIndex.
1178       * @throws SensorBaseClientException If the server does not return success or returns something
1179       *         that cannot be marshalled into Java SensorDataIndex instance.
1180       */
1181      public synchronized SensorDataIndex getProjectSensorData(String owner, String projectName,
1182          XMLGregorianCalendar startTime, XMLGregorianCalendar endTime, String sdt)
1183          throws SensorBaseClientException {
1184        Response response = makeRequest(Method.GET, projectsUri + owner + "/" + projectName
1185            + "/sensordata?sdt=" + sdt + "&startTime=" + startTime + andEndTime + endTime, null);
1186        SensorDataIndex index;
1187        if (!response.getStatus().isSuccess()) {
1188          throw new SensorBaseClientException(response.getStatus());
1189        }
1190        try {
1191          String xmlData = response.getEntity().getText();
1192          index = makeSensorDataIndex(xmlData);
1193        }
1194        catch (Exception e) {
1195          throw new SensorBaseClientException(response.getStatus(), e);
1196        }
1197        return index;
1198      }
1199      
1200      /**
1201       * Returns a SensorDataIndex representing the SensorData with the given SDT for the Project 
1202       * during the time interval.
1203       * 
1204       * @param owner The project owner's email.
1205       * @param projectName The project name.
1206       * @param startTime The start time.
1207       * @param endTime The end time.
1208       * @param sdt The SensorDataType.
1209       * @param tool The tool that generated this sensor data of the given type.
1210       * @return A SensorDataIndex.
1211       * @throws SensorBaseClientException If the server does not return success or returns something
1212       *         that cannot be marshalled into Java SensorDataIndex instance.
1213       */
1214      public synchronized SensorDataIndex getProjectSensorData(String owner, String projectName,
1215          XMLGregorianCalendar startTime, XMLGregorianCalendar endTime, String sdt, String tool)
1216          throws SensorBaseClientException {
1217        Response response = makeRequest(Method.GET, projectsUri + owner + "/" + projectName
1218            + "/sensordata?sdt=" + sdt + "&startTime=" + startTime + andEndTime + endTime +
1219            "&tool=" + tool, null);
1220        SensorDataIndex index;
1221        if (!response.getStatus().isSuccess()) {
1222          throw new SensorBaseClientException(response.getStatus());
1223        }
1224        try {
1225          String xmlData = response.getEntity().getText();
1226          index = makeSensorDataIndex(xmlData);
1227        }
1228        catch (Exception e) {
1229          throw new SensorBaseClientException(response.getStatus(), e);
1230        }
1231        return index;
1232      }
1233      
1234      /**
1235       * Returns a SensorDataIndex representing the SensorData with the startIndex and 
1236       * maxInstances for the Project during the time interval.
1237       * The startIndex must be non-negative, and is zero-based.  The maxInstances must be non-negative.
1238       * If startIndex is greater than the number of instances in the time interval, then an 
1239       * empty SensorDataIndex is returned.  
1240       * 
1241       * @param owner The project owner's email.
1242       * @param projectName The project name.
1243       * @param startTime The start time.
1244       * @param endTime The end time.
1245       * @param startIndex The zero-based index to the first Sensor Data instance to be returned in 
1246       * the time interval, when the instances are all ordered by timestamp.
1247       * @param maxInstances The maximum number of instances to return.
1248       * @return A SensorDataIndex.
1249       * @throws SensorBaseClientException If the server does not return success or returns something
1250       *         that cannot be marshalled into Java SensorDataIndex instance.
1251       */
1252      public synchronized SensorDataIndex getProjectSensorData(String owner, String projectName,
1253          XMLGregorianCalendar startTime, XMLGregorianCalendar endTime, int startIndex, 
1254          int maxInstances) throws SensorBaseClientException {
1255        Response response = makeRequest(Method.GET, projectsUri + owner + "/" + projectName
1256            + "/sensordata?startTime=" + startTime + andEndTime + endTime + "&startIndex="
1257            + startIndex + "&maxInstances=" + maxInstances, null);
1258        SensorDataIndex index;
1259        if (!response.getStatus().isSuccess()) {
1260          throw new SensorBaseClientException(response.getStatus());
1261        }
1262        try {
1263          String xmlData = response.getEntity().getText();
1264          index = makeSensorDataIndex(xmlData);
1265        }
1266        catch (Exception e) {
1267          throw new SensorBaseClientException(response.getStatus(), e);
1268        }
1269        return index;
1270      }
1271      
1272      /**
1273       * Returns a SensorDataIndex containing a snapshot of the sensor data for the given project and
1274       * sdt during the specified time interval.  A "snapshot" is the set of sensor data with the most
1275       * recent runtime value during that time interval.
1276       * @param owner The owner of the project.
1277       * @param projectName The project name.
1278       * @param startTime The start time.
1279       * @param endTime The end time.
1280       * @param sdt The sdt of interest for the sensor data.
1281       * @return The SensorDataIndex containing the "snapshot".
1282       * @throws SensorBaseClientException If problems occur.
1283       */
1284      public synchronized SensorDataIndex getProjectSensorDataSnapshot(String owner, String projectName,
1285          XMLGregorianCalendar startTime, XMLGregorianCalendar endTime, String sdt) 
1286      throws SensorBaseClientException {
1287        Response response = makeRequest(Method.GET, projectsUri + owner + "/" + projectName
1288            + "/snapshot?startTime=" + startTime + andEndTime + endTime + "&sdt=" + sdt, null);
1289        SensorDataIndex index;
1290        if (!response.getStatus().isSuccess()) {
1291          throw new SensorBaseClientException(response.getStatus());
1292        }
1293        try {
1294          String xmlData = response.getEntity().getText();
1295          index = makeSensorDataIndex(xmlData);
1296        }
1297        catch (Exception e) {
1298          throw new SensorBaseClientException(response.getStatus(), e);
1299        }
1300        return index;
1301      }
1302      
1303      /**
1304       * Returns a SensorDataIndex containing a snapshot of the sensor data for the given project, sdt
1305       * and tool during the specified time interval.  A "snapshot" is the set of sensor data with the 
1306       * most recent runtime value during that time interval.
1307       * @param owner The owner of the project.
1308       * @param projectName The project name.
1309       * @param startTime The start time.
1310       * @param endTime The end time.
1311       * @param sdt The sdt of interest for the sensor data.
1312       * @param tool The tool of interest for the sensor data. 
1313       * @return The SensorDataIndex containing the "snapshot".
1314       * @throws SensorBaseClientException If problems occur.
1315       */
1316      public synchronized SensorDataIndex getProjectSensorDataSnapshot(String owner, String projectName,
1317          XMLGregorianCalendar startTime, XMLGregorianCalendar endTime, String sdt, String tool) 
1318      throws SensorBaseClientException {
1319        Response response = makeRequest(Method.GET, projectsUri + owner + "/" + projectName
1320            + "/snapshot?startTime=" + startTime + andEndTime + endTime + "&sdt=" + sdt +
1321            "&tool=" + tool, null);
1322        SensorDataIndex index;
1323        if (!response.getStatus().isSuccess()) {
1324          throw new SensorBaseClientException(response.getStatus());
1325        }
1326        try {
1327          String xmlData = response.getEntity().getText();
1328          index = makeSensorDataIndex(xmlData);
1329        }
1330        catch (Exception e) {
1331          throw new SensorBaseClientException(response.getStatus(), e);
1332        }
1333        return index;
1334      }
1335      
1336      /**
1337       * Returns a ProjectSummary representing a summary of the number of sensor data instances of
1338       * each type for the given interval.
1339       * @param owner The project owner.
1340       * @param projectName The project name.
1341       * @param startTime The start time. 
1342       * @param endTime The end time.
1343       * @return A ProjectSummary.
1344       * @throws SensorBaseClientException If problems occur.
1345       */
1346      public synchronized ProjectSummary getProjectSummary(String owner, String projectName, 
1347          XMLGregorianCalendar startTime, XMLGregorianCalendar endTime) 
1348      throws  SensorBaseClientException {
1349        Response response = makeRequest(Method.GET, projectsUri + owner + "/" + projectName 
1350            + "/summary?startTime=" + startTime + andEndTime + endTime, null);
1351        
1352        ProjectSummary summary;
1353        if (!response.getStatus().isSuccess()) {
1354          throw new SensorBaseClientException(response.getStatus());
1355        }
1356        try {
1357          String xmlData = response.getEntity().getText();
1358          summary = makeProjectSummary(xmlData);
1359        }
1360        catch (Exception e) {
1361          throw new SensorBaseClientException(response.getStatus(), e);
1362        }
1363        return summary;
1364      }
1365    
1366      /**
1367       * Returns a MultiDayProjectSummary for the specified interval of days. 
1368       * @param owner The project owner. 
1369       * @param projectName The project name. 
1370       * @param startTime The start day. 
1371       * @param numDays The number of days, each one getting a ProjectSummary instance. 
1372       * @return The MulitDayProjectSummary instance. 
1373       * @throws SensorBaseClientException If problems occur. 
1374       */
1375      public synchronized MultiDayProjectSummary getMultiDayProjectSummary(String owner, 
1376          String projectName, XMLGregorianCalendar startTime, int numDays) 
1377      throws  SensorBaseClientException {
1378        Response response = makeRequest(Method.GET, projectsUri + owner + "/" + projectName 
1379            + "/summary?startTime=" + startTime + "&numDays=" + numDays, null);
1380        
1381        MultiDayProjectSummary summary;
1382        if (!response.getStatus().isSuccess()) {
1383          throw new SensorBaseClientException(response.getStatus());
1384        }
1385        try {
1386          String xmlData = response.getEntity().getText();
1387          summary = makeMultiDayProjectSummary(xmlData);
1388        }
1389        catch (Exception e) {
1390          throw new SensorBaseClientException(response.getStatus(), e);
1391        }
1392        return summary;
1393      }
1394      
1395      /**
1396       * Returns a MultiDayProjectSummary for the specified project, year, and month (zero based).
1397       * @param owner The project owner. 
1398       * @param projectName The project name. 
1399       * @param year The year. 
1400       * @param month The month (zero based). 
1401       * @return A MultiDayProjectSummary for the specified month. 
1402       * @throws SensorBaseClientException If problems occur. 
1403       */
1404      public synchronized MultiDayProjectSummary getMonthProjectSummary (String owner, 
1405          String projectName, int year, int month) throws SensorBaseClientException {
1406        XMLGregorianCalendar startDay = Tstamp.makeTimestamp();
1407        startDay.setDay(1);
1408        startDay.setMonth(month);
1409        startDay.setYear(year);
1410        int numDays = startDay.toGregorianCalendar().getActualMaximum(GregorianCalendar.DAY_OF_MONTH);
1411        return getMultiDayProjectSummary(owner, projectName, startDay, numDays);
1412      }
1413    
1414      /**
1415       * Creates the passed Project on the server.
1416       * 
1417       * @param project The project to create.
1418       * @throws SensorBaseClientException If problems occur posting this data.
1419       */
1420      public synchronized void putProject(Project project) throws SensorBaseClientException {
1421        try {
1422          String xmlData = makeProject(project);
1423          Representation representation = SensorBaseResource.getStringRepresentation(xmlData);
1424          String uri = projectsUri + project.getOwner() + "/" + project.getName();
1425          Response response = makeRequest(Method.PUT, uri, representation);
1426          if (!response.getStatus().isSuccess()) {
1427            throw new SensorBaseClientException(response.getStatus());
1428          }
1429        }
1430        // Allow SensorBaseClientExceptions to be thrown out of this method.
1431        catch (SensorBaseClientException f) {
1432          throw f;
1433        }
1434        // All other exceptions are caught and rethrown.
1435        catch (Exception e) {
1436          throw new SensorBaseClientException("Error marshalling sensor data", e);
1437        }
1438      }
1439    
1440      /**
1441       * Deletes the Project given its owner and projectName.
1442       * 
1443       * @param email The email of the project owner.
1444       * @param projectName The project name.
1445       * @throws SensorBaseClientException If the server does not indicate success.
1446       */
1447      public synchronized void deleteProject(String email, String projectName)
1448          throws SensorBaseClientException {
1449        Response response = makeRequest(Method.DELETE, projectsUri + email + "/" + projectName, null);
1450        if (!response.getStatus().isSuccess()) {
1451          throw new SensorBaseClientException(response.getStatus());
1452        }
1453      }
1454    
1455      /**
1456       * Registers the given user email with the given SensorBase.
1457       * Timeout is set to 5 seconds. 
1458       * 
1459       * @param host The host name, such as "http://localhost:9876/sensorbase".
1460       * @param email The user email.
1461       * @throws SensorBaseClientException If problems occur during registration.
1462       */
1463      public static void registerUser(String host, String email) throws SensorBaseClientException {
1464        RestletLoggerUtil.disableLogging();
1465        String registerUri = host.endsWith("/") ? host + "register" : host + "/register";
1466        Request request = new Request();
1467        request.setResourceRef(registerUri);
1468        request.setMethod(Method.POST);
1469        Form form = new Form();
1470        form.add("email", email);
1471        request.setEntity(form.getWebRepresentation());
1472        Client client = new Client(Protocol.HTTP);
1473        setClientTimeout(client, getDefaultTimeout());
1474        Response response = client.handle(request);
1475        if (!response.getStatus().isSuccess()) {
1476          throw new SensorBaseClientException(response.getStatus());
1477        }
1478      }
1479      
1480      /**
1481       * Sets the lastHostNotAvailable timestamp for the passed host.
1482       * @param host The host that was determined to be not available.
1483       */
1484      private static void setLastHostNotAvailable(String host) {
1485        lastHostNotAvailable.put(host, (new Date()).getTime());
1486      }
1487      
1488      /**
1489       * Gets the lastHostNotAvailable timestamp associated with host.
1490       * Returns 0 if there is no lastHostNotAvailable timestamp.
1491       * @param host The host whose lastNotAvailable timestamp is to be retrieved.
1492       * @return The timestamp.
1493       */
1494      private static long getLastHostNotAvailable(String host) {
1495        Long time = lastHostNotAvailable.get(host);
1496        return (time == null) ? 0 : time;
1497      }
1498    
1499      /**
1500       * Returns true if the passed host is a SensorBase host.
1501       * The timeout is set at the default timeout value. 
1502       * Since checking isHost() when the host is not available is expensive, we cache the timestamp
1503       * whenever we find the host to be unavailable and if there is another call to isHost() within
1504       * two seconds, we will immediately return false.  This makes startup of clients like 
1505       * SensorShell go much faster, since they call isHost() several times during startup. 
1506       * 
1507       * @param host The URL of a sensorbase host, such as "http://localhost:9876/sensorbase".
1508       * @return True if this URL responds as a SensorBase host.
1509       */
1510      public static boolean isHost(String host) {
1511        RestletLoggerUtil.disableLogging();
1512        // We return false immediately if we failed to contact the host within the last two seconds. 
1513        long currTime = (new Date()).getTime();
1514        if ((currTime - getLastHostNotAvailable(host)) < 2 * 1000) {
1515          return false;
1516        }
1517    
1518        // All sensorbase hosts use the HTTP protocol.
1519        if (!host.startsWith("http://")) {
1520          setLastHostNotAvailable(host);
1521          return false;
1522        }
1523        // Create the host/register URL.
1524        try {
1525          String registerUri = host.endsWith("/") ? host + "ping" : host + "/ping";
1526          Request request = new Request();
1527          request.setResourceRef(registerUri);
1528          request.setMethod(Method.GET);
1529          Client client = new Client(Protocol.HTTP);
1530          setClientTimeout(client, getDefaultTimeout());
1531          Response response = client.handle(request);
1532          String pingText = response.getEntity().getText();
1533          boolean isAvailable = (response.getStatus().isSuccess() && "SensorBase".equals(pingText)); 
1534          if (!isAvailable) {
1535            setLastHostNotAvailable(host);
1536          }
1537          return isAvailable;
1538        }
1539        catch (Exception e) {
1540          setLastHostNotAvailable(host);
1541          return false;
1542        }
1543      }
1544    
1545      /**
1546       * Returns true if the user and password is registered as a user with this host.
1547       * 
1548       * @param host The URL of a sensorbase host, such as "http://localhost:9876/sensorbase".
1549       * @param email The user email.
1550       * @param password The user password.
1551       * @return True if this user is registered with this host.
1552       */
1553      public static boolean isRegistered(String host, String email, String password) {
1554        RestletLoggerUtil.disableLogging();
1555        // Make sure the host is OK, which captures bogus hosts like "foo".
1556        if (!isHost(host)) {
1557          return false;
1558        }
1559        // Now try to authenticate.
1560        try {
1561          SensorBaseClient client = new SensorBaseClient(host, email, password);
1562          client.authenticate();
1563          return true;
1564        }
1565        catch (Exception e) {
1566          return false;
1567        }
1568      }
1569      
1570      
1571    
1572      /**
1573       * Throws an unchecked illegal argument exception if the arg is null or empty.
1574       * 
1575       * @param arg The String that must be non-null and non-empty.
1576       */
1577      private void validateArg(String arg) {
1578        if ((arg == null) || ("".equals(arg))) {
1579          throw new IllegalArgumentException(arg + " cannot be null or the empty string.");
1580        }
1581      }
1582    
1583      /**
1584       * Does the housekeeping for making HTTP requests to the SensorBase by a test or admin user.
1585       * 
1586       * @param method The type of Method.
1587       * @param requestString A string, such as "users". No preceding slash.
1588       * @param entity The representation to be sent with the request, or null if not needed.
1589       * @return The Response instance returned from the server.
1590       */
1591      private Response makeRequest(Method method, String requestString, Representation entity) {
1592        Reference reference = new Reference(this.sensorBaseHost + requestString);
1593        Request request = (entity == null) ? new Request(method, reference) : new Request(method,
1594            reference, entity);
1595        request.getClientInfo().getAcceptedMediaTypes().add(xmlMedia);
1596        ChallengeResponse authentication = new ChallengeResponse(scheme, this.userEmail, this.password);
1597        request.setChallengeResponse(authentication);
1598        if (this.isTraceEnabled) {
1599          System.out.println("SensorBaseClient Tracing: " + method + " " + reference);
1600          if (entity != null) {
1601            try {
1602              System.out.println(entity.getText());
1603            }
1604            catch (Exception e) {
1605              System.out.println("  Problems with getText() on entity.");
1606            }
1607          }
1608        }
1609        Response response = this.client.handle(request);
1610        if (this.isTraceEnabled) {
1611          Status status = response.getStatus();
1612          System.out.println("  => " + status.getCode() + " " + status.getDescription());
1613        }
1614        return response;
1615      }
1616    
1617      /**
1618       * Takes a String encoding of a SensorDataType in XML format and converts it to an instance.
1619       * 
1620       * @param xmlString The XML string representing a SensorDataType
1621       * @return The corresponding SensorDataType instance.
1622       * @throws Exception If problems occur during unmarshalling.
1623       */
1624      private SensorDataType makeSensorDataType(String xmlString) throws Exception {
1625        Unmarshaller unmarshaller = sdtJAXB.createUnmarshaller();
1626        return (SensorDataType) unmarshaller.unmarshal(new StringReader(xmlString));
1627      }
1628    
1629      /**
1630       * Takes a String encoding of a SensorDataTypeIndex in XML format and converts it to an instance.
1631       * 
1632       * @param xmlString The XML string representing a SensorDataTypeIndex.
1633       * @return The corresponding SensorDataTypeIndex instance.
1634       * @throws Exception If problems occur during unmarshalling.
1635       */
1636      private SensorDataTypeIndex makeSensorDataTypeIndex(String xmlString) throws Exception {
1637        Unmarshaller unmarshaller = sdtJAXB.createUnmarshaller();
1638        return (SensorDataTypeIndex) unmarshaller.unmarshal(new StringReader(xmlString));
1639      }
1640    
1641      /**
1642       * Returns the passed SensorDataType instance as a String encoding of its XML representation.
1643       * 
1644       * @param sdt The SensorDataType instance.
1645       * @return The XML String representation.
1646       * @throws Exception If problems occur during translation.
1647       */
1648      private String makeSensorDataType(SensorDataType sdt) throws Exception {
1649        Marshaller marshaller = sdtJAXB.createMarshaller();
1650        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
1651        dbf.setNamespaceAware(true);
1652        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
1653        Document doc = documentBuilder.newDocument();
1654        marshaller.marshal(sdt, doc);
1655        DOMSource domSource = new DOMSource(doc);
1656        StringWriter writer = new StringWriter();
1657        StreamResult result = new StreamResult(writer);
1658        TransformerFactory tf = TransformerFactory.newInstance();
1659        Transformer transformer = tf.newTransformer();
1660        transformer.transform(domSource, result);
1661        String xmlString = writer.toString();
1662        // Now remove the processing instruction. This approach seems like a total hack.
1663        xmlString = xmlString.substring(xmlString.indexOf('>') + 1);
1664        return xmlString;
1665      }
1666    
1667      /**
1668       * Takes a String encoding of a User in XML format and converts it to an instance.
1669       * 
1670       * @param xmlString The XML string representing a User
1671       * @return The corresponding User instance.
1672       * @throws Exception If problems occur during unmarshalling.
1673       */
1674      private User makeUser(String xmlString) throws Exception {
1675        Unmarshaller unmarshaller = userJAXB.createUnmarshaller();
1676        return (User) unmarshaller.unmarshal(new StringReader(xmlString));
1677      }
1678    
1679      /**
1680       * Takes a String encoding of a UserIndex in XML format and converts it to an instance.
1681       * 
1682       * @param xmlString The XML string representing a UserIndex.
1683       * @return The corresponding UserIndex instance.
1684       * @throws Exception If problems occur during unmarshalling.
1685       */
1686      private UserIndex makeUserIndex(String xmlString) throws Exception {
1687        Unmarshaller unmarshaller = userJAXB.createUnmarshaller();
1688        return (UserIndex) unmarshaller.unmarshal(new StringReader(xmlString));
1689      }
1690    
1691      /**
1692       * Returns the passed Properties instance as a String encoding of its XML representation.
1693       * 
1694       * @param properties The Properties instance.
1695       * @return The XML String representation.
1696       * @throws Exception If problems occur during translation.
1697       */
1698      private String makeProperties(Properties properties) throws Exception {
1699        Marshaller marshaller = userJAXB.createMarshaller();
1700        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
1701        dbf.setNamespaceAware(true);
1702        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
1703        Document doc = documentBuilder.newDocument();
1704        marshaller.marshal(properties, doc);
1705        DOMSource domSource = new DOMSource(doc);
1706        StringWriter writer = new StringWriter();
1707        StreamResult result = new StreamResult(writer);
1708        TransformerFactory tf = TransformerFactory.newInstance();
1709        Transformer transformer = tf.newTransformer();
1710        transformer.transform(domSource, result);
1711        String xmlString = writer.toString();
1712        // Now remove the processing instruction. This approach seems like a total hack.
1713        xmlString = xmlString.substring(xmlString.indexOf('>') + 1);
1714        return xmlString;
1715      }
1716    
1717      /**
1718       * Takes an XML Document representing a SensorDataIndex and converts it to an instance.
1719       * 
1720       * @param xmlString The XML string representing a SensorDataIndex.
1721       * @return The corresponding SensorDataIndex instance.
1722       * @throws Exception If problems occur during unmarshalling.
1723       */
1724      private SensorDataIndex makeSensorDataIndex(String xmlString) throws Exception {
1725        Unmarshaller unmarshaller = sensordataJAXB.createUnmarshaller();
1726        return (SensorDataIndex) unmarshaller.unmarshal(new StringReader(xmlString));
1727      }
1728      
1729      /**
1730       * Takes an XML Document representing a ProjectSummary and converts it to an instance.
1731       * 
1732       * @param xmlString The XML string representing a ProjectSummary.
1733       * @return The corresponding ProjectSummary instance.
1734       * @throws Exception If problems occur during unmarshalling.
1735       */
1736      private ProjectSummary makeProjectSummary(String xmlString) throws Exception {
1737        Unmarshaller unmarshaller = projectJAXB.createUnmarshaller();
1738        return (ProjectSummary) unmarshaller.unmarshal(new StringReader(xmlString));
1739      }
1740      
1741      /**
1742       * Takes an XML Document representing a MultiDayProjectSummary and converts it to an instance.
1743       * 
1744       * @param xmlString The XML string representing a MultiDayProjectSummary.
1745       * @return The corresponding MultiDayProjectSummary instance.
1746       * @throws Exception If problems occur during unmarshalling.
1747       */
1748      private MultiDayProjectSummary makeMultiDayProjectSummary(String xmlString) throws Exception {
1749        Unmarshaller unmarshaller = projectJAXB.createUnmarshaller();
1750        return (MultiDayProjectSummary) unmarshaller.unmarshal(new StringReader(xmlString));
1751      }
1752    
1753      /**
1754       * Takes a String encoding of a SensorData in XML format and converts it to an instance.
1755       * 
1756       * @param xmlString The XML string representing a SensorData.
1757       * @return The corresponding SensorData instance.
1758       * @throws Exception If problems occur during unmarshalling.
1759       */
1760      private SensorData makeSensorData(String xmlString) throws Exception {
1761        Unmarshaller unmarshaller = sensordataJAXB.createUnmarshaller();
1762        return (SensorData) unmarshaller.unmarshal(new StringReader(xmlString));
1763      }
1764    
1765      /**
1766       * Returns the passed SensorData instance as a String encoding of its XML representation. Final
1767       * because it's called in constructor.
1768       * 
1769       * @param data The SensorData instance.
1770       * @return The XML String representation.
1771       * @throws Exception If problems occur during translation.
1772       */
1773      private final String makeSensorData(SensorData data) throws Exception {
1774        Marshaller marshaller = sensordataJAXB.createMarshaller();
1775        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
1776        dbf.setNamespaceAware(true);
1777        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
1778        Document doc = documentBuilder.newDocument();
1779        marshaller.marshal(data, doc);
1780        DOMSource domSource = new DOMSource(doc);
1781        StringWriter writer = new StringWriter();
1782        StreamResult result = new StreamResult(writer);
1783        TransformerFactory tf = TransformerFactory.newInstance();
1784        Transformer transformer = tf.newTransformer();
1785        transformer.transform(domSource, result);
1786        String xmlString = writer.toString();
1787        // Now remove the processing instruction. This approach seems like a total hack.
1788        xmlString = xmlString.substring(xmlString.indexOf('>') + 1);
1789        return xmlString;
1790      }
1791    
1792      /**
1793       * Returns the passed SensorDatas instance as a String encoding of its XML representation.
1794       * 
1795       * @param data The SensorDatas instance.
1796       * @return The XML String representation.
1797       * @throws Exception If problems occur during translation.
1798       */
1799      private String makeSensorDatas(SensorDatas data) throws Exception {
1800        Marshaller marshaller = sensordataJAXB.createMarshaller();
1801        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
1802        dbf.setNamespaceAware(true);
1803        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
1804        Document doc = documentBuilder.newDocument();
1805        marshaller.marshal(data, doc);
1806        DOMSource domSource = new DOMSource(doc);
1807        StringWriter writer = new StringWriter();
1808        StreamResult result = new StreamResult(writer);
1809        TransformerFactory tf = TransformerFactory.newInstance();
1810        Transformer transformer = tf.newTransformer();
1811        transformer.transform(domSource, result);
1812        String xmlString = writer.toString();
1813        // Now remove the processing instruction. This approach seems like a total hack.
1814        xmlString = xmlString.substring(xmlString.indexOf('>') + 1);
1815        return xmlString;
1816      }
1817    
1818      /**
1819       * Takes a String encoding of a Project in XML format and converts it to an instance.
1820       * 
1821       * @param xmlString The XML string representing a Project
1822       * @return The corresponding Project instance.
1823       * @throws Exception If problems occur during unmarshalling.
1824       */
1825      private Project makeProject(String xmlString) throws Exception {
1826        Unmarshaller unmarshaller = projectJAXB.createUnmarshaller();
1827        return (Project) unmarshaller.unmarshal(new StringReader(xmlString));
1828      }
1829    
1830      /**
1831       * Takes a String encoding of a ProjectIndex in XML format and converts it to an instance.
1832       * 
1833       * @param xmlString The XML string representing a ProjectIndex.
1834       * @return The corresponding ProjectIndex instance.
1835       * @throws Exception If problems occur during unmarshalling.
1836       */
1837      private ProjectIndex makeProjectIndex(String xmlString) throws Exception {
1838        Unmarshaller unmarshaller = projectJAXB.createUnmarshaller();
1839        return (ProjectIndex) unmarshaller.unmarshal(new StringReader(xmlString));
1840      }
1841    
1842      /**
1843       * Returns the passed Project instance as a String encoding of its XML representation.
1844       * 
1845       * @param project The Project instance.
1846       * @return The XML String representation.
1847       * @throws Exception If problems occur during translation.
1848       */
1849      private String makeProject(Project project) throws Exception {
1850        Marshaller marshaller = projectJAXB.createMarshaller();
1851        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
1852        dbf.setNamespaceAware(true);
1853        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
1854        Document doc = documentBuilder.newDocument();
1855        marshaller.marshal(project, doc);
1856        DOMSource domSource = new DOMSource(doc);
1857        StringWriter writer = new StringWriter();
1858        StreamResult result = new StreamResult(writer);
1859        TransformerFactory tf = TransformerFactory.newInstance();
1860        Transformer transformer = tf.newTransformer();
1861        transformer.transform(domSource, result);
1862        String xmlString = writer.toString();
1863        // Now remove the processing instruction. This approach seems like a total hack.
1864        xmlString = xmlString.substring(xmlString.indexOf('>') + 1);
1865        return xmlString;
1866      }
1867      
1868      /**
1869       * Attempts to set timeout values for the passed client. 
1870       * @param client The client .
1871       * @param milliseconds The timeout value. 
1872       */
1873      private static void setClientTimeout(Client client, int milliseconds) {
1874        client.setConnectTimeout(milliseconds);
1875    //    client.getContext().getParameters().removeAll("connectTimeout");
1876    //    client.getContext().getParameters().add("connectTimeout", String.valueOf(milliseconds));
1877    //    // For the Apache Commons client.
1878    //    client.getContext().getParameters().removeAll("readTimeout");
1879    //    client.getContext().getParameters().add("readTimeout", String.valueOf(milliseconds));
1880    //    client.getContext().getParameters().removeAll("connectionManagerTimeout");
1881    //    client.getContext().getParameters().add("connectionManagerTimeout", 
1882    //        String.valueOf(milliseconds));
1883      }
1884      
1885      /**
1886       * Enables caching in this client.
1887       * If caching has already been enabled, then does nothing.  
1888       * @param cacheName The name of the cache.
1889       * @param subDir The subdirectory in which the cache backend store is saved.
1890       * @param maxLife The default expiration time for objects, in days.
1891       * @param capacity The maximum number of instances to be held in-memory.
1892       */
1893      public synchronized void enableCaching(String cacheName, String subDir, Double maxLife, 
1894          Long capacity) {
1895        if (!this.isCacheEnabled) {
1896          this.uriCache = new UriCache(cacheName, subDir, maxLife, capacity);
1897          this.isCacheEnabled = true;
1898        }
1899      }
1900     
1901      /**
1902       * Delete all entries from this cache. 
1903       * If the cache is not enabled, then does nothing.
1904       */
1905      public synchronized void clearCache() {
1906        if (this.isCacheEnabled) {
1907          this.uriCache.clear();
1908        }
1909      }
1910      
1911      /**
1912       * Compresses the server database tables.  
1913       * You must be the admin user in order for this command to succeed.
1914       * @throws SensorBaseClientException If problems occur posting this data.
1915       */
1916      public synchronized void compressTables() throws SensorBaseClientException {
1917        try {
1918          Response response = makeRequest(Method.PUT, "db/table/compress", null);
1919          if (!response.getStatus().isSuccess()) {
1920            throw new SensorBaseClientException(response.getStatus());
1921          }
1922        }
1923        // Allow SensorBaseClientExceptions to be thrown out of this method.
1924        catch (SensorBaseClientException f) {
1925          throw f;
1926        }
1927        // All other exceptions are caught and rethrown.
1928        catch (Exception e) {
1929          throw new SensorBaseClientException("Error in db command.", e);
1930        }
1931      }
1932      
1933      /**
1934       * Indexes the server database tables.  
1935       * You must be the admin user in order for this command to succeed.
1936       * @throws SensorBaseClientException If problems occur posting this data.
1937       */
1938      public synchronized void indexTables() throws SensorBaseClientException {
1939        try {
1940          Response response = makeRequest(Method.PUT, "db/table/index", null);
1941          if (!response.getStatus().isSuccess()) {
1942            throw new SensorBaseClientException(response.getStatus());
1943          }
1944        }
1945        // Allow SensorBaseClientExceptions to be thrown out of this method.
1946        catch (SensorBaseClientException f) {
1947          throw f;
1948        }
1949        // All other exceptions are caught and rethrown.
1950        catch (Exception e) {
1951          throw new SensorBaseClientException("Error in db command", e);
1952        }
1953      }
1954      
1955      /**
1956       * Gets the rowcount for the specified table.
1957       * You must be the admin user in order for this command to succeed.
1958       * @param table The name of the table whose rowcount is to be retrieved.
1959       * @return The number of rows in that table. 
1960       * @throws SensorBaseClientException If problems occur posting this data.
1961       * This can happen if the user is not the admin user, or if the table name is invalid.
1962       */
1963      public synchronized int rowCount(String table) throws SensorBaseClientException {
1964        try {
1965          Response response = makeRequest(Method.GET, "db/table/" + table + "/rowcount", null);
1966          if (!response.getStatus().isSuccess()) {
1967            throw new SensorBaseClientException(response.getStatus());
1968          }
1969          String rowCountString = response.getEntity().getText();
1970          return Integer.valueOf(rowCountString).intValue();
1971        }
1972        // Allow SensorBaseClientExceptions to be thrown out of this method.
1973        catch (SensorBaseClientException f) {
1974          throw f;
1975        }
1976        // All other exceptions are caught and rethrown.
1977        catch (Exception e) {
1978          throw new SensorBaseClientException("Error in rowcount command", e);
1979        }
1980      }
1981    
1982    }