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    }