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 }