001 package org.hackystat.tickertape.tickerlingua; 002 003 import java.io.File; 004 import java.lang.reflect.Constructor; 005 import java.util.ArrayList; 006 import java.util.Collection; 007 import java.util.HashMap; 008 import java.util.List; 009 import java.util.Map; 010 import java.util.logging.Logger; 011 012 import javax.xml.bind.JAXBContext; 013 import javax.xml.bind.Unmarshaller; 014 015 import org.hackystat.dailyprojectdata.client.DailyProjectDataClient; 016 import org.hackystat.sensorbase.client.SensorBaseClient; 017 import org.hackystat.telemetry.service.client.TelemetryClient; 018 import org.hackystat.tickertape.ticker.Ticker; 019 import org.hackystat.tickertape.tickerlingua.jaxb.Properties; 020 import org.hackystat.utilities.logger.HackystatLogger; 021 022 /** 023 * Reads a TickerLingua definition file, validates it, and provides access to its components. 024 * <p> 025 * A valid TickerLingua definition file is both syntactically and semantically valid. 026 * <p> 027 * Syntactic validity means that it satisfies the XmlSchema definition for a TickerLingua file. 028 * See the tickerlingua.definition.xsd file for details on syntactic validity. This is assessed 029 * by the JAXB parser when the file is read in. 030 * <p> 031 * Semantic validity involves ensuring the following: 032 * <ul> 033 * <li> All HackystatAccountRef refid attributes name a defined HackystatAccount. 034 * <li> All HackystatProjectRef refid attributes name a defined HackystatProject. 035 * <li> All HackystatUserRef refid attributes name a defined HackystatUser. 036 * <li> All TwitterAccountRef refid attributes name a defined TwitterAccount. 037 * <li> All FacebookAccountRef refid attributes name a defined FacebookAccount. 038 * <li> All NabaztagRef refid attributes name a defined Nabaztag. 039 * <li> All SmsAccountRef refid attributes name a defined SmsAccount. 040 * <li> All hackystatservice-refid attributes name a defined HackystatService. 041 * <li> All Ticker class attributes name a defined Java class implementing the Ticker interface. 042 * <li> All HackystatAccountRef refid attributes name a defined HackystatAccount. 043 * <li> All intervalhours attributes specify a double. 044 * <li> All enabled attributes specify a boolean. 045 * </ul> 046 * <p> 047 * In addition, if a HackystatProject refers to a sensorbase that cannot be contacted, or a 048 * user/password combination that is not legal, then a warning message is logged. 049 * <p> 050 * If a tickerlingua.xml definition file cannot be found, or is not both syntactically and 051 * semantically valid, then a RuntimeException is thrown. 052 * 053 * @author Philip Johnson 054 * 055 */ 056 public class TickerLingua { 057 058 /** Holds the instance of the XML TickerLingua definition. */ 059 private org.hackystat.tickertape.tickerlingua.jaxb.TickerLingua jaxbTickerLingua; 060 061 // Here's where we collect all the objects. 062 private Map<String, HackystatService> services = new HashMap<String, HackystatService>(); 063 private Map<String, HackystatUser> users = new HashMap<String, HackystatUser>(); 064 private Map<String, HackystatProject> projects = new HashMap<String, HackystatProject>(); 065 private Map<String, Nabaztag> nabaztags = new HashMap<String, Nabaztag>(); 066 private Map<String, TwitterAccount> twitters = new HashMap<String, TwitterAccount>(); 067 private Map<String, FacebookAccount> facebooks = new HashMap<String, FacebookAccount>(); 068 private Map<String, Tickertape> tickertapes = new HashMap<String, Tickertape>(); 069 070 private Logger logger = HackystatLogger.getLogger("TickerLinguaLogger", "tickertape", true); 071 072 private String smtpServer = null; 073 private String loggingLevel = "INFO"; 074 075 /** 076 * Creates a TickerLingua instance from default location ~/.hackystat/tickertape/tickertape.xml. 077 * @throws TickerLinguaException If problems occur while getting or processing the file. 078 */ 079 public TickerLingua() throws TickerLinguaException { 080 this(System.getProperty("user.home") + "/.hackystat/tickertape/tickertape.xml"); 081 } 082 083 /** 084 * Creates a TickerLingua instance from the passed file. 085 * @param filePath A path to the file. 086 * @throws TickerLinguaException If problems occur while getting or processing the file. 087 */ 088 public TickerLingua(String filePath) throws TickerLinguaException { 089 File definitionFile = new File(filePath); 090 091 // Return if we can't find the passed file. 092 if (!definitionFile.exists()) { 093 throw new TickerLinguaException("Tickertape definition file missing: " + filePath); 094 } 095 096 // Now initialize our jaxbTickerLingua instance variable with the contents of the XML file. 097 try { 098 JAXBContext jaxbContext = JAXBContext 099 .newInstance(org.hackystat.tickertape.tickerlingua.jaxb.ObjectFactory.class); 100 Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); 101 this.jaxbTickerLingua = (org.hackystat.tickertape.tickerlingua.jaxb.TickerLingua) 102 unmarshaller.unmarshal(definitionFile); 103 } 104 catch (Exception e) { 105 throw new TickerLinguaException("Tickertape definition file invalid: " + filePath, e); 106 } 107 108 // Now start processing the JAXB instances and creating our validated entities. 109 // This means (a) checking that certain strings are valid, and (b) resolving "refids". 110 // Order is important in the following. Need to go bottom up. 111 processHackystatServices(); 112 processNabaztags(); 113 processFacebookAccounts(); 114 processTwitterAccounts(); 115 processHackystatUsers(); 116 processHackystatProjects(); 117 processTickertapes(); 118 processGlobals(); 119 120 } 121 122 123 /** 124 * Creates instances of all HackystatServices. 125 * Logs a warning if the services cannot be reached. 126 * @throws TickerLinguaException If duplicate ID encountered. 127 */ 128 private void processHackystatServices() throws TickerLinguaException { 129 for (org.hackystat.tickertape.tickerlingua.jaxb.HackystatService jaxb : 130 this.jaxbTickerLingua.getHackystatServices().getHackystatService()) { 131 String id = jaxb.getId(); 132 if (this.services.containsKey(id)) { 133 throw new TickerLinguaException("Duplicate HackystatService id: " + id); 134 } 135 HackystatService service = new HackystatService(jaxb); 136 if (!SensorBaseClient.isHost(service.getSensorbase())) { 137 logger.warning("Sensorbase not found: " + service.getSensorbase()); 138 } 139 if (!DailyProjectDataClient.isHost(service.getDailyProjectData())) { 140 logger.warning("DailyProjectData service not found: " + service.getDailyProjectData()); 141 } 142 if (!TelemetryClient.isHost(service.getTelemetry())) { 143 logger.warning("Telemetry service not found: " + service.getTelemetry()); 144 } 145 this.services.put(id, new HackystatService(jaxb)); 146 } 147 } 148 149 /** 150 * Processes the Globals section of the Tickertape definition. 151 */ 152 private void processGlobals() { 153 try { 154 if (this.jaxbTickerLingua.getGlobals() == null) { 155 return; 156 } 157 if (this.jaxbTickerLingua.getGlobals().getMail() != null) { 158 this.smtpServer = this.jaxbTickerLingua.getGlobals().getMail().getSmtpServer(); 159 } 160 if (this.jaxbTickerLingua.getGlobals().getLoggingLevel() != null) { 161 this.loggingLevel = this.jaxbTickerLingua.getGlobals().getLoggingLevel().getLevel(); 162 } 163 } 164 catch (Exception e) { 165 this.logger.warning("Errors processing Globals section: " + e.getMessage()); 166 } 167 } 168 169 /** 170 * Returns the smtp server specified in the Globals section, or null if none specified. 171 * @return The smtp server, or null. 172 */ 173 public String getSmtpServer() { 174 return this.smtpServer; 175 } 176 177 178 /** 179 * Returns the logging level specified in the globals section, or "INFO" if none specified. 180 * @return The logging level, or "INFO" if not specified. 181 */ 182 public String getLoggingLevel () { 183 return this.loggingLevel; 184 } 185 186 187 /** 188 * Defines Nabaztag instances from the JAXB. 189 * @throws TickerLinguaException If a duplicate ID is found. 190 */ 191 private void processNabaztags() throws TickerLinguaException { 192 if (this.jaxbTickerLingua.getNabaztags() == null) { 193 return; 194 } 195 for (org.hackystat.tickertape.tickerlingua.jaxb.Nabaztag jaxb : 196 this.jaxbTickerLingua.getNabaztags().getNabaztag()) { 197 String id = jaxb.getId(); 198 if (this.nabaztags.containsKey(id)) { 199 throw new TickerLinguaException("Duplicate Nabaztag id: " + id); 200 } 201 this.nabaztags.put(id, new Nabaztag(jaxb)); 202 } 203 } 204 205 /** 206 * Defines FacebookAccounts from the JAXB. 207 * @throws TickerLinguaException If a duplicate ID is found. 208 */ 209 private void processFacebookAccounts() throws TickerLinguaException { 210 if (this.jaxbTickerLingua.getFacebookAccounts() == null) { 211 return; 212 } 213 for (org.hackystat.tickertape.tickerlingua.jaxb.FacebookAccount jaxb : 214 this.jaxbTickerLingua.getFacebookAccounts().getFacebookAccount()) { 215 String id = jaxb.getId(); 216 if (this.facebooks.containsKey(id)) { 217 throw new TickerLinguaException("Duplicate Facebook id: " + id); 218 } 219 this.facebooks.put(id, new FacebookAccount(jaxb)); 220 } 221 } 222 223 /** 224 * Defines TwitterAccounts from the JAXB. 225 * @throws TickerLinguaException If a duplicate ID is found. 226 */ 227 private void processTwitterAccounts() throws TickerLinguaException { 228 for (org.hackystat.tickertape.tickerlingua.jaxb.TwitterAccount jaxb : 229 this.jaxbTickerLingua.getTwitterAccounts().getTwitterAccount()) { 230 String id = jaxb.getId(); 231 if (this.twitters.containsKey(id)) { 232 throw new TickerLinguaException("Duplicate Twitter id: " + id); 233 } 234 this.twitters.put(id, new TwitterAccount(jaxb)); 235 } 236 } 237 238 /** 239 * Defines HackystatUser instances from the JAXB. 240 * Logs warnings if the sensorbase cannot be reached or the user is not registered. 241 * 242 * @throws TickerLinguaException If there is a duplicate ID, or if the references 243 * to the hackystat service, twitter, or facebook cannot be resolved. 244 */ 245 private void processHackystatUsers() throws TickerLinguaException { 246 // Process HackystatUsers. Validate and resolve Service, Twitter, and Facebook references. 247 for (org.hackystat.tickertape.tickerlingua.jaxb.HackystatUser jaxb : 248 this.jaxbTickerLingua.getHackystatUsers().getHackystatUser()) { 249 String id = jaxb.getId(); 250 if (this.users.containsKey(id)) { 251 throw new TickerLinguaException("Duplicate HackystatUser id: " + id); 252 } 253 // Validate and resolve HackystatServiceRef 254 String serviceId = jaxb.getHackystatAccount().getHackystatserviceRefid(); 255 HackystatService service = this.services.get(serviceId); 256 if (service == null) { 257 throw new TickerLinguaException("Invalid hackystatservice-refid: " + serviceId); 258 } 259 260 // Check to see if user/password/service are OK. 261 String sensorbase = service.getSensorbase(); 262 String hackystatUser = jaxb.getHackystatAccount().getUser(); 263 String hackystatPassword = jaxb.getHackystatAccount().getPassword(); 264 if (!SensorBaseClient.isHost(sensorbase)) { 265 this.logger.warning("Warning: Sensorbase not found: " + sensorbase); 266 } 267 if ((hackystatPassword != null) && 268 (!SensorBaseClient.isRegistered(sensorbase, hackystatUser, hackystatPassword))) { 269 this.logger.warning("Warning: Hackystat credentials not OK: " + hackystatUser); 270 } 271 272 // Validate and resolve TwitterAccountRef (if any). 273 // Either twitterAccount will be null or will contain a valid TwitterAccount instance. 274 String twitterId = null; 275 TwitterAccount twitterAccount = null; 276 if (jaxb.getTwitterAccountRef() != null) { 277 twitterId = jaxb.getTwitterAccountRef().getRefid(); 278 } 279 if (twitterId != null) { 280 twitterAccount = this.twitters.get(twitterId); 281 if (twitterAccount == null) { 282 throw new TickerLinguaException("Invalid TwitterAccountRef: " + twitterId); 283 } 284 } 285 // Validate and resolve FacebookAccountRef (if any). 286 // Either facebookAccount will be null or will contain a valid FacebookAccount instance. 287 String facebookId = null; 288 FacebookAccount facebookAccount = null; 289 if (jaxb.getFacebookAccountRef() != null) { 290 facebookId = jaxb.getFacebookAccountRef().getRefid(); 291 } 292 if (facebookId != null) { 293 facebookAccount = this.facebooks.get(facebookId); 294 if (facebookAccount == null) { 295 throw new TickerLinguaException("Invalid FacebookAccountRef: " + facebookId); 296 } 297 } 298 String smsNumber = null; 299 if (jaxb.getSmsAccount() != null) { 300 smsNumber = jaxb.getSmsAccount().getNumber(); 301 } 302 303 String emailAccount = null; 304 if (jaxb.getEmailAccount() != null) { 305 emailAccount = jaxb.getEmailAccount().getAccount(); 306 } 307 // References are resolved, everything is OK, so define this user. 308 HackystatUser user = new HackystatUser(id, jaxb.getFullname(), jaxb.getShortname(), 309 emailAccount, service, jaxb.getHackystatAccount().getUser(), 310 jaxb.getHackystatAccount().getPassword(), twitterAccount, facebookAccount, 311 smsNumber); 312 // If that was successful, then add it to the users list. 313 this.users.put(id, user); 314 } 315 } 316 317 /** 318 * Defines a new HackystatProject. 319 * @throws TickerLinguaException If the id is a duplicate, or if the references to 320 * the HackystatService or HackystatUser cannot be resolved. 321 */ 322 private void processHackystatProjects() throws TickerLinguaException { 323 // Process HackystatProjects. Validate and resolve service and user references. 324 for (org.hackystat.tickertape.tickerlingua.jaxb.HackystatProject jaxb : 325 this.jaxbTickerLingua.getHackystatProjects().getHackystatProject()) { 326 String id = jaxb.getId(); 327 if (this.projects.containsKey(id)) { 328 throw new TickerLinguaException("Duplicate HackystatProject id: " + id); 329 } 330 String serviceId = jaxb.getHackystatserviceRefid(); 331 HackystatService service = this.services.get(serviceId); 332 if (service == null) { 333 throw new TickerLinguaException("Invalid HackystatService-refid: " + serviceId); 334 } 335 String ownerId = jaxb.getProjectownerRefid(); 336 HackystatUser owner = this.users.get(ownerId); 337 if (owner == null) { 338 throw new TickerLinguaException("Invalid ProjectOwner-refid: " + ownerId); 339 } 340 String authUserId = jaxb.getAuthuserRefid(); 341 HackystatUser authUser = this.users.get(authUserId); 342 if (authUser == null) { 343 throw new TickerLinguaException("Invalid AuthUser-refid: " + authUserId); 344 } 345 if (!authUser.hasPassword()) { 346 throw new TickerLinguaException("AuthUser must have a password: " + authUserId); 347 } 348 349 // Should be OK, so create it. 350 HackystatProject project = new HackystatProject(id, jaxb.getName(), jaxb.getShortname(), 351 service, owner, authUser, jaxb.getMailinglist()); 352 this.projects.put(id, project); 353 } 354 } 355 356 /** 357 * Defines the Tickertape instances from the JAXB. 358 * 359 * @throws TickerLinguaException If the id is a duplicate, or if the interval hours is not a 360 * double, or if enabled is not a boolean, or if a reference to a HackystatProject, HackystatUser, 361 * TwitterAccount, FacebookAccount, or Nabaztag cannot be resolved, or if the Ticker class 362 * cannot be found. 363 */ 364 private void processTickertapes() throws TickerLinguaException { 365 // Process HackystatProjects. Validate and resolve service and user references. 366 for (org.hackystat.tickertape.tickerlingua.jaxb.Tickertape jaxb : 367 this.jaxbTickerLingua.getTickertapes().getTickertape()) { 368 String id = jaxb.getId(); 369 if (this.tickertapes.containsKey(id)) { 370 throw new TickerLinguaException("Duplicate Tickertape id: " + id); 371 } 372 double intervalHours; 373 try { 374 intervalHours = Double.parseDouble(jaxb.getIntervalhours()); 375 } 376 catch (Exception e) { 377 throw new TickerLinguaException("Invalid intervalhours: " + jaxb.getIntervalhours(), e); 378 } 379 boolean enabled; 380 try { 381 enabled = Boolean.parseBoolean(jaxb.getEnabled()); 382 } 383 catch (Exception e) { 384 throw new TickerLinguaException("Invalid enabled: " + jaxb.getEnabled(), e); 385 } 386 // Create a list of all HackystatProjects listed. 387 List<HackystatProject> projects = new ArrayList<HackystatProject>(); 388 for (org.hackystat.tickertape.tickerlingua.jaxb.HackystatProjectRef projectRef : 389 jaxb.getHackystatProjectRef()) { 390 String refId = projectRef.getRefid(); 391 if (!this.projects.containsKey(refId)) { 392 throw new TickerLinguaException("Invalid HackystatProject refid: " + refId); 393 } 394 projects.add(this.projects.get(refId)); 395 } 396 397 // Create a list of all HackystatUsers listed. 398 List<HackystatUser> users = new ArrayList<HackystatUser>(); 399 for (org.hackystat.tickertape.tickerlingua.jaxb.HackystatUserRef userRef : 400 jaxb.getHackystatUserRef()) { 401 String refId = userRef.getRefid(); 402 if (!this.users.containsKey(refId)) { 403 throw new TickerLinguaException("Invalid HackystatUser refid: " + refId); 404 } 405 users.add(this.users.get(refId)); 406 } 407 408 // Create the notification services. 409 List<NotificationService> services = new ArrayList<NotificationService>(); 410 for (org.hackystat.tickertape.tickerlingua.jaxb.TwitterAccountRef ref : 411 jaxb.getTwitterAccountRef()) { 412 String refId = ref.getRefid(); 413 if (!this.twitters.containsKey(refId)) { 414 throw new TickerLinguaException("Invalid TwitterAccount refid: " + refId); 415 } 416 services.add(this.twitters.get(refId)); 417 } 418 for (org.hackystat.tickertape.tickerlingua.jaxb.FacebookAccountRef ref : 419 jaxb.getFacebookAccountRef()) { 420 String refId = ref.getRefid(); 421 if (!this.facebooks.containsKey(refId)) { 422 throw new TickerLinguaException("Invalid FacebookAccount refid: " + refId); 423 } 424 services.add(this.facebooks.get(refId)); 425 } 426 for (org.hackystat.tickertape.tickerlingua.jaxb.NabaztagRef ref : 427 jaxb.getNabaztagRef()) { 428 String refId = ref.getRefid(); 429 if (!this.nabaztags.containsKey(refId)) { 430 throw new TickerLinguaException("Invalid Nabaztag refid: " + refId); 431 } 432 services.add(this.nabaztags.get(refId)); 433 } 434 435 // Check the Ticker class. 436 String className = jaxb.getTicker().getClazz(); 437 Class<? extends Ticker> tickerClass; 438 try { 439 Class<?> c = Class.forName(className); 440 tickerClass = c.asSubclass(Ticker.class); 441 // Make an instance now to ensure it's OK. If problems will throw an exception. 442 Constructor<? extends Ticker> ctor = tickerClass.getConstructor(); 443 ctor.newInstance(); 444 } 445 catch (Exception e) { 446 throw new TickerLinguaException("Problem defining ticker class. " + className, e); 447 } 448 449 // Pass in a ticker properties instance. 450 Properties properties = new Properties(); 451 if ((jaxb.getTicker() != null) && (jaxb.getTicker().getProperties() != null)) { 452 properties = jaxb.getTicker().getProperties(); 453 } 454 455 Tickertape tickertape = new Tickertape(id, intervalHours, enabled, jaxb.getStarttime(), 456 jaxb.getDescription(), projects, services, tickerClass, properties); 457 this.tickertapes.put(id, tickertape); 458 } 459 } 460 461 /** 462 * Return the services. 463 * Package-private for testing. 464 * @return The services. 465 */ 466 Map<String, HackystatService> getServices() { 467 return this.services; 468 } 469 470 /** 471 * Return the services. 472 * Package-private for testing. 473 * @return The services. 474 */ 475 Map<String, HackystatUser> getUsers() { 476 return this.users; 477 } 478 479 /** 480 * Return the services. 481 * Package-private for testing. 482 * @return The services. 483 */ 484 Map<String, HackystatProject> getProjects() { 485 return this.projects; 486 } 487 488 /** 489 * Return the Tickertape instance with the specified ID, or null if not found. 490 * @param id The id. 491 * @return The Tickertape instance, or null. 492 */ 493 public Tickertape getTickertape(String id) { 494 return this.tickertapes.get(id); 495 } 496 497 /** 498 * Return a list of all Tickertape instances. 499 * @return The list of Tickertape instances. 500 */ 501 public List<Tickertape> getTickertapes () { 502 List<Tickertape> tickertapeList = new ArrayList<Tickertape>(); 503 tickertapeList.addAll(this.tickertapes.values()); 504 return tickertapeList; 505 } 506 507 /** 508 * Returns the set of defined HackystatUsers. 509 * @return The hackystat user definitions. 510 */ 511 public Collection<HackystatUser> getHackystatUsers() { 512 return this.users.values(); 513 } 514 515 /** 516 * Returns the full name associated with the email, or null if not found. 517 * @param email The email. 518 * @return The full name, or null if not found. 519 */ 520 public String findFullName(String email) { 521 for (HackystatUser user : this.users.values()) { 522 if ((user.getEmailAccount() != null) && user.getEmailAccount().equalsIgnoreCase(email)) { 523 return user.getFullName(); 524 } 525 } 526 return null; 527 } 528 529 }