extensions/net.sf.basedb.reggie/trunk/src/net/sf/basedb/reggie/activity/ActivityLog.java

Code
Comments
Other
Rev Date Author Line
5376 23 Apr 19 nicklas 1 package net.sf.basedb.reggie.activity;
5376 23 Apr 19 nicklas 2
5762 28 Nov 19 nicklas 3 import java.io.InputStream;
5762 28 Nov 19 nicklas 4 import java.io.InputStreamReader;
5762 28 Nov 19 nicklas 5 import java.io.StringWriter;
5762 28 Nov 19 nicklas 6 import java.net.URL;
5762 28 Nov 19 nicklas 7 import java.net.URLConnection;
5404 08 May 19 nicklas 8 import java.nio.charset.Charset;
5762 28 Nov 19 nicklas 9 import java.nio.charset.StandardCharsets;
5376 23 Apr 19 nicklas 10 import java.time.LocalDateTime;
5380 24 Apr 19 nicklas 11 import java.time.ZoneId;
5376 23 Apr 19 nicklas 12 import java.time.temporal.ChronoUnit;
5376 23 Apr 19 nicklas 13 import java.util.ArrayList;
5376 23 Apr 19 nicklas 14 import java.util.Comparator;
5380 24 Apr 19 nicklas 15 import java.util.Date;
5376 23 Apr 19 nicklas 16 import java.util.Iterator;
5376 23 Apr 19 nicklas 17 import java.util.List;
5404 08 May 19 nicklas 18 import java.util.Locale;
5376 23 Apr 19 nicklas 19 import java.util.SortedSet;
5376 23 Apr 19 nicklas 20 import java.util.TimerTask;
5376 23 Apr 19 nicklas 21 import java.util.TreeSet;
5380 24 Apr 19 nicklas 22 import java.util.function.Predicate;
5376 23 Apr 19 nicklas 23
7025 08 Feb 23 nicklas 24 import org.apache.commons.lang3.time.FastDateFormat;
5404 08 May 19 nicklas 25 import org.jdom2.Document;
5404 08 May 19 nicklas 26 import org.jdom2.Element;
5404 08 May 19 nicklas 27 import org.jdom2.output.Format;
5404 08 May 19 nicklas 28 import org.jdom2.output.XMLOutputter;
5762 28 Nov 19 nicklas 29 import org.json.simple.JSONArray;
5762 28 Nov 19 nicklas 30 import org.json.simple.JSONObject;
5762 28 Nov 19 nicklas 31 import org.json.simple.parser.JSONParser;
5376 23 Apr 19 nicklas 32 import org.slf4j.LoggerFactory;
5376 23 Apr 19 nicklas 33
5404 08 May 19 nicklas 34 import net.sf.basedb.clients.web.extensions.ExtensionsControl;
5762 28 Nov 19 nicklas 35 import net.sf.basedb.core.AbsoluteProgressReporter;
5376 23 Apr 19 nicklas 36 import net.sf.basedb.core.Application;
5376 23 Apr 19 nicklas 37 import net.sf.basedb.reggie.Reggie;
5617 20 Sep 19 nicklas 38 import net.sf.basedb.reggie.XmlConfig;
5762 28 Nov 19 nicklas 39 import net.sf.basedb.util.FileUtil;
5376 23 Apr 19 nicklas 40 import net.sf.basedb.util.Values;
7080 27 Mar 23 nicklas 41 import net.sf.basedb.util.extensions.logging.ExtensionsLog;
7080 27 Mar 23 nicklas 42 import net.sf.basedb.util.extensions.logging.ExtensionsLogger;
5376 23 Apr 19 nicklas 43
5376 23 Apr 19 nicklas 44 /**
5376 23 Apr 19 nicklas 45   The activity log contains a sorted list of events that has happened
5376 23 Apr 19 nicklas 46   in the lab or in the analysis of sequenced samples.
5376 23 Apr 19 nicklas 47   
5617 20 Sep 19 nicklas 48   Old events are automatically cleared according to settings in reggie-config.xml/<activity-log>:
5376 23 Apr 19 nicklas 49   
5376 23 Apr 19 nicklas 50    * All events within the last two days are always kept
5617 20 Sep 19 nicklas 51    * All events older than 14 days are always removed
5617 20 Sep 19 nicklas 52    * Other events are kept but not more than 35
5376 23 Apr 19 nicklas 53   
5376 23 Apr 19 nicklas 54   The log is automatically saved to disc when needed.
5376 23 Apr 19 nicklas 55   
5376 23 Apr 19 nicklas 56    @author nicklas
5376 23 Apr 19 nicklas 57   @since 4.23
5376 23 Apr 19 nicklas 58 */
5376 23 Apr 19 nicklas 59 public class ActivityLog 
5376 23 Apr 19 nicklas 60 {
5376 23 Apr 19 nicklas 61
5376 23 Apr 19 nicklas 62   /**
5376 23 Apr 19 nicklas 63     Used for testing and debugging wizards that don't want to
5376 23 Apr 19 nicklas 64     call dc.commit() during the testing phase.
5376 23 Apr 19 nicklas 65     
5376 23 Apr 19 nicklas 66      If enabled, activities are inserted by both ActivityEntry.onAfterCommit()
5376 23 Apr 19 nicklas 67      and ActivityEntry.onRollback().
5376 23 Apr 19 nicklas 68      
5376 23 Apr 19 nicklas 69     Enable debug mode by adding the following to reggie-config.xml:
5376 23 Apr 19 nicklas 70       <activity-log>
5376 23 Apr 19 nicklas 71         <debug>1</debug>
5376 23 Apr 19 nicklas 72       </activity-log>
5376 23 Apr 19 nicklas 73   */
5376 23 Apr 19 nicklas 74   public static boolean debugMode()
5376 23 Apr 19 nicklas 75   {
5379 23 Apr 19 nicklas 76     return getInstance() != null && instance.debugMode;
5376 23 Apr 19 nicklas 77   }
5376 23 Apr 19 nicklas 78   
7080 27 Mar 23 nicklas 79   private static final ExtensionsLogger logger = 
7080 27 Mar 23 nicklas 80     ExtensionsLog.getLogger("net.sf.basedb.reggie.start-page", true).wrap(LoggerFactory.getLogger(ActivityLog.class));
5376 23 Apr 19 nicklas 81   
5376 23 Apr 19 nicklas 82   private static ActivityLog instance = null;
5376 23 Apr 19 nicklas 83   
5376 23 Apr 19 nicklas 84   /**
5376 23 Apr 19 nicklas 85     Get the singleton instance of the service. If the service has
5376 23 Apr 19 nicklas 86     not been created yet it is created at this time.
5376 23 Apr 19 nicklas 87   */
5376 23 Apr 19 nicklas 88   public static final ActivityLog getInstance()
5376 23 Apr 19 nicklas 89   {
5376 23 Apr 19 nicklas 90     if (instance == null)
5376 23 Apr 19 nicklas 91     {
5376 23 Apr 19 nicklas 92       synchronized (ActivityLog.class)
5376 23 Apr 19 nicklas 93       {
5376 23 Apr 19 nicklas 94         if (instance == null)
5376 23 Apr 19 nicklas 95         {
5376 23 Apr 19 nicklas 96           logger.debug("Starting up the activity logger...");
5376 23 Apr 19 nicklas 97           ActivityLog tmp = new ActivityLog();
5380 24 Apr 19 nicklas 98           tmp.init();
5376 23 Apr 19 nicklas 99           instance = tmp;
5376 23 Apr 19 nicklas 100           logger.debug("The activity logger is up and running " + (tmp.debugMode ? " (debug mode)" : ""));
5376 23 Apr 19 nicklas 101         }
5376 23 Apr 19 nicklas 102       }
5376 23 Apr 19 nicklas 103     }
5376 23 Apr 19 nicklas 104     return instance;
5376 23 Apr 19 nicklas 105   }
5376 23 Apr 19 nicklas 106   
5385 26 Apr 19 nicklas 107   /**
5385 26 Apr 19 nicklas 108     Name of log file where activity entries are persisted.
5385 26 Apr 19 nicklas 109     NOTE!! If changes are made to any of the classes holding 
5385 26 Apr 19 nicklas 110     activity information that affects the serializability
5385 26 Apr 19 nicklas 111     we need to change the filename here since otherwise the
5385 26 Apr 19 nicklas 112     old file will not be loaded.
5385 26 Apr 19 nicklas 113   */
5385 26 Apr 19 nicklas 114   private static final String LOG_FILE = "reggie/activity-log-v1";
5385 26 Apr 19 nicklas 115
5376 23 Apr 19 nicklas 116   private final SortedSet<ActivityEntry> activities;
5376 23 Apr 19 nicklas 117   private volatile TimerTask timer;
5376 23 Apr 19 nicklas 118   private boolean debugMode = false;
5617 20 Sep 19 nicklas 119   private int maxAgeInDays = 14;
5617 20 Sep 19 nicklas 120   private int maxEntries = 35;
5762 28 Nov 19 nicklas 121   private int maxQuoteAgeInSeconds = 3600 * 12; //  12 hours is the default
5762 28 Nov 19 nicklas 122   private URL quoteUrl;
5376 23 Apr 19 nicklas 123   
5404 08 May 19 nicklas 124   private volatile byte[] rssFeed;
5761 28 Nov 19 nicklas 125   private volatile QuoteOfTheDay quoteOfTheDay;
5404 08 May 19 nicklas 126   
5376 23 Apr 19 nicklas 127   private ActivityLog()
5376 23 Apr 19 nicklas 128   {
5380 24 Apr 19 nicklas 129     activities = new TreeSet<>(new ActivityComparator());
5380 24 Apr 19 nicklas 130   }
5380 24 Apr 19 nicklas 131   
5380 24 Apr 19 nicklas 132   /**
5380 24 Apr 19 nicklas 133     Initialize the activity log by reading stored events from  the
5380 24 Apr 19 nicklas 134     disk, filtering out old events, and then scheduling a timer
5380 24 Apr 19 nicklas 135     for storing them back again.
5380 24 Apr 19 nicklas 136   */
5380 24 Apr 19 nicklas 137   private void init()
5380 24 Apr 19 nicklas 138   {
5380 24 Apr 19 nicklas 139     reloadConfig();
5376 23 Apr 19 nicklas 140     readActivities();
5380 24 Apr 19 nicklas 141     asyncStoreActivities();
5376 23 Apr 19 nicklas 142   }
5376 23 Apr 19 nicklas 143   
5376 23 Apr 19 nicklas 144   /**
5376 23 Apr 19 nicklas 145     Add an activity to the log. If the activity is mergeable
5376 23 Apr 19 nicklas 146     the log will try to merge it with other activities on the
5376 23 Apr 19 nicklas 147     same day.
5376 23 Apr 19 nicklas 148   */
5376 23 Apr 19 nicklas 149   public synchronized void addActivity(ActivityEntry activity)
5376 23 Apr 19 nicklas 150   {
5376 23 Apr 19 nicklas 151     if (activity.canMerge())
5376 23 Apr 19 nicklas 152     {
5376 23 Apr 19 nicklas 153       LocalDateTime activityDay = activity.getEventDay();
5376 23 Apr 19 nicklas 154       Iterator<ActivityEntry> it = activities.iterator();
5376 23 Apr 19 nicklas 155       while (it.hasNext())
5376 23 Apr 19 nicklas 156       {
5376 23 Apr 19 nicklas 157         ActivityEntry other = it.next();
5376 23 Apr 19 nicklas 158         LocalDateTime otherDay = other.getEventDay();
5376 23 Apr 19 nicklas 159         // If the events are on the same day...
5376 23 Apr 19 nicklas 160         if (ChronoUnit.DAYS.between(activityDay, otherDay) == 0)
5376 23 Apr 19 nicklas 161         {
5376 23 Apr 19 nicklas 162           // and if the events can be merged...
5376 23 Apr 19 nicklas 163           if (activity.merge(other))
5376 23 Apr 19 nicklas 164           {
5376 23 Apr 19 nicklas 165             // we remove the existing event and replace it with the new event
5376 23 Apr 19 nicklas 166             it.remove();
5376 23 Apr 19 nicklas 167             break;
5376 23 Apr 19 nicklas 168           }
5376 23 Apr 19 nicklas 169         }
5376 23 Apr 19 nicklas 170       }
5376 23 Apr 19 nicklas 171     }
5376 23 Apr 19 nicklas 172     activities.add(activity);
5376 23 Apr 19 nicklas 173     asyncStoreActivities();
5376 23 Apr 19 nicklas 174   }
5376 23 Apr 19 nicklas 175   
5376 23 Apr 19 nicklas 176   /**
5376 23 Apr 19 nicklas 177     Get all activities in the log as a list. The list is sorted 
5376 23 Apr 19 nicklas 178     latest first.
5376 23 Apr 19 nicklas 179   */
5376 23 Apr 19 nicklas 180   public synchronized List<ActivityEntry> getActivities()
5376 23 Apr 19 nicklas 181   {
5376 23 Apr 19 nicklas 182     return new ArrayList<>(activities);
5376 23 Apr 19 nicklas 183   }
5376 23 Apr 19 nicklas 184   
5404 08 May 19 nicklas 185   /**
5404 08 May 19 nicklas 186     Get the activity log as an RSS feed. The feed is returned
5404 08 May 19 nicklas 187     as a byte array ready to be streamed to the receiver.
5404 08 May 19 nicklas 188   */
5404 08 May 19 nicklas 189   public byte[] getRssFeed(String serverRoot, String contextRoot)
5404 08 May 19 nicklas 190   {
5404 08 May 19 nicklas 191     if (rssFeed == null) createRssFeed(serverRoot, contextRoot);
5404 08 May 19 nicklas 192     return rssFeed;
5404 08 May 19 nicklas 193   }
5376 23 Apr 19 nicklas 194   
5376 23 Apr 19 nicklas 195   /**
5376 23 Apr 19 nicklas 196     Re-load configuration settings for the activity log.
5376 23 Apr 19 nicklas 197   */
5376 23 Apr 19 nicklas 198   public synchronized void reloadConfig()
5376 23 Apr 19 nicklas 199   {
5617 20 Sep 19 nicklas 200     XmlConfig cfg = Reggie.getConfig();
5617 20 Sep 19 nicklas 201     debugMode = Values.getBoolean(cfg.getConfig("activity-log/debug"));
5617 20 Sep 19 nicklas 202     maxAgeInDays = Values.getInt(cfg.getConfig("activity-log/max-age-in-days"), 14);
5617 20 Sep 19 nicklas 203     maxEntries = Values.getInt(cfg.getConfig("activity-log/max-entries"), 35);
5762 28 Nov 19 nicklas 204     maxQuoteAgeInSeconds = Values.getInt(cfg.getConfig("activity-log/quote-of-the-day/max-age-in-seconds"), 12*3600);
5762 28 Nov 19 nicklas 205     try
5762 28 Nov 19 nicklas 206     {
5762 28 Nov 19 nicklas 207       quoteUrl = new URL(cfg.getConfig("activity-log/quote-of-the-day/url"));
5762 28 Nov 19 nicklas 208     }
5762 28 Nov 19 nicklas 209     catch (Exception ex)
5762 28 Nov 19 nicklas 210     {
5762 28 Nov 19 nicklas 211       quoteUrl = null;
5762 28 Nov 19 nicklas 212     }
5376 23 Apr 19 nicklas 213   }
5376 23 Apr 19 nicklas 214
5761 28 Nov 19 nicklas 215   public QuoteOfTheDay getQuoteOfTheDay()
5761 28 Nov 19 nicklas 216   {
5762 28 Nov 19 nicklas 217     if (quoteUrl == null) return null; // Disabled
5762 28 Nov 19 nicklas 218     
5762 28 Nov 19 nicklas 219     if (quoteOfTheDay == null || quoteOfTheDay.getAgeInSeconds() > maxQuoteAgeInSeconds)
5761 28 Nov 19 nicklas 220     {
5762 28 Nov 19 nicklas 221       synchronized (quoteUrl)
5762 28 Nov 19 nicklas 222       {
5762 28 Nov 19 nicklas 223         if (quoteOfTheDay == null || quoteOfTheDay.getAgeInSeconds() > maxQuoteAgeInSeconds)
5762 28 Nov 19 nicklas 224         {
5762 28 Nov 19 nicklas 225           InputStream in = null;
5762 28 Nov 19 nicklas 226           QuoteOfTheDay tmp = new QuoteOfTheDay();
5762 28 Nov 19 nicklas 227           try
5762 28 Nov 19 nicklas 228           {
5762 28 Nov 19 nicklas 229             // Read data from the URL, if external we have a fixed limit
5762 28 Nov 19 nicklas 230             AbsoluteProgressReporter maxDataFilter = quoteUrl.getProtocol().startsWith("http") ? new MaxDownloadFilter(5000) : null;
5762 28 Nov 19 nicklas 231             logger.debug("Loading quote from " + quoteUrl);
5762 28 Nov 19 nicklas 232             URLConnection conn = quoteUrl.openConnection();
5762 28 Nov 19 nicklas 233             conn.setConnectTimeout(3000);
5762 28 Nov 19 nicklas 234             conn.setReadTimeout(3000);
5762 28 Nov 19 nicklas 235             in = conn.getInputStream();
5762 28 Nov 19 nicklas 236             StringWriter sout = new StringWriter();
5762 28 Nov 19 nicklas 237             FileUtil.copy(new InputStreamReader(in, StandardCharsets.UTF_8), sout, maxDataFilter);
5762 28 Nov 19 nicklas 238             logger.debug("A quote was loaded from " + quoteUrl + "; bytes="+sout.getBuffer().length());
5762 28 Nov 19 nicklas 239             
5762 28 Nov 19 nicklas 240             // Parse as JSON { "contents": { "quotes": [ {"quote": "...", "author": "..." }, {...} ] }}
5762 28 Nov 19 nicklas 241             JSONObject json = (JSONObject)new JSONParser().parse(sout.toString());
5762 28 Nov 19 nicklas 242             json = (JSONObject)json.get("contents");
5762 28 Nov 19 nicklas 243             JSONArray jsonQuotes = (JSONArray)json.get("quotes");
5762 28 Nov 19 nicklas 244             
5762 28 Nov 19 nicklas 245             int index = (int)Math.floor(Math.random()*jsonQuotes.size());
5762 28 Nov 19 nicklas 246             JSONObject jsonQuote = (JSONObject)jsonQuotes.get(index);        
5762 28 Nov 19 nicklas 247             String quote = (String)jsonQuote.get("quote");
5762 28 Nov 19 nicklas 248             String author = (String)jsonQuote.get("author");
5762 28 Nov 19 nicklas 249             
5762 28 Nov 19 nicklas 250             // Check that we have values of acceptable lengths            
5762 28 Nov 19 nicklas 251             if (quote != null && author != null && quote.length() < 160 && author.length() < 40)
5762 28 Nov 19 nicklas 252             {
5762 28 Nov 19 nicklas 253               tmp.setAuthor(author);
5762 28 Nov 19 nicklas 254               tmp.setQuote(quote);
5762 28 Nov 19 nicklas 255               logger.debug("A quote was successfully loaded from "+ quoteUrl);
5762 28 Nov 19 nicklas 256             }
5762 28 Nov 19 nicklas 257           }
5762 28 Nov 19 nicklas 258           catch (Exception ex)
5762 28 Nov 19 nicklas 259           {
5762 28 Nov 19 nicklas 260             logger.warn("Could not load quote-of-the-day", ex);
5762 28 Nov 19 nicklas 261           }
5762 28 Nov 19 nicklas 262           finally
5762 28 Nov 19 nicklas 263           {
5762 28 Nov 19 nicklas 264             FileUtil.close(in);
5762 28 Nov 19 nicklas 265             quoteOfTheDay = tmp;
5762 28 Nov 19 nicklas 266           }
5762 28 Nov 19 nicklas 267         }
5762 28 Nov 19 nicklas 268       }
5761 28 Nov 19 nicklas 269     }
5762 28 Nov 19 nicklas 270     else if (logger.isDebugEnabled() || true)
5762 28 Nov 19 nicklas 271     {
5762 28 Nov 19 nicklas 272       logger.debug("Using existing quote; age="+quoteOfTheDay.getAgeInSeconds());
5762 28 Nov 19 nicklas 273     }
5762 28 Nov 19 nicklas 274     return quoteOfTheDay == null || quoteOfTheDay.getQuote() == null ? null : quoteOfTheDay;
5761 28 Nov 19 nicklas 275   }
5376 23 Apr 19 nicklas 276   
5376 23 Apr 19 nicklas 277   /**
5376 23 Apr 19 nicklas 278     Read activities from the disc. Called once when the
5376 23 Apr 19 nicklas 279     activity log is started up.
5376 23 Apr 19 nicklas 280   */
5380 24 Apr 19 nicklas 281   private void readActivities()
5376 23 Apr 19 nicklas 282   {
5385 26 Apr 19 nicklas 283     if (Application.getStaticCache().exists(LOG_FILE))
5376 23 Apr 19 nicklas 284     {
5385 26 Apr 19 nicklas 285       logger.debug("Reading activities from <static-cache>/"+LOG_FILE+"...");
5385 26 Apr 19 nicklas 286       try
5385 26 Apr 19 nicklas 287       {
5385 26 Apr 19 nicklas 288         List<ActivityEntry> tmp = Application.getStaticCache().load(LOG_FILE, Reggie.class.getClassLoader(), 10);
5385 26 Apr 19 nicklas 289         logger.debug("Loaded " + tmp.size() + " activities from <static-cache>/"+LOG_FILE);
5617 20 Sep 19 nicklas 290         tmp.removeIf(new ActivityFilter(maxAgeInDays, maxEntries));
5385 26 Apr 19 nicklas 291         logger.debug("Kept " + tmp.size() + " activities after filtering");
5385 26 Apr 19 nicklas 292         tmp.toString(); // This will trigger an exception if not all activities have been loaded correctly
5385 26 Apr 19 nicklas 293         activities.addAll(tmp);
5385 26 Apr 19 nicklas 294       }
5385 26 Apr 19 nicklas 295       catch (RuntimeException ex)
5385 26 Apr 19 nicklas 296       {
5385 26 Apr 19 nicklas 297         activities.clear();
5385 26 Apr 19 nicklas 298         logger.error("Failed to load activities from <static-cache>/"+LOG_FILE, ex);
5385 26 Apr 19 nicklas 299       }
5376 23 Apr 19 nicklas 300     }
5376 23 Apr 19 nicklas 301     else
5376 23 Apr 19 nicklas 302     {
5385 26 Apr 19 nicklas 303       logger.debug("No current activities in <static-cache>/"+LOG_FILE);
5376 23 Apr 19 nicklas 304     }
5376 23 Apr 19 nicklas 305   }
5376 23 Apr 19 nicklas 306   
5376 23 Apr 19 nicklas 307   /**
5380 24 Apr 19 nicklas 308     Filter the activity log.
5380 24 Apr 19 nicklas 309   */
5380 24 Apr 19 nicklas 310   private void filterActivities()
5380 24 Apr 19 nicklas 311   {
5380 24 Apr 19 nicklas 312     logger.debug("Preparing to filter activities ("+activities.size()+")...");
5617 20 Sep 19 nicklas 313     activities.removeIf(new ActivityFilter(maxAgeInDays, maxEntries));
5380 24 Apr 19 nicklas 314     logger.debug("Filter activities completed ("+activities.size()+")...");
5380 24 Apr 19 nicklas 315   }
5380 24 Apr 19 nicklas 316
5380 24 Apr 19 nicklas 317   /**
5376 23 Apr 19 nicklas 318     Schedule a timer for storing the current list of activities. The activities
5376 23 Apr 19 nicklas 319     are typically stored within a few seconds. Calling this method again before
5376 23 Apr 19 nicklas 320     the list is stored is ignored.
5376 23 Apr 19 nicklas 321   */
5376 23 Apr 19 nicklas 322   void asyncStoreActivities()
5376 23 Apr 19 nicklas 323   {
5376 23 Apr 19 nicklas 324     if (timer == null)
5376 23 Apr 19 nicklas 325     {
5376 23 Apr 19 nicklas 326       logger.debug("Scheduling save of current activity log.");
5376 23 Apr 19 nicklas 327       timer = Application.getScheduler().schedule(new SaveActivityLog(), 5000, true);
5376 23 Apr 19 nicklas 328     }
5376 23 Apr 19 nicklas 329   }
5376 23 Apr 19 nicklas 330   
5376 23 Apr 19 nicklas 331   /**
5380 24 Apr 19 nicklas 332     Write the activities to disc.
5376 23 Apr 19 nicklas 333   */
5380 24 Apr 19 nicklas 334   synchronized void storeActivities()
5376 23 Apr 19 nicklas 335   {
5380 24 Apr 19 nicklas 336     logger.debug("Preparing to save activites...");
5380 24 Apr 19 nicklas 337     filterActivities();
5376 23 Apr 19 nicklas 338     List<ActivityEntry> log = getActivities();
5385 26 Apr 19 nicklas 339     Application.getStaticCache().store(LOG_FILE, log, 10);
5404 08 May 19 nicklas 340     rssFeed = null;
5385 26 Apr 19 nicklas 341     logger.debug("Saved " + log.size() + " activities to <static-cache>/"+LOG_FILE);
5376 23 Apr 19 nicklas 342     // Important! Nullify the timer so that it can be activated the next time addActivity is called
5376 23 Apr 19 nicklas 343     timer = null;
5376 23 Apr 19 nicklas 344   }
5376 23 Apr 19 nicklas 345   
5404 08 May 19 nicklas 346   private synchronized void createRssFeed(String serverRoot, String contextRoot)
5404 08 May 19 nicklas 347   {
5404 08 May 19 nicklas 348     if (rssFeed != null) return;
5404 08 May 19 nicklas 349     
5404 08 May 19 nicklas 350     List<ActivityEntry> log = getActivities();
5404 08 May 19 nicklas 351     
5404 08 May 19 nicklas 352     // Generate the XML document
5404 08 May 19 nicklas 353     String name = contextRoot.replace("/", ""); // Get rid of '/'
5404 08 May 19 nicklas 354     name = name.substring(0, 1).toUpperCase()+name.substring(1);
5404 08 May 19 nicklas 355     String channelTitle = name + " Activity Log";
7025 08 Feb 23 nicklas 356     FastDateFormat rfc822 = FastDateFormat.getInstance("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", Locale.US);
5404 08 May 19 nicklas 357   
5404 08 May 19 nicklas 358     // <rss version="2.0">
5404 08 May 19 nicklas 359     Element rss = new Element("rss").setAttribute("version", "2.0");
5404 08 May 19 nicklas 360     // <channel>
5404 08 May 19 nicklas 361     Element channel = new Element("channel");
5404 08 May 19 nicklas 362     // ..<title>.</title>
5404 08 May 19 nicklas 363     channel.addContent(new Element("title").setText(channelTitle));
5404 08 May 19 nicklas 364     // ..<link>.</link>
5404 08 May 19 nicklas 365     channel.addContent(new Element("link").setText(serverRoot+contextRoot));
5404 08 May 19 nicklas 366     // ..<description>.</description>
5404 08 May 19 nicklas 367 //    channel.addContent(new Element("description").setText(about));
5404 08 May 19 nicklas 368     // ..<generator>.</generator>
5404 08 May 19 nicklas 369     channel.addContent(new Element("generator").setText("Reggie "+Reggie.VERSION));
5404 08 May 19 nicklas 370     // ..<pubDate>.</pubDate>
5404 08 May 19 nicklas 371     String now = rfc822.format(new Date());
5404 08 May 19 nicklas 372     String buildDate = log.size() == 0 ? now : rfc822.format(log.get(0).getEventDate());
5404 08 May 19 nicklas 373     channel.addContent(new Element("pubDate").setText(now));
5404 08 May 19 nicklas 374     // ..<lastBuildDate>.</lastBuildDate>
5404 08 May 19 nicklas 375     channel.addContent(new Element("lastBuildDate").setText(buildDate));
5404 08 May 19 nicklas 376
5404 08 May 19 nicklas 377     // ..<image>
5404 08 May 19 nicklas 378     Element image = new Element("image");
5404 08 May 19 nicklas 379     // ....<url>.</url>
5404 08 May 19 nicklas 380     String home = ExtensionsControl.getHomeUrl("net.sf.basedb.reggie");
5404 08 May 19 nicklas 381     image.addContent(new Element("url").setText(serverRoot+home+"/images/snake-icon-64.png"));
5404 08 May 19 nicklas 382     // ....<title>.</title>
5404 08 May 19 nicklas 383     image.addContent(new Element("title").setText(channelTitle));
5404 08 May 19 nicklas 384     // ....<link>.</link>
5404 08 May 19 nicklas 385     image.addContent(new Element("link").setText(serverRoot+contextRoot));
5404 08 May 19 nicklas 386     // ..</image>
5404 08 May 19 nicklas 387     channel.addContent(image);
5404 08 May 19 nicklas 388     
5404 08 May 19 nicklas 389     // </channel>
5404 08 May 19 nicklas 390     rss.addContent(channel);
5404 08 May 19 nicklas 391     
5404 08 May 19 nicklas 392     // Each News item goes into an <item> tag
5404 08 May 19 nicklas 393     long lastTime = 0;
5404 08 May 19 nicklas 394     int sameTimeCounter = 0;
5404 08 May 19 nicklas 395     for (ActivityEntry a : log)
5404 08 May 19 nicklas 396     {
5404 08 May 19 nicklas 397       long time = a.getEventDate().getTime();
5404 08 May 19 nicklas 398       String guid = "T"+Long.toString(time); // The 'guid' should be unique
5404 08 May 19 nicklas 399       if (time == lastTime)
5404 08 May 19 nicklas 400       {
5404 08 May 19 nicklas 401         sameTimeCounter++;
5404 08 May 19 nicklas 402       }
5404 08 May 19 nicklas 403       else
5404 08 May 19 nicklas 404       {
5404 08 May 19 nicklas 405         sameTimeCounter = 0;
5404 08 May 19 nicklas 406       }
5404 08 May 19 nicklas 407       guid += "I"+sameTimeCounter;
5404 08 May 19 nicklas 408       
5404 08 May 19 nicklas 409       // <item>
5404 08 May 19 nicklas 410       Element item = new Element("item");
5404 08 May 19 nicklas 411       // ..<guid isPermaLink="false">.</guid>
5404 08 May 19 nicklas 412       item.addContent(new Element("guid").setAttribute("isPermaLink", "false").setText(guid));
5404 08 May 19 nicklas 413       // ..<title>.</title>
5404 08 May 19 nicklas 414       item.addContent(new Element("title").setText(a.getMessage()));
5421 13 May 19 nicklas 415       if (a.getUser() != null)
5421 13 May 19 nicklas 416       {
5421 13 May 19 nicklas 417         // ..<author>.</author>
5421 13 May 19 nicklas 418         item.addContent(new Element("author").setText(a.getUser()));
5421 13 May 19 nicklas 419       }
5404 08 May 19 nicklas 420       // ..<pubDate>.</pubDate>
5404 08 May 19 nicklas 421       item.addContent(new Element("pubDate").setText(rfc822.format(a.getEventDate())));
5404 08 May 19 nicklas 422       // </item>
5404 08 May 19 nicklas 423       channel.addContent(item);
5404 08 May 19 nicklas 424     }
5404 08 May 19 nicklas 425     
5404 08 May 19 nicklas 426     Document dom = new Document(rss);
5404 08 May 19 nicklas 427     String xml = new XMLOutputter(Format.getPrettyFormat()).outputString(dom);
5404 08 May 19 nicklas 428     rssFeed = xml.getBytes(Charset.forName("UTF-8"));
5404 08 May 19 nicklas 429   }
5404 08 May 19 nicklas 430   
5404 08 May 19 nicklas 431   
5376 23 Apr 19 nicklas 432   @Override
5376 23 Apr 19 nicklas 433   public String toString() 
5376 23 Apr 19 nicklas 434   {
5376 23 Apr 19 nicklas 435     return Values.getString(activities, "\n", true);
5376 23 Apr 19 nicklas 436   }
5376 23 Apr 19 nicklas 437   
5376 23 Apr 19 nicklas 438   /**
5376 23 Apr 19 nicklas 439     Compartor for sorting activity events latest first. Events with exactly the
5376 23 Apr 19 nicklas 440     same date+time are sorted by message.
5376 23 Apr 19 nicklas 441   */
5376 23 Apr 19 nicklas 442   static class ActivityComparator
5376 23 Apr 19 nicklas 443     implements Comparator<ActivityEntry>
5376 23 Apr 19 nicklas 444   {
5376 23 Apr 19 nicklas 445
5376 23 Apr 19 nicklas 446     @Override
5376 23 Apr 19 nicklas 447     public int compare(ActivityEntry o1, ActivityEntry o2) 
5376 23 Apr 19 nicklas 448     {
5376 23 Apr 19 nicklas 449       int dateCompare = o2.getEventDate().compareTo(o1.getEventDate()); // NOTE! Latest is sorted first
5376 23 Apr 19 nicklas 450       return dateCompare == 0 ? o1.getMessage().compareTo(o2.getMessage()) : dateCompare;
5376 23 Apr 19 nicklas 451     }
5376 23 Apr 19 nicklas 452   }
5376 23 Apr 19 nicklas 453   
5376 23 Apr 19 nicklas 454   /**
5380 24 Apr 19 nicklas 455     Filter the activity log.
5380 24 Apr 19 nicklas 456     We keep all events from today and yesterday
5617 20 Sep 19 nicklas 457     Events older than 'maxAgeInDays' weeks are removed
5617 20 Sep 19 nicklas 458     Events between 2-maxAgeInDays days are kept but not more than 'maxEntries'
5380 24 Apr 19 nicklas 459   */
5380 24 Apr 19 nicklas 460   static class ActivityFilter
5380 24 Apr 19 nicklas 461     implements Predicate<ActivityEntry>
5380 24 Apr 19 nicklas 462   {
5380 24 Apr 19 nicklas 463
5380 24 Apr 19 nicklas 464     private final LocalDateTime today;
5617 20 Sep 19 nicklas 465     private final int maxAgeInDays;
5617 20 Sep 19 nicklas 466     private final int maxEntries;
5617 20 Sep 19 nicklas 467     
5380 24 Apr 19 nicklas 468     private int numPassed;
5380 24 Apr 19 nicklas 469     
5617 20 Sep 19 nicklas 470     ActivityFilter(int maxAgeInDays, int maxEntries)
5380 24 Apr 19 nicklas 471     {
5380 24 Apr 19 nicklas 472       this.today = LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS);
5617 20 Sep 19 nicklas 473       this.maxAgeInDays = maxAgeInDays;
5617 20 Sep 19 nicklas 474       this.maxEntries = maxEntries;
5380 24 Apr 19 nicklas 475     }
5380 24 Apr 19 nicklas 476     
5380 24 Apr 19 nicklas 477     @Override
5380 24 Apr 19 nicklas 478     public boolean test(ActivityEntry a)
5380 24 Apr 19 nicklas 479     {
5380 24 Apr 19 nicklas 480       int ageInDays = (int)ChronoUnit.DAYS.between(a.getEventDay(), today);
5617 20 Sep 19 nicklas 481       boolean remove = ageInDays > maxAgeInDays || (ageInDays > 1 && numPassed >= maxEntries);
5740 20 Nov 19 nicklas 482       if (a.getMessage().contains("Reggie the Snake") && a.getAgeInSeconds() > 180)
5740 20 Nov 19 nicklas 483       {
5740 20 Nov 19 nicklas 484         // Reggie the Snake never stays more than 3 minutes
5740 20 Nov 19 nicklas 485         remove = true;
5740 20 Nov 19 nicklas 486       }
5380 24 Apr 19 nicklas 487       if (!remove) numPassed++;
5380 24 Apr 19 nicklas 488       return remove;
5380 24 Apr 19 nicklas 489     }
5380 24 Apr 19 nicklas 490   }
5380 24 Apr 19 nicklas 491   
5380 24 Apr 19 nicklas 492   /**
5376 23 Apr 19 nicklas 493     Task that is responsible for saving the activity log
5376 23 Apr 19 nicklas 494     to the disc at regular intervals.
5376 23 Apr 19 nicklas 495   */
5740 20 Nov 19 nicklas 496   public static class SaveActivityLog
5376 23 Apr 19 nicklas 497     extends TimerTask
5376 23 Apr 19 nicklas 498   {
5740 20 Nov 19 nicklas 499     public SaveActivityLog() 
5376 23 Apr 19 nicklas 500     {}
5376 23 Apr 19 nicklas 501     
5376 23 Apr 19 nicklas 502     @Override
5376 23 Apr 19 nicklas 503     public void run()
5376 23 Apr 19 nicklas 504     {
5376 23 Apr 19 nicklas 505       try
5376 23 Apr 19 nicklas 506       {
5380 24 Apr 19 nicklas 507         getInstance().storeActivities();
5376 23 Apr 19 nicklas 508       }
5376 23 Apr 19 nicklas 509       catch (Exception ex)
5376 23 Apr 19 nicklas 510       {
5376 23 Apr 19 nicklas 511         logger.error("Exception when saving activity log", ex);
5376 23 Apr 19 nicklas 512       }
5376 23 Apr 19 nicklas 513     }
5376 23 Apr 19 nicklas 514   
5376 23 Apr 19 nicklas 515   }
5376 23 Apr 19 nicklas 516
5762 28 Nov 19 nicklas 517   /**
5762 28 Nov 19 nicklas 518     To prevent external data overflow.
5762 28 Nov 19 nicklas 519   */
5762 28 Nov 19 nicklas 520   static class MaxDownloadFilter
5762 28 Nov 19 nicklas 521     implements AbsoluteProgressReporter
5762 28 Nov 19 nicklas 522   {
5762 28 Nov 19 nicklas 523   
5762 28 Nov 19 nicklas 524     private final long maxBytes;
5762 28 Nov 19 nicklas 525     
5762 28 Nov 19 nicklas 526     public MaxDownloadFilter(long maxBytes) 
5762 28 Nov 19 nicklas 527     {
5762 28 Nov 19 nicklas 528       this.maxBytes = maxBytes;
5762 28 Nov 19 nicklas 529     }
5762 28 Nov 19 nicklas 530     
5762 28 Nov 19 nicklas 531     @Override
5762 28 Nov 19 nicklas 532     public void display(int percent, String message) 
5762 28 Nov 19 nicklas 533     {}
5762 28 Nov 19 nicklas 534   
5762 28 Nov 19 nicklas 535     @Override
5762 28 Nov 19 nicklas 536     public void append(String message) 
5762 28 Nov 19 nicklas 537     {}
5762 28 Nov 19 nicklas 538   
5762 28 Nov 19 nicklas 539     @Override
5762 28 Nov 19 nicklas 540     public void displayAbsolute(long completed, String message) 
5762 28 Nov 19 nicklas 541     {
5762 28 Nov 19 nicklas 542       if (completed > maxBytes)
5762 28 Nov 19 nicklas 543       {
5762 28 Nov 19 nicklas 544         throw new RuntimeException("Too much data: " + completed + "; max allowed: " + maxBytes);
5762 28 Nov 19 nicklas 545       }
5762 28 Nov 19 nicklas 546     }
5762 28 Nov 19 nicklas 547   
5762 28 Nov 19 nicklas 548   }
5762 28 Nov 19 nicklas 549 }