001    package org.hackystat.telemetry.service.client;
002    
003    import java.io.StringReader;
004    import java.util.Date;
005    import java.util.logging.Logger;
006    
007    import javax.xml.bind.JAXBContext;
008    import javax.xml.bind.Unmarshaller;
009    import javax.xml.datatype.XMLGregorianCalendar;
010    
011    import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryChartDefinition;
012    import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryChartIndex;
013    import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryChartData;
014    import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryPoint;
015    import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryStream;
016    import org.hackystat.utilities.logger.HackystatLogger;
017    import org.restlet.Client;
018    import org.restlet.data.ChallengeResponse;
019    import org.restlet.data.ChallengeScheme;
020    import org.restlet.data.MediaType;
021    import org.restlet.data.Method;
022    import org.restlet.data.Preference;
023    import org.restlet.data.Protocol;
024    import org.restlet.data.Reference;
025    import org.restlet.data.Request;
026    import org.restlet.data.Response;
027    import org.restlet.data.Status;
028    import org.restlet.resource.Representation;
029    
030    /**
031     * Provides a client to support access to the DailyProjectData service. 
032     * @author Philip Johnson
033     */
034    public class TelemetryClient {
035      
036      /** Holds the userEmail to be associated with this client. */
037      private String userEmail;
038      /** Holds the password to be associated with this client. */
039      private String password;
040      /** The Telemetry host, such as "http://localhost:9878/telemetry". */
041      private String telemetryHost;
042      /** The Restlet Client instance used to communicate with the server. */
043      private Client client;
044      /** Chart JAXBContext. */
045      private JAXBContext chartJAXB;
046      /** The http authentication approach. */
047      private ChallengeScheme scheme = ChallengeScheme.HTTP_BASIC;
048      /** The preferred representation type. */
049      private Preference<MediaType> xmlMedia = new Preference<MediaType>(MediaType.TEXT_XML);
050      /** To facilitate debugging of problems using this system. */
051      private boolean isTraceEnabled = false;
052      /** The logger for telemetry client information. */
053      private Logger logger;
054      /** The System property key used to retrieve the default timeout value in milliseconds. */
055      public static final String TELEMETRYCLIENT_TIMEOUT_KEY = "telemetryclient.timeout";
056      /** Required by PMD. */
057      private static final String cache = "cache/";
058      /** Required by PMD. */
059      private String space = " : ";
060      
061      /**
062       * Initializes a new TelemetryClient, given the host, userEmail, and password. 
063       * Note that the userEmail and password refer to the underlying SensorBase client
064       * associated with this Telemetry service.  This service does not keep its own
065       * independent set of userEmails and passwords.  Authentication is not actually performed
066       * in this constructor. Use the authenticate() method to explicitly check the authentication
067       * credentials. 
068       * @param host The host, such as 'http://localhost:9878/telemetry'.
069       * @param email The user's email used for authentication. 
070       * @param password The password used for authentication.
071       */
072      public TelemetryClient(String host, String email, String password) {
073        this.logger = HackystatLogger.getLogger(
074            "org.hackystat.telemetry.service.client.TelemetryClient", "telemetry", false);
075        this.logger.info("Instantiating client for: " + host + " " + email);
076        validateArg(host);
077        validateArg(email);
078        validateArg(password);
079        this.userEmail = email;
080        this.password = password;
081        this.telemetryHost = host;
082        if (!this.telemetryHost.endsWith("/")) {
083          this.telemetryHost = this.telemetryHost + "/";
084        }
085        if (this.isTraceEnabled) {
086          System.out.println("TelemetryClient Tracing: INITIALIZE " + 
087              "host='" + host + "', email='" + email + "', password='" + password + "'");
088        }
089        this.client = new Client(Protocol.HTTP);
090        setTimeoutFromSystemProperty();
091        try {
092          this.chartJAXB = 
093            JAXBContext.newInstance(
094                org.hackystat.telemetry.service.resource.chart.jaxb.ObjectFactory.class);
095        }
096        catch (Exception e) {
097          throw new RuntimeException("Couldn't create JAXB context instances.", e);
098        }
099      }
100      
101      /**
102       * Throws an unchecked illegal argument exception if the arg is null or empty. 
103       * @param arg The String that must be non-null and non-empty. 
104       */
105      private void validateArg(String arg) {
106        if ((arg == null) || ("".equals(arg))) {
107          throw new IllegalArgumentException(arg + " cannot be null or the empty string.");
108        }
109      }
110      
111      
112      /**
113       * Sets the timeout for this client if the system property telemetryclient.timeout is set
114       * and if it can be parsed to an integer.
115       */
116      private void setTimeoutFromSystemProperty() {
117        String systemTimeout = System.getProperty(TELEMETRYCLIENT_TIMEOUT_KEY);
118        // if not set, then return immediately.
119        if (systemTimeout == null) {
120          return;
121        }
122        // systemTimeout has a value, so set it if we can.
123        try {
124          int timeout = Integer.parseInt(systemTimeout);
125          setTimeout(timeout);
126          this.logger.info("TelemetryClient timeout set to: " + timeout + " milliseconds");
127        }
128        catch (Exception e) {
129          this.logger.warning("telemetryclient.timeout has non integer value: " + systemTimeout);
130        }
131      }
132      
133      /**
134       * Sets the timeout value for this client.
135       * 
136       * @param milliseconds The number of milliseconds to wait before timing out.
137       */
138      public final synchronized void setTimeout(int milliseconds) {
139        client.getContext().getParameters().removeAll("connectTimeout");
140        client.getContext().getParameters().add("connectTimeout", String.valueOf(milliseconds));
141        // For the Apache Commons client.
142        client.getContext().getParameters().removeAll("readTimeout");
143        client.getContext().getParameters().add("readTimeout", String.valueOf(milliseconds));
144        client.getContext().getParameters().removeAll("connectionManagerTimeout");
145        client.getContext().getParameters().add("connectionManagerTimeout",
146            String.valueOf(milliseconds));
147      }
148    
149      
150      /**
151       * Does the housekeeping for making HTTP requests to the SensorBase by a test or admin user. 
152       * @param method The type of Method.
153       * @param requestString A string, such as "users". No preceding slash. 
154       * @param entity The representation to be sent with the request, or null if not needed.  
155       * @return The Response instance returned from the server.
156       */
157      private Response makeRequest(Method method, String requestString, Representation entity) {
158        Reference reference = new Reference(this.telemetryHost + requestString);
159        Request request = (entity == null) ? 
160            new Request(method, reference) :
161              new Request(method, reference, entity);
162        request.getClientInfo().getAcceptedMediaTypes().add(xmlMedia); 
163        ChallengeResponse authentication = new ChallengeResponse(scheme, this.userEmail, this.password);
164        request.setChallengeResponse(authentication);
165        if (this.isTraceEnabled) {
166          System.out.println("TelemetryClient Tracing: " + method + " " + reference);
167          if (entity != null) {
168            try {
169              System.out.println(entity.getText());
170            }
171            catch (Exception e) {
172              System.out.println("  Problems with getText() on entity.");
173            }
174          }
175        }
176        Response response = this.client.handle(request);
177        if (this.isTraceEnabled) {
178          Status status = response.getStatus();
179          System.out.println("  => " + status.getCode() + " " + status.getDescription());
180        }
181        return response;
182      }
183      
184      /**
185       * Takes a String encoding of a TelemetryChart in XML format and converts it. 
186       * @param xmlString The XML string representing a TelemetryChart.
187       * @return The corresponding TelemetryChart instance. 
188       * @throws Exception If problems occur during unmarshalling.
189       */
190      private TelemetryChartData makeChart(String xmlString) throws Exception {
191        Unmarshaller unmarshaller = this.chartJAXB.createUnmarshaller();
192        return (TelemetryChartData)unmarshaller.unmarshal(new StringReader(xmlString));
193      }
194      
195      /**
196       * Takes a String encoding of a TelemetryChartIndex in XML format and converts it. 
197       * @param xmlString The XML string representing a TelemetryChartIndex.
198       * @return The corresponding TelemetryChartIndex instance. 
199       * @throws Exception If problems occur during unmarshalling.
200       */
201      private TelemetryChartIndex makeChartIndex(String xmlString) throws Exception {
202        Unmarshaller unmarshaller = this.chartJAXB.createUnmarshaller();
203        return (TelemetryChartIndex)unmarshaller.unmarshal(new StringReader(xmlString));
204      }
205      
206      /**
207       * Takes a String encoding of a TelemetryChartDefinition in XML format and converts it. 
208       * @param xmlString The XML string representing a TelemetryChartDefinition.
209       * @return The corresponding TelemetryChartDefinition instance. 
210       * @throws Exception If problems occur during unmarshalling.
211       */
212      private TelemetryChartDefinition makeChartDefinition(String xmlString) throws Exception {
213        Unmarshaller unmarshaller = this.chartJAXB.createUnmarshaller();
214        return (TelemetryChartDefinition)unmarshaller.unmarshal(new StringReader(xmlString));
215      }
216      
217      /**
218       * Authenticates this user and password with this Telemetry service, throwing a
219       * TelemetryClientException if the user and password associated with this instance
220       * are not valid credentials. 
221       * Note that authentication is performed by checking these credentials with the underlying
222       * SensorBase; this service does not keep its own independent set of usernames and passwords.
223       * @return This TelemetryClient instance. 
224       * @throws TelemetryClientException If authentication is not successful. 
225       */
226      public synchronized TelemetryClient authenticate() throws TelemetryClientException {
227        // Performs authentication by invoking ping with user and password as form params.
228        String uri = "ping?user=" + this.userEmail + "&password=" + this.password;
229        Response response = makeRequest(Method.GET, uri, null); 
230        if (!response.getStatus().isSuccess()) {
231          throw new TelemetryClientException(response.getStatus());
232        }
233        String responseString;
234        try {
235          responseString = response.getEntity().getText();
236        }
237        catch (Exception e) {
238          throw new TelemetryClientException("Bad response", e);
239        }
240        if (!"Telemetry authenticated".equals(responseString)) {
241          throw new TelemetryClientException("Authentication failed");
242        }
243        return this;
244      }
245      
246      /**
247       * Returns a TelemetryChart instance from this server, or throws a
248       * TelemetryClientException if problems occur.  
249       * @param name The chart name.
250       * @param user The user email.
251       * @param project The project.
252       * @param granularity Either Day, Week, or Month.
253       * @param start The start day.
254       * @param end The end day.
255       * @return The TelemetryChart instance. 
256       * @throws TelemetryClientException If the credentials associated with this instance
257       * are not valid, or if the underlying SensorBase service cannot be reached, or if one or more
258       * of the supplied user, password, or timestamp is not valid.
259       */
260      public synchronized TelemetryChartData getChart(String name, String user, String project, 
261          String granularity, XMLGregorianCalendar start, XMLGregorianCalendar end) 
262      throws TelemetryClientException {
263        return getChart(name, user, project, granularity, start, end, null);
264      } 
265      
266      /**
267       * Returns a TelemetryChart instance from this server, or throws a
268       * TelemetryClientException if problems occur.  
269       * @param name The chart name.
270       * @param user The user email.
271       * @param project The project.
272       * @param granularity Either Day, Week, or Month.
273       * @param start The start day.
274       * @param end The end day.
275       * @param params The parameter string, or null if no params are present.
276       * @return The TelemetryChart instance. 
277       * @throws TelemetryClientException If the credentials associated with this instance
278       * are not valid, or if the underlying SensorBase service cannot be reached, or if one or more
279       * of the supplied user, password, or timestamp is not valid.
280       */
281      public synchronized TelemetryChartData getChart(String name, String user, String project, 
282          String granularity, XMLGregorianCalendar start, XMLGregorianCalendar end, String params) 
283      throws TelemetryClientException {
284        long startTime = (new Date()).getTime();
285        String uri = 
286          "chart/" + name + "/" + user + "/" + project + "/" + granularity + "/" + start + "/" + end +
287          ((params == null) ? "" : "?params=" + params);
288        Response response = makeRequest(Method.GET,  uri, null);
289        TelemetryChartData chart;
290        if (!response.getStatus().isSuccess()) {
291          String msg = response.getStatus().getDescription() + space + uri;
292          logElapsedTime(msg, startTime);
293          throw new TelemetryClientException(response.getStatus());
294        }
295        try {
296          String xmlData = response.getEntity().getText();
297          chart = makeChart(xmlData);
298        }
299        catch (Exception e) {
300          logElapsedTime(uri, startTime, e);
301          throw new TelemetryClientException(response.getStatus(), e);
302        }
303        logElapsedTime(uri, startTime);
304        return chart;
305      }
306      
307      /**
308       * Clears the DailyProjectData cache associated with this user in the Telemetry service 
309       * associated with this TelemetryClient.
310       * @throws TelemetryClientException If problems occur. 
311       */
312      public synchronized void clearServerCache() throws TelemetryClientException {
313        long startTime = (new Date()).getTime();
314        String uri = cache;
315        Response response = makeRequest(Method.DELETE,  uri, null);
316        if (!response.getStatus().isSuccess()) {
317          String msg = response.getStatus().getDescription() + space + uri;
318          logElapsedTime(msg, startTime);
319          throw new TelemetryClientException(response.getStatus());
320        }
321      }
322      
323      /**
324       * Clears the DailyProjectData cache entries for this user that are associated with the passed 
325       * project and its owner in the Telemetry service associated with this TelemetryClient.
326       * @param project The project to be cleared. 
327       * @param owner The owner of the project. 
328       * @throws TelemetryClientException If problems occur. 
329       */
330      public synchronized void clearServerCache(String owner, String project) 
331      throws TelemetryClientException {
332        long startTime = (new Date()).getTime();
333        String uri = cache + owner + "/" + project;
334        Response response = makeRequest(Method.DELETE,  uri, null);
335        if (!response.getStatus().isSuccess()) {
336          String msg = response.getStatus().getDescription() + space + uri;
337          logElapsedTime(msg, startTime);
338          throw new TelemetryClientException(response.getStatus());
339        }
340      }
341      
342      /**
343       * Returns a TelemetryChartIndex instance from this server, or throws a
344       * TelemetryClientException if problems occur.  
345       * @return The TelemetryChartIndex instance. 
346       * @throws TelemetryClientException If the credentials associated with this instance
347       * are not valid, or if the underlying SensorBase service cannot be reached, or if one or more
348       * of the supplied user, password, or timestamp is not valid.
349       */
350      public synchronized TelemetryChartIndex getChartIndex() throws TelemetryClientException {
351        long startTime = (new Date()).getTime();
352        String uri = "charts";
353        Response response = makeRequest(Method.GET,  uri, null);
354        TelemetryChartIndex index;
355        if (!response.getStatus().isSuccess()) {
356          String msg = response.getStatus().getDescription() + space + uri;
357          logElapsedTime(msg, startTime);
358          throw new TelemetryClientException(response.getStatus());
359        }
360        try {
361          String xmlData = response.getEntity().getText();
362          index = makeChartIndex(xmlData);
363        }
364        catch (Exception e) {
365          logElapsedTime(uri, startTime, e);
366          throw new TelemetryClientException(response.getStatus(), e);
367        }
368        logElapsedTime(uri, startTime);
369        return index;
370      } 
371      
372      /**
373       * Returns a TelemetryChartDefinition instance from this server, or throws a
374       * TelemetryClientException if problems occur.  
375       * 
376       * @param chartName The name of the chart whose definition is to be retrieved.
377       * @return The TelemetryChartDefinition instance. 
378       * @throws TelemetryClientException If the credentials associated with this instance
379       * are not valid, or if the underlying SensorBase service cannot be reached, or if one or more
380       * of the supplied user, password, or timestamp is not valid.
381       */
382      public synchronized TelemetryChartDefinition getChartDefinition(String chartName) 
383      throws TelemetryClientException {
384        long startTime = (new Date()).getTime();
385        String uri = "chart/" + chartName;
386        Response response = makeRequest(Method.GET,  uri, null);
387        TelemetryChartDefinition chartDef;
388        if (!response.getStatus().isSuccess()) {
389          String msg = response.getStatus().getDescription() + space + uri;
390          logElapsedTime(msg, startTime);
391          throw new TelemetryClientException(response.getStatus());
392        }
393        try {
394          String xmlData = response.getEntity().getText();
395          chartDef = makeChartDefinition(xmlData);
396        }
397        catch (Exception e) {
398          logElapsedTime(uri, startTime, e);
399          throw new TelemetryClientException(response.getStatus(), e);
400        }
401        logElapsedTime(uri, startTime);
402        return chartDef;
403      } 
404      
405      /**
406       * Returns true if the passed host is a Telemetry host. 
407       * @param host The URL of a Telemetry host, "http://localhost:9875/telemetry".
408       * @return True if this URL responds as a Telemetry host. 
409       */
410      public static boolean isHost(String host) {
411        // All sensorbase hosts use the HTTP protocol.
412        if (!host.startsWith("http://")) {
413          return false;
414        }
415        // Create the host/register URL.
416        try {
417          String registerUri = host.endsWith("/") ? host + "ping" : host + "/ping"; 
418          Request request = new Request();
419          request.setResourceRef(registerUri);
420          request.setMethod(Method.GET);
421          Client client = new Client(Protocol.HTTP);
422          Response response = client.handle(request);
423          String pingText = response.getEntity().getText();
424          return (response.getStatus().isSuccess() && "Telemetry".equals(pingText));
425        }
426        catch (Exception e) {
427          return false;
428        }
429      }
430    
431      /**
432       * Returns the host associated with this Telemetry client. 
433       * @return The host.
434       */
435      public String getHostName() {
436        return this.telemetryHost;
437      }
438      
439      /**
440       * Returns the passed telemetry chart data in a human-readable string.
441       * For debugging purposes, this method is expensive. 
442       * @param chart The chart data.
443       * @return The chart data, as a string.
444       */
445      public static String toString(TelemetryChartData chart) {
446        StringBuilder toString = new StringBuilder();
447        toString.append("Telemetry Chart Data : ");
448        toString.append(chart.getURI()); 
449        for (TelemetryStream stream : chart.getTelemetryStream()) {
450          toString.append("\n ");
451          toString.append(stream.getName());
452          for (TelemetryPoint point : stream.getTelemetryPoint()) {
453            toString.append("   ");
454            toString.append(point.getValue());
455          }
456        }
457        return toString.toString();
458      }
459      
460      /**
461       * Logs info to the logger about the elapsed time for this request. 
462       * @param uri The URI requested.
463       * @param startTime The startTime of the call.
464       * @param e The exception thrown, or null if no exception. 
465       */
466      private void logElapsedTime (String uri, long startTime, Exception e) {
467        long millis = (new Date()).getTime() - startTime;
468        String msg = millis + " millis: " + uri + ((e == null) ? "" : " " + e);
469        this.logger.info(msg);
470      }
471      
472      /**
473       * Logs info to the logger about the elapsed time for this request. 
474       * @param uri The URI requested.
475       * @param startTime The startTime of the call.
476       */
477      private void logElapsedTime (String uri, long startTime) {
478        logElapsedTime(uri, startTime, null);
479      }
480    
481    }