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 |
The activity log contains a sorted list of events that has happened |
5376 |
23 Apr 19 |
nicklas |
in the lab or in the analysis of sequenced samples. |
5376 |
23 Apr 19 |
nicklas |
47 |
|
5617 |
20 Sep 19 |
nicklas |
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 |
* All events within the last two days are always kept |
5617 |
20 Sep 19 |
nicklas |
* All events older than 14 days are always removed |
5617 |
20 Sep 19 |
nicklas |
* Other events are kept but not more than 35 |
5376 |
23 Apr 19 |
nicklas |
53 |
|
5376 |
23 Apr 19 |
nicklas |
The log is automatically saved to disc when needed. |
5376 |
23 Apr 19 |
nicklas |
55 |
|
5376 |
23 Apr 19 |
nicklas |
@author nicklas |
5376 |
23 Apr 19 |
nicklas |
@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 |
Used for testing and debugging wizards that don't want to |
5376 |
23 Apr 19 |
nicklas |
call dc.commit() during the testing phase. |
5376 |
23 Apr 19 |
nicklas |
65 |
|
5376 |
23 Apr 19 |
nicklas |
If enabled, activities are inserted by both ActivityEntry.onAfterCommit() |
5376 |
23 Apr 19 |
nicklas |
and ActivityEntry.onRollback(). |
5376 |
23 Apr 19 |
nicklas |
68 |
|
5376 |
23 Apr 19 |
nicklas |
Enable debug mode by adding the following to reggie-config.xml: |
5376 |
23 Apr 19 |
nicklas |
<activity-log> |
5376 |
23 Apr 19 |
nicklas |
<debug>1</debug> |
5376 |
23 Apr 19 |
nicklas |
</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 |
Get the singleton instance of the service. If the service has |
5376 |
23 Apr 19 |
nicklas |
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 |
Name of log file where activity entries are persisted. |
5385 |
26 Apr 19 |
nicklas |
NOTE!! If changes are made to any of the classes holding |
5385 |
26 Apr 19 |
nicklas |
activity information that affects the serializability |
5385 |
26 Apr 19 |
nicklas |
we need to change the filename here since otherwise the |
5385 |
26 Apr 19 |
nicklas |
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 |
Initialize the activity log by reading stored events from the |
5380 |
24 Apr 19 |
nicklas |
disk, filtering out old events, and then scheduling a timer |
5380 |
24 Apr 19 |
nicklas |
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 |
Add an activity to the log. If the activity is mergeable |
5376 |
23 Apr 19 |
nicklas |
the log will try to merge it with other activities on the |
5376 |
23 Apr 19 |
nicklas |
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 |
// 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 |
// 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 |
// 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 |
Get all activities in the log as a list. The list is sorted |
5376 |
23 Apr 19 |
nicklas |
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 |
Get the activity log as an RSS feed. The feed is returned |
5404 |
08 May 19 |
nicklas |
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 |
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 |
// 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 |
// 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 |
// 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 |
Read activities from the disc. Called once when the |
5376 |
23 Apr 19 |
nicklas |
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 |
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 |
Schedule a timer for storing the current list of activities. The activities |
5376 |
23 Apr 19 |
nicklas |
are typically stored within a few seconds. Calling this method again before |
5376 |
23 Apr 19 |
nicklas |
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 |
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 |
// 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 |
// 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 |
// <rss version="2.0"> |
5404 |
08 May 19 |
nicklas |
359 |
Element rss = new Element("rss").setAttribute("version", "2.0"); |
5404 |
08 May 19 |
nicklas |
// <channel> |
5404 |
08 May 19 |
nicklas |
361 |
Element channel = new Element("channel"); |
5404 |
08 May 19 |
nicklas |
// ..<title>.</title> |
5404 |
08 May 19 |
nicklas |
363 |
channel.addContent(new Element("title").setText(channelTitle)); |
5404 |
08 May 19 |
nicklas |
// ..<link>.</link> |
5404 |
08 May 19 |
nicklas |
365 |
channel.addContent(new Element("link").setText(serverRoot+contextRoot)); |
5404 |
08 May 19 |
nicklas |
// ..<description>.</description> |
5404 |
08 May 19 |
nicklas |
// channel.addContent(new Element("description").setText(about)); |
5404 |
08 May 19 |
nicklas |
// ..<generator>.</generator> |
5404 |
08 May 19 |
nicklas |
369 |
channel.addContent(new Element("generator").setText("Reggie "+Reggie.VERSION)); |
5404 |
08 May 19 |
nicklas |
// ..<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 |
// ..<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 |
// ..<image> |
5404 |
08 May 19 |
nicklas |
378 |
Element image = new Element("image"); |
5404 |
08 May 19 |
nicklas |
// ....<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 |
// ....<title>.</title> |
5404 |
08 May 19 |
nicklas |
383 |
image.addContent(new Element("title").setText(channelTitle)); |
5404 |
08 May 19 |
nicklas |
// ....<link>.</link> |
5404 |
08 May 19 |
nicklas |
385 |
image.addContent(new Element("link").setText(serverRoot+contextRoot)); |
5404 |
08 May 19 |
nicklas |
// ..</image> |
5404 |
08 May 19 |
nicklas |
387 |
channel.addContent(image); |
5404 |
08 May 19 |
nicklas |
388 |
|
5404 |
08 May 19 |
nicklas |
// </channel> |
5404 |
08 May 19 |
nicklas |
390 |
rss.addContent(channel); |
5404 |
08 May 19 |
nicklas |
391 |
|
5404 |
08 May 19 |
nicklas |
// 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 |
// <item> |
5404 |
08 May 19 |
nicklas |
410 |
Element item = new Element("item"); |
5404 |
08 May 19 |
nicklas |
// ..<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 |
// ..<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 |
// ..<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 |
// ..<pubDate>.</pubDate> |
5404 |
08 May 19 |
nicklas |
421 |
item.addContent(new Element("pubDate").setText(rfc822.format(a.getEventDate()))); |
5404 |
08 May 19 |
nicklas |
// </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 |
Compartor for sorting activity events latest first. Events with exactly the |
5376 |
23 Apr 19 |
nicklas |
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 |
Filter the activity log. |
5380 |
24 Apr 19 |
nicklas |
We keep all events from today and yesterday |
5617 |
20 Sep 19 |
nicklas |
Events older than 'maxAgeInDays' weeks are removed |
5617 |
20 Sep 19 |
nicklas |
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 |
// 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 |
Task that is responsible for saving the activity log |
5376 |
23 Apr 19 |
nicklas |
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 |
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 |
} |