001    package org.hackystat.tickertape.ticker;
002    
003    import java.util.Date;
004    import java.util.logging.Logger;
005    
006    import org.hackystat.sensorbase.client.SensorBaseClient;
007    import org.hackystat.tickertape.notifier.twitter.TwitterNotifier;
008    import org.hackystat.tickertape.tickerlingua.HackystatUser;
009    import org.hackystat.tickertape.tickerlingua.NotificationService;
010    import org.hackystat.tickertape.tickerlingua.TickerLingua;
011    import org.hackystat.tickertape.tickerlingua.Tickertape;
012    import org.hackystat.tickertape.tickerlingua.HackystatProject;
013    import org.hackystat.tickertape.ticker.data.MultiProjectSensorDataLog;
014    import org.hackystat.tickertape.ticker.data.ProjectSensorDataLog;
015    
016    /**
017     * Provides a Ticker that can monitor multiple Hackystat projects and send tweets to a single 
018     * Twitter account occasionally to summarize activity.  There is one tweet generated per user
019     * and per project. 
020     * @author Philip Johnson
021     */
022    public class MultiProjectTweetsTicker implements Ticker {
023      
024      private TwitterNotifier twitterNotifier;
025      private Tickertape tickertape;
026      private Logger logger;
027      private TickerLingua tickerLingua;
028      private MultiProjectSensorDataLog multiLog = new MultiProjectSensorDataLog();
029      
030      /** The string used as the key for the TimeBetweenTweets property. */
031      public static final String TIME_BETWEEN_TWEETS_KEY = "TimeBetweenTweets";
032    
033    
034      /**
035       * The run method for this ticker. 
036       * @param tickertape The Tickertape instance indicating what to do. 
037       * @param tickerLingua The TickerLingua instance with global data info.
038       * @param logger The logger to be used to communicate status. 
039       */
040      public void run(Tickertape tickertape, TickerLingua tickerLingua, Logger logger) {
041        logger.info("Running MultiProjectTweetsTicker...");
042        this.tickertape = tickertape;
043        this.logger = logger;
044        this.tickerLingua = tickerLingua;
045        notify(null);  // Force a tweet just the first time we run this Ticker. 
046        
047        // Process each project.
048        for (HackystatProject project : tickertape.getHackystatProjects()) {
049          this.logger.info("\n\nChecking status of project " + project.getName());
050          
051          // Find or create the ProjectSensorDataLog.
052          String projectName = project.getName();
053          String projectOwner = project.getHackystatOwner().getHackystatUserAccount();
054          HackystatUser authUser = project.getHackystatAuthUser();
055          SensorBaseClient client = authUser.getSensorBaseClient();
056          ProjectSensorDataLog log = multiLog.get(client, this.getMaxLife(), projectOwner, projectName, 
057              logger);
058          
059          // Now get any new data.
060          log.update();
061              
062          for (String user : log.getProjectParticipants()) {
063            if (log.hasRecentSensorData(user) && !log.hasRecentTweet(user)) {
064              String workedOnMsg = this.getWorkedOnMsg(log, user);
065              String builtMsg = this.getBuiltMsg(log, user);
066              String testMsg = this.getTestMsg(log, user);
067              String commitMsg = this.getCommitMsg(log, user);
068              String toolMsg = log.getToolString(user);
069              StringBuffer buff = new StringBuffer();
070              buff.append(getShortName(user)).append(' ');
071              if (workedOnMsg != null) {
072                buff.append(workedOnMsg).append(", ");
073              }
074              if (builtMsg != null) {
075                buff.append(builtMsg).append(", ");
076              }
077              if (testMsg != null) {
078                buff.append(testMsg).append(", ");
079              }
080              if (commitMsg != null) {
081                buff.append(commitMsg).append(", ");
082              }
083              if (toolMsg != null) {
084                buff.append("using ").append(toolMsg).append(" for ").append(project.getShortName());
085              }
086              notify(buff.toString());
087              log.setTweet(user);
088            }
089          }
090        }
091        logger.info("Finished running MultiProjectTweetsTicker.");
092      }
093      
094      /**
095       * Returns a string indicating the files worked on, or null if no files were worked on.
096       * @param log The ProjectSensorData log. 
097       * @param user The user of interest.
098       * @return The string indicating work, or null.
099       */
100      private String getWorkedOnMsg(ProjectSensorDataLog log, String user) {
101        String mostWorkedFile = log.mostWorkedOnFile(user);
102        return (mostWorkedFile == null) ?  null :
103            String.format("worked on %s file(s) (including %s)", log.getNumFilesWorkedOn(user),
104                mostWorkedFile);
105      }
106      
107      /**
108       * Returns a string indicating build activity.
109       * @param log The ProjectSensorDataLog.
110       * @param user The user. 
111       * @return A string indicating build activity, or null if there was none.
112       */
113      private String getBuiltMsg(ProjectSensorDataLog log, String user) {
114        int numBuilds = log.getSensorDataCount(user, "Build");
115        return (numBuilds == 0) ? null :
116          String.format("built %s time(s) (%s successful)", numBuilds, 
117              log.getBuildSuccessCount(user));
118      }
119      
120      
121      /**
122       * Returns a string indicating commit activity.
123       * @param log The ProjectSensorDataLog.
124       * @param user The user. 
125       * @return A string indicating commit activity, or null if there was none.
126       */
127      private String getCommitMsg(ProjectSensorDataLog log, String user) {
128        int numCommits = log.getSensorDataCount(user, "Commit");
129        return (numCommits == 0) ? null :
130          String.format("committed %s file(s)", numCommits); 
131      }
132      
133      /**
134       * Returns a string indicating build activity.
135       * @param log The ProjectSensorDataLog.
136       * @param user The user. 
137       * @return A string indicating build activity, or null if there was none.
138       */
139      private String getTestMsg(ProjectSensorDataLog log, String user) {
140        int numTests = log.getSensorDataCount(user, "UnitTest");
141        return (numTests == 0) ? null :
142          String.format("ran %s test(s) (%s passing)", numTests, log.getTestPassCount(user));
143      }
144      
145      
146      /**
147       * Returns the shortname associated with this email, or the email if the shortname could not be 
148       * found.
149       * @param email The email corresponding to a Hackystat user account.
150       * @return The short name or the email. 
151       */
152      private String getShortName(String email) {
153        for (HackystatUser user : this.tickerLingua.getHackystatUsers()) {
154          if (email.equals(user.getHackystatUserAccount())) {
155            return user.getShortName();
156          }
157        }
158        return email;
159      }
160      
161    
162      /**
163       * Sends a message to the Twitter account associated with this ticker, if the message is not null.
164       * @param message The message to be sent. 
165       */
166      private void notify(String message) {
167        if (this.twitterNotifier == null) {
168          NotificationService notifier = tickertape.getNotificationServices().get(0);
169          this.twitterNotifier = new TwitterNotifier(notifier.getId(), notifier.getPassword(), logger);
170          this.twitterNotifier.notify("Starting up a new TwitterNotifier at: " + new Date());
171        }
172        if (message != null) {
173          this.twitterNotifier.notify(message);
174        }
175      }
176      
177      /**
178       * Gets the maxLife by looking for the TimeBetweenTweets ticker property and converting it to
179       * a double.  Returns 0.5 (30 minutes) if the property is not found or could not be parsed.
180       * @return The maxLife. 
181       */
182      private double getMaxLife() {
183        double defaultMaxLife = 0.5;
184        String timeBetweenTweets = this.tickertape.getTickerProperties().get(TIME_BETWEEN_TWEETS_KEY);
185        if (timeBetweenTweets == null) {
186          return defaultMaxLife;
187        }
188        try {
189          return Double.parseDouble(timeBetweenTweets);
190        }
191        catch (Exception e) {
192          this.logger.warning("Error parsing TimeBetweenTweets: " + timeBetweenTweets);
193          return defaultMaxLife;
194        }
195      }
196    }