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