2 * Copyright (C) 2001, 2002 The Mir-coders group
4 * This file is part of Mir.
6 * Mir is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * Mir is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Mir; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 * In addition, as a special exception, The Mir-coders gives permission to link
21 * the code of this program with any library licensed under the Apache Software License,
22 * The Sun (tm) Java Advanced Imaging library (JAI), The Sun JIMI library
23 * (or with modified versions of the above that use the same license as the above),
24 * and distribute linked combinations including the two. You must obey the
25 * GNU General Public License in all respects for all of the code used other than
26 * the above mentioned libraries. If you modify this file, you may extend this
27 * exception to your version of the file, but you are not obligated to do so.
28 * If you do not wish to do so, delete this exception statement from your version.
31 package mircoders.global;
33 import java.io.BufferedOutputStream;
35 import java.io.FileNotFoundException;
36 import java.io.FileOutputStream;
37 import java.util.Arrays;
38 import java.util.Date;
39 import java.util.GregorianCalendar;
40 import java.util.HashMap;
41 import java.util.Iterator;
42 import java.util.List;
44 import java.util.Random;
45 import java.util.Vector;
47 import javax.servlet.http.Cookie;
48 import javax.servlet.http.HttpServletResponse;
50 import mir.config.MirPropertiesConfiguration;
51 import mir.entity.Entity;
52 import mir.log.LoggerWrapper;
53 import mir.session.Request;
54 import mir.util.DateTimeFunctions;
55 import mir.util.GeneratorFormatAdapters;
56 import mir.util.StringRoutines;
57 import mir.util.EntityUtility;
58 import mircoders.entity.EntityComment;
59 import mircoders.entity.EntityContent;
60 import mircoders.localizer.MirAdminInterfaceLocalizer;
61 import mircoders.localizer.MirAntiAbuseFilterType;
63 import org.apache.commons.collections.ExtendedProperties;
67 private List filterRules;
68 private Map filterTypes;
69 private List filterTypeIds;
70 private int maxIdentifier;
71 private LoggerWrapper logger;
73 private boolean logEnabled;
74 private boolean openPostingDisabled;
75 private boolean openPostingPassword;
76 private boolean cookieOnBlock;
77 private String articleBlockAction;
78 private String commentBlockAction;
80 private File configFile = MirGlobal.config().getFile("Abuse.Config");
82 private MirPropertiesConfiguration configuration;
84 private static String cookieName = MirGlobal.config().getString("Abuse.CookieName");
85 private static int cookieMaxAge = 60 * 60 * MirGlobal.config().getInt("Abuse.CookieMaxAge");
88 logger = new LoggerWrapper("Global.Abuse");
89 filterRules = new Vector();
94 configuration = MirPropertiesConfiguration.instance();
97 throw new RuntimeException("Can't get configuration: " + e.getMessage());
102 articleBlockAction = "";
103 commentBlockAction = "";
104 openPostingPassword = false;
105 openPostingDisabled = false;
106 cookieOnBlock = false;
109 filterTypes = new HashMap();
110 filterTypeIds = new Vector();
112 Iterator i = MirGlobal.localizer().openPostings().getAntiAbuseFilterTypes().iterator();
114 while (i.hasNext()) {
115 MirAntiAbuseFilterType filterType = (MirAntiAbuseFilterType) i.next();
116 filterTypes.put(filterType.getName(), filterType);
117 filterTypeIds.add(filterType.getName());
120 catch (Throwable t) {
121 throw new RuntimeException("Can't get filter types: " + t.getMessage());
127 private void setCookie(HttpServletResponse aResponse) {
128 Random random = new Random();
130 Cookie cookie = new Cookie(cookieName, Integer.toString(random.nextInt(1000000000)));
131 cookie.setMaxAge(cookieMaxAge);
134 if (aResponse != null)
135 aResponse.addCookie(cookie);
138 private boolean checkCookie(List aCookies) {
139 if (getCookieOnBlock()) {
140 Iterator i = aCookies.iterator();
142 while (i.hasNext()) {
143 Cookie cookie = (Cookie) i.next();
145 if (cookie.getName().equals(cookieName)) {
146 logger.debug("cookie match");
155 FilterRule findMatchingFilter(Entity anEntity, Request aRequest) {
156 Iterator iterator = filterRules.iterator();
158 while (iterator.hasNext()) {
159 FilterRule rule = (FilterRule) iterator.next();
161 if (rule.test(anEntity, aRequest))
168 public void checkComment(EntityComment aComment, Request aRequest, HttpServletResponse aResponse) {
170 long time = System.currentTimeMillis();
172 FilterRule filterRule = findMatchingFilter(aComment, aRequest);
174 if (filterRule != null) {
175 logger.debug("Match for " + filterRule.getType() + " rule '" + filterRule.getExpression() + "'");
176 filterRule.setLastHit(new GregorianCalendar().getTime());
178 StringBuffer line = new StringBuffer();
180 line.append(DateTimeFunctions.advancedDateFormat(
181 configuration.getString("Mir.DefaultDateTimeFormat"),
182 (new GregorianCalendar()).getTime(), configuration.getString("Mir.DefaultTimezone")));
185 line.append("filter");
188 line.append(filterRule.getType() +" ("+ filterRule.getExpression()+")");
189 EntityUtility.appendLineToField(aComment, "comment", line.toString());
191 MirGlobal.performCommentOperation(null, aComment, filterRule.getCommentAction());
192 setCookie(aResponse);
194 logComment(aComment, aRequest, filterRule.getType(), filterRule.getExpression());
197 logComment(aComment, aRequest);
200 logger.info("checkComment: " + (System.currentTimeMillis() - time) + "ms");
202 catch (Throwable t) {
203 t.printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
204 logger.error("Abuse.checkComment: " + t.toString());
208 public void checkArticle(EntityContent anArticle, Request aRequest, HttpServletResponse aResponse) {
210 long time = System.currentTimeMillis();
212 FilterRule filterRule = findMatchingFilter(anArticle, aRequest);
214 if (filterRule != null) {
215 logger.debug("Match for " + filterRule.getType() + " rule '" + filterRule.getExpression() + "'");
216 filterRule.setLastHit(new GregorianCalendar().getTime());
218 StringBuffer line = new StringBuffer();
220 line.append(DateTimeFunctions.advancedDateFormat(
221 configuration.getString("Mir.DefaultDateTimeFormat"),
222 (new GregorianCalendar()).getTime(), configuration.getString("Mir.DefaultTimezone")));
225 line.append("filter");
228 line.append(filterRule.getType() +" ("+ filterRule.getExpression()+")");
229 EntityUtility.appendLineToField(anArticle, "comment", line.toString());
231 MirGlobal.performArticleOperation(null, anArticle, filterRule.getArticleAction());
232 setCookie(aResponse);
234 logArticle(anArticle, aRequest, filterRule.getType(), filterRule.getExpression());
237 logArticle(anArticle, aRequest);
239 logger.info("checkArticle: " + (System.currentTimeMillis() - time) + "ms");
241 catch (Throwable t) {
242 t.printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
243 logger.error("Abuse.checkArticle: " + t.toString());
247 public boolean getLogEnabled() {
251 public void setLogEnabled(boolean anEnabled) {
252 if (!configuration.getString("Abuse.DisallowIPLogging", "0").equals("1"))
253 logEnabled = anEnabled;
257 public int getLogSize() {
261 public void setLogSize(int aSize) {
266 public boolean getOpenPostingDisabled() {
267 return openPostingDisabled;
270 public void setOpenPostingDisabled(boolean anOpenPostingDisabled) {
271 openPostingDisabled = anOpenPostingDisabled;
274 public boolean getOpenPostingPassword() {
275 return openPostingPassword;
278 public void setOpenPostingPassword(boolean anOpenPostingPassword) {
279 openPostingPassword = anOpenPostingPassword;
282 public boolean getCookieOnBlock() {
283 return cookieOnBlock;
286 public void setCookieOnBlock(boolean aCookieOnBlock) {
287 cookieOnBlock = aCookieOnBlock;
290 public String getArticleBlockAction() {
291 return articleBlockAction;
294 public void setArticleBlockAction(String anAction) {
295 articleBlockAction = anAction;
298 public String getCommentBlockAction() {
299 return commentBlockAction;
302 public void setCommentBlockAction(String anAction) {
303 commentBlockAction = anAction;
306 public List getLog() {
309 List result = new Vector();
311 Iterator i = log.iterator();
312 while (i.hasNext()) {
313 LogEntry logEntry = (LogEntry) i.next();
314 Map entry = new HashMap();
316 entry.put("ip", logEntry.getIpNumber());
317 entry.put("id", logEntry.getId());
318 entry.put("timestamp", new GeneratorFormatAdapters.DateFormatAdapter(logEntry.getTimeStamp(), MirPropertiesConfiguration.instance().getString("Mir.DefaultTimezone")));
319 if (logEntry.getIsArticle())
320 entry.put("type", "content");
322 entry.put("type", "comment");
323 entry.put("browser", logEntry.getBrowserString());
324 entry.put("hitfiltertype", logEntry.getHitFilterType());
325 entry.put("hitfilterexpression", logEntry.getHitFilterExpression());
332 catch (Throwable t) {
333 throw new RuntimeException(t.toString());
338 public void logComment(Entity aComment, Request aRequest) {
339 logComment(aComment, aRequest, null, null);
342 public void logComment(Entity aComment, Request aRequest, String aHitFilterType, String aHitFilterExpression) {
343 String ipAddress = aRequest.getHeader("ip");
344 String id = aComment.getId();
345 String browser = aRequest.getHeader("User-Agent");
347 logComment(ipAddress, id, new Date(), browser, aHitFilterType, aHitFilterExpression);
350 public void logArticle(Entity anArticle, Request aRequest) {
351 logArticle(anArticle, aRequest, null, null);
354 public void logArticle(Entity anArticle, Request aRequest, String aHitFilterType, String aHitFilterExpression) {
355 String ipAddress = aRequest.getHeader("ip");
356 String id = anArticle.getId();
357 String browser = aRequest.getHeader("User-Agent");
359 logArticle(ipAddress, id, new Date(), browser, aHitFilterType, aHitFilterExpression);
362 public void logComment(String anIp, String anId, Date aTimeStamp, String aBrowser, String aHitFilterType, String aHitFilterExpression) {
363 appendLog(new LogEntry(aTimeStamp, anIp, aBrowser, anId, false, aHitFilterType, aHitFilterExpression));
366 public void logArticle(String anIp, String anId, Date aTimeStamp, String aBrowser, String aHitFilterType, String aHitFilterExpression) {
367 appendLog(new LogEntry(aTimeStamp, anIp, aBrowser, anId, true, aHitFilterType, aHitFilterExpression));
371 synchronized (filterRules) {
373 ExtendedProperties configuration = new ExtendedProperties();
376 configuration = new ExtendedProperties(configFile.getAbsolutePath());
378 catch (FileNotFoundException e) {
381 getFilterConfig(filterRules, "abuse.filter", configuration);
383 setOpenPostingDisabled(configuration.getString("abuse.openPostingDisabled", "0").equals("1"));
384 setOpenPostingPassword(configuration.getString("abuse.openPostingPassword", "0").equals("1"));
385 setCookieOnBlock(configuration.getString("abuse.cookieOnBlock", "0").equals("1"));
386 setLogEnabled(configuration.getString("abuse.logEnabled", "0").equals("1"));
387 setLogSize(configuration.getInt("abuse.logSize", 10));
388 setArticleBlockAction(configuration.getString("abuse.articleBlockAction", ""));
389 setCommentBlockAction(configuration.getString("abuse.commentBlockAction", ""));
391 catch (Throwable t) {
392 throw new RuntimeException(t.toString());
398 synchronized (filterRules) {
400 ExtendedProperties configuration = new ExtendedProperties();
402 setFilterConfig(filterRules, "abuse.filter", configuration);
404 configuration.addProperty("abuse.openPostingDisabled", getOpenPostingDisabled() ? "1" : "0");
405 configuration.addProperty("abuse.openPostingPassword", getOpenPostingPassword() ? "1" : "0");
406 configuration.addProperty("abuse.cookieOnBlock", getCookieOnBlock() ? "1" : "0");
407 configuration.addProperty("abuse.logEnabled", getLogEnabled() ? "1" : "0");
408 configuration.addProperty("abuse.logSize", Integer.toString(getLogSize()));
409 configuration.addProperty("abuse.articleBlockAction", getArticleBlockAction());
410 configuration.addProperty("abuse.commentBlockAction", getCommentBlockAction());
412 configuration.save(new BufferedOutputStream(new FileOutputStream(configFile),8192), "Anti abuse configuration");
414 catch (Throwable t) {
415 throw new RuntimeException(t.toString());
420 public List getFilterTypes() {
422 List result = new Vector();
424 Iterator i = filterTypeIds.iterator();
425 while (i.hasNext()) {
426 String id = (String) i.next();
428 Map action = new HashMap();
429 action.put("resource", id);
430 action.put("identifier", id);
437 catch (Throwable t) {
438 throw new RuntimeException("can't get article actions");
442 public List getArticleActions() {
444 List result = new Vector();
446 Iterator i = MirGlobal.localizer().adminInterface().simpleArticleOperations().iterator();
447 while (i.hasNext()) {
448 MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation =
449 (MirAdminInterfaceLocalizer.MirSimpleEntityOperation) i.next();
451 Map action = new HashMap();
452 action.put("resource", operation.getName());
453 action.put("identifier", operation.getName());
460 catch (Throwable t) {
461 throw new RuntimeException("can't get article actions");
465 public List getCommentActions() {
467 List result = new Vector();
469 Iterator i = MirGlobal.localizer().adminInterface().simpleCommentOperations().iterator();
470 while (i.hasNext()) {
471 MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation =
472 (MirAdminInterfaceLocalizer.MirSimpleEntityOperation) i.next();
474 Map action = new HashMap();
475 action.put("resource", operation.getName());
476 action.put("identifier", operation.getName());
483 catch (Throwable t) {
484 throw new RuntimeException("can't get comment actions");
488 public List getFilters() {
489 List result = new Vector();
491 synchronized (filterRules) {
492 Iterator i = filterRules.iterator();
493 while (i.hasNext()) {
494 FilterRule filter = (FilterRule) i.next();
495 result.add(filter.clone());
501 public String addFilter(String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction) {
502 return addFilter(aType, anExpression, aComments, aCommentAction, anArticleAction, null);
505 public String addFilter(String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction, Date aListHit) {
506 return addFilter(filterRules, aType, anExpression, aComments, aCommentAction, anArticleAction, aListHit);
509 public FilterRule getFilter(String anId) {
510 synchronized (filterRules) {
511 FilterRule result = findFilter(filterRules, anId);
515 return (FilterRule) result.clone();
519 public String setFilter(String anIdentifier, String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction) {
520 return setFilter(filterRules, anIdentifier, aType, anExpression, aComments, aCommentAction, anArticleAction);
523 public void deleteFilter(String anIdentifier) {
524 deleteFilter(filterRules, anIdentifier);
527 public void moveFilterUp(String anIdentifier) {
528 moveFilter(filterRules, anIdentifier, -1);
531 public void moveFilterDown(String anIdentifier) {
532 moveFilter(filterRules, anIdentifier, 1);
535 public void moveFilterToTop(String anIdentifier) {
536 setFilterPosition(filterRules, anIdentifier, 0);
539 public void moveFilterToBottom(String anIdentifier) {
540 setFilterPosition(filterRules, anIdentifier, Integer.MAX_VALUE);
543 private String addFilter(List aFilters, String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction, Date aLastHit) {
544 MirAntiAbuseFilterType type = (MirAntiAbuseFilterType) filterTypes.get(aType);
547 return "invalidtype";
549 if (!type.validate(anExpression)) {
550 return "invalidexpression";
553 FilterRule filter = new FilterRule();
555 filter.setId(generateId());
556 filter.setExpression(anExpression);
557 filter.setType(aType);
558 filter.setComments(aComments);
559 filter.setArticleAction(anArticleAction);
560 filter.setCommentAction(aCommentAction);
561 filter.setLastHit(aLastHit);
563 synchronized (aFilters) {
564 aFilters.add(filter);
570 private String setFilter(List aFilters, String anIdentifier, String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction) {
571 MirAntiAbuseFilterType type = (MirAntiAbuseFilterType) filterTypes.get(aType);
574 return "invalidtype";
576 if (!type.validate(anExpression)) {
577 return "invalidexpression";
580 synchronized (aFilters) {
581 FilterRule filter = findFilter(aFilters, anIdentifier);
583 if (filter != null) {
584 filter.setExpression(anExpression);
585 filter.setType(aType);
586 filter.setCommentAction(aCommentAction);
587 filter.setArticleAction(anArticleAction);
588 filter.setComments(aComments);
595 private FilterRule findFilter(List aFilters, String anIdentifier) {
596 synchronized (aFilters) {
597 Iterator i = aFilters.iterator();
598 while (i.hasNext()) {
599 FilterRule filter = (FilterRule) i.next();
601 if (filter.getId().equals(anIdentifier)) {
610 private void setFilterPosition(List aFilters, String anIdentifier, int aPosition) {
611 synchronized (aFilters) {
615 for (int i = 0; i < aFilters.size(); i++) {
616 FilterRule rule = (FilterRule) aFilters.get(i);
618 if (rule.getId().equals(anIdentifier)) {
619 aFilters.remove(rule);
621 if (aPosition<aFilters.size())
622 aFilters.add(aPosition, rule);
631 private void moveFilter(List aFilters, String anIdentifier, int aDirection) {
632 synchronized (aFilters) {
633 for (int i = 0; i < aFilters.size(); i++) {
634 FilterRule rule = (FilterRule) aFilters.get(i);
636 if (rule.getId().equals(anIdentifier) && (i + aDirection >= 0) && (i + aDirection < aFilters.size())) {
637 aFilters.remove(rule);
638 aFilters.add(i + aDirection, rule);
645 private void deleteFilter(List aFilters, String anIdentifier) {
646 synchronized (aFilters) {
647 FilterRule filter = findFilter(aFilters, anIdentifier);
649 if (filter != null) {
650 aFilters.remove(filter);
655 private String generateId() {
656 synchronized (this) {
657 maxIdentifier = maxIdentifier + 1;
659 return Integer.toString(maxIdentifier);
663 public class FilterRule {
664 private String identifier;
665 private String expression;
667 private String comments;
668 private String articleAction;
669 private String commentAction;
670 private Date lastHit;
672 public FilterRule() {
677 articleAction = articleBlockAction;
678 commentAction = commentBlockAction;
682 public Date getLastHit() {
686 public void setLastHit(Date aDate) {
690 public String getId() {
694 public void setId(String anId) {
698 public String getExpression() {
702 public void setExpression(String anExpression) {
703 expression = anExpression;
706 public String getType() {
710 public void setType(String aType) {
714 public void setComments(String aComments) {
715 comments = aComments;
718 public String getComments() {
722 public String getArticleAction() {
723 return articleAction;
726 public void setArticleAction(String anArticleAction) {
727 articleAction = anArticleAction;
730 public String getCommentAction() {
731 return commentAction;
734 public void setCommentAction(String aCommentAction) {
735 commentAction = aCommentAction;
738 public boolean test(Entity anEntity, Request aRequest) {
739 MirAntiAbuseFilterType filterType = (MirAntiAbuseFilterType) filterTypes.get(type);
741 if (filterType != null)
742 return filterType.test(expression, anEntity, aRequest);
744 catch (Throwable t) {
745 logger.error("error while testing " + type + "-filter '" + expression + "'");
751 public Object clone() {
752 FilterRule result = new FilterRule();
753 result.setComments(getComments());
754 result.setExpression(getExpression());
755 result.setId(getId());
756 result.setType(getType());
757 result.setArticleAction(getArticleAction());
758 result.setCommentAction(getCommentAction());
759 result.setLastHit(getLastHit());
765 private String escapeConfigListEntry(String aFilterPart) {
766 return StringRoutines.replaceStringCharacters(aFilterPart,
767 new char[] {'\\', ':'},
768 new String[] {"\\\\", "\\:"});
771 private String escapeFilterPart(String aFilterPart) {
772 return StringRoutines.replaceStringCharacters(aFilterPart,
773 new char[] {'\\', '\n', '\r', '\t', ' '},
774 new String[] {"\\\\", "\\n", "\\r", "\\t", "\\ "});
777 private String deescapeFilterPart(String aFilterPart) {
778 return StringRoutines.replaceEscapedStringCharacters(aFilterPart,
780 new char[] {'\\', ':', 'n', 'r', 't', ' '},
781 new String[] {"\\", ":", "\n", "\r", "\t", " "});
784 private void setFilterConfig(List aFilters, String aConfigKey, ExtendedProperties aConfiguration) {
785 synchronized (aFilters) {
786 Iterator i = aFilters.iterator();
788 while (i.hasNext()) {
789 FilterRule filter = (FilterRule) i.next();
791 String filterconfig =
792 escapeConfigListEntry(escapeFilterPart(filter.getType())) + ":" +
793 escapeConfigListEntry(escapeFilterPart(filter.getExpression())) + ":" +
794 escapeConfigListEntry(escapeFilterPart(filter.getArticleAction())) + ":" +
795 escapeConfigListEntry(escapeFilterPart(filter.getCommentAction())) + ":" +
796 escapeConfigListEntry(escapeFilterPart(filter.getComments())) + ":";
798 if (filter.getLastHit() != null)
799 filterconfig = filterconfig + filter.getLastHit().getTime();
801 aConfiguration.addProperty(aConfigKey, filterconfig);
806 private void getFilterConfig(List aFilters, String aConfigKey, ExtendedProperties aConfiguration) {
807 synchronized (aFilters) {
810 if (aConfiguration.getStringArray(aConfigKey) != null) {
812 Iterator i = Arrays.asList(aConfiguration.getStringArray(aConfigKey)).
815 while (i.hasNext()) {
816 String filter = (String) i.next();
817 List parts = StringRoutines.splitStringWithEscape(filter, ':', '\\');
818 if (parts.size() == 2) {
819 parts.add(articleBlockAction);
820 parts.add(commentBlockAction);
825 if (parts.size() >= 5) {
828 if (parts.size() >= 6) {
829 String lastHitString = (String) parts.get(5);
832 lastHit = new Date(Long.parseLong(lastHitString));
834 catch (Throwable t) {
838 addFilter(deescapeFilterPart( (String) parts.get(0)),
839 deescapeFilterPart( (String) parts.get(1)),
840 deescapeFilterPart( (String) parts.get(4)),
841 deescapeFilterPart( (String) parts.get(3)),
842 deescapeFilterPart( (String) parts.get(2)), lastHit);
849 private static class LogEntry {
850 private String ipNumber;
851 private String browserString;
853 private Date timeStamp;
854 private boolean isArticle;
855 private String hitFilterType;
856 private String hitFilterExpression;
858 public LogEntry(Date aTimeStamp, String anIpNumber, String aBrowserString, String anId, boolean anIsArticle, String aHitFilterType, String aHitFilterExpression) {
859 ipNumber = anIpNumber;
860 browserString = aBrowserString;
862 isArticle = anIsArticle;
863 timeStamp = aTimeStamp;
864 hitFilterType = aHitFilterType;
865 hitFilterExpression = aHitFilterExpression;
868 public LogEntry(Date aTimeStamp, String anIpNumber, String aBrowserString, String anId, boolean anIsArticle) {
869 this(aTimeStamp, anIpNumber, aBrowserString, anId, anIsArticle, null, null);
872 public String getIpNumber() {
876 public String getBrowserString() {
877 return browserString;
880 public String getId() {
884 public String getHitFilterType() {
885 return hitFilterType;
888 public String getHitFilterExpression() {
889 return hitFilterExpression;
892 public Date getTimeStamp() {
896 public boolean getIsArticle() {
901 private void truncateLog() {
906 while (log.size() > 0 && log.size() > logSize) {
907 log.remove(log.size()-1);
913 private void appendLog(LogEntry anEntry) {