/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.node.useralerts; import java.text.Format; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import freenet.clients.fcp.FCPConnectionHandler; import freenet.l10n.NodeL10n; import freenet.node.NodeClientCore; import freenet.support.Base64; import freenet.support.HTMLNode; import freenet.support.Logger; /** * Collection of UserAlert's. */ public class UserAlertManager implements Comparator<UserAlert> { // No point keeping them sorted as some alerts can change priority. private final Set<UserAlert> alerts; private final NodeClientCore core; private final Set<FCPConnectionHandler> subscribers; private final Map<UserEvent.Type, UserEvent> events; private final Set<UserEvent.Type> unregisteredEventTypes; private long lastUpdated; public UserAlertManager(NodeClientCore core) { this.core = core; alerts = new HashSet<UserAlert>(); subscribers = new CopyOnWriteArraySet<FCPConnectionHandler>(); events = new HashMap<UserEvent.Type, UserEvent>(); unregisteredEventTypes = new HashSet<UserEvent.Type>(); lastUpdated = System.currentTimeMillis(); } public void register(UserAlert alert) { if(alert instanceof UserEvent) register((UserEvent) alert); synchronized (alerts) { if (!alerts.contains(alert)) { alerts.add(alert); lastUpdated = System.currentTimeMillis(); notifySubscribers(alert); } } } public void register(UserEvent event) { // The event is ignored if it has been indefinitely unregistered synchronized(unregisteredEventTypes) { if(unregisteredEventTypes.contains(event.getEventType())) return; } // Only the latest event is displayed as an alert synchronized (events) { UserEvent lastEvent = events.get(event.getEventType()); synchronized (alerts) { if (lastEvent != null) alerts.remove(lastEvent); alerts.add(event); } events.put(event.getEventType(), event); lastUpdated = System.currentTimeMillis(); notifySubscribers(event); } } private void notifySubscribers(final UserAlert alert) { // Run off-thread, because of locking, and because client // callbacks may take some time core.clientContext.mainExecutor.execute(new Runnable() { @Override public void run() { for (FCPConnectionHandler subscriber : subscribers) subscriber.outputHandler.queue(alert.getFCPMessage()); } }, "UserAlertManager callback executor"); } public void unregister(UserAlert alert) { if(alert == null) return; if(alert instanceof UserEvent) unregister(((UserEvent)alert).getEventType()); synchronized (alerts) { alerts.remove(alert); } } public void unregister(UserEvent.Type eventType) { if(eventType.unregisterIndefinitely()) synchronized (unregisteredEventTypes) { unregisteredEventTypes.add(eventType); } synchronized (events) { UserEvent latestEvent; latestEvent = events.remove(eventType); if(latestEvent != null) synchronized(alerts) { alerts.remove(latestEvent); } } } /** * Tries to find the user alert with the given hash code and dismisses it, * if found. * * @see #unregister(UserAlert) * @param alertHashCode * The hash code of the user alert to dismiss */ public void dismissAlert(int alertHashCode) { UserAlert[] userAlerts = getAlerts(); for (UserAlert userAlert: userAlerts) { if (userAlert.hashCode() == alertHashCode) { if (userAlert.userCanDismiss()) { if (userAlert.shouldUnregisterOnDismiss()) { userAlert.onDismiss(); unregister(userAlert); } else { userAlert.isValid(false); } } } } } public UserAlert[] getAlerts() { UserAlert[] a; synchronized (alerts) { a = alerts.toArray(new UserAlert[alerts.size()]); } Arrays.sort(a, this); return a; } @Override public int compare(UserAlert a0, UserAlert a1) { if(a0 == a1) return 0; // common case, also we should be consistent with == even with proxyuseralert's short prio0 = a0.getPriorityClass(); short prio1 = a1.getPriorityClass(); if(prio0 - prio1 == 0) { boolean isEvent0 = a0.isEventNotification(); boolean isEvent1 = a1.isEventNotification(); if(isEvent0 && !isEvent1) return 1; if((!isEvent0) && isEvent1) return -1; // First go by class int classHash0 = a0.getClass().hashCode(); int classHash1 = a1.getClass().hashCode(); if(classHash0 > classHash1) return 1; else if(classHash0 < classHash1) return -1; // Then go by time (newest first) if(a0.getUpdatedTime() < a1.getUpdatedTime()) return 1; else if(a0.getUpdatedTime() > a1.getUpdatedTime()) return -1; // And finally by object hashCode int hash0 = a0.hashCode(); int hash1 = a1.hashCode(); if(hash0 > hash1) return 1; if(hash1 > hash0) return -1; return 0; } else { if(prio0 > prio1) return 1; else return -1; } } public HTMLNode createAlerts() { return createAlerts(true); } /** * Write the alerts as HTML. */ public HTMLNode createAlerts(boolean showOnlyErrors) { HTMLNode alertsNode = new HTMLNode("div"); int totalNumber = 0; for (UserAlert alert: getAlerts()) { if(showOnlyErrors && alert.getPriorityClass() > UserAlert.ERROR) continue; if (!alert.isValid()) continue; totalNumber++; alertsNode.addChild("a", "name", alert.anchor()); if(showOnlyErrors) { // Paranoia. Don't break the web interface no matter what. try { alertsNode.addChild(renderAlert(alert)); } catch (Throwable t) { Logger.error(this, "FAILED TO RENDER ALERT: "+alert+" : "+t, t); } } else { // Alerts toadlet itself can error, that's OK. alertsNode.addChild(renderAlert(alert)); } } if (totalNumber == 0) { return new HTMLNode("#", ""); } return alertsNode; } /** * Renders the given alert and returns the rendered HTML node. * * @param userAlert * The user alert to render * @return The rendered HTML node */ public HTMLNode renderAlert(UserAlert userAlert) { HTMLNode userAlertNode = null; short level = userAlert.getPriorityClass(); userAlertNode = new HTMLNode("div", "class", "infobox infobox-"+getAlertLevelName(level)); userAlertNode.addChild("div", "class", "infobox-header", userAlert.getTitle()); HTMLNode alertContentNode = userAlertNode.addChild("div", "class", "infobox-content"); alertContentNode.addChild(userAlert.getHTMLText()); alertContentNode.addChild(renderDismissButton(userAlert, null)); return userAlertNode; } public HTMLNode renderDismissButton(UserAlert userAlert, String redirectToAfterDisable) { HTMLNode result = new HTMLNode("div"); if (userAlert.userCanDismiss()) { HTMLNode dismissFormNode = result.addChild("form", new String[] { "action", "method" }, new String[] { "/alerts/", "post" }).addChild("div"); dismissFormNode.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "disable", String.valueOf(userAlert.hashCode()) }); dismissFormNode.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "formPassword", core.formPassword }); dismissFormNode.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "dismiss-user-alert", userAlert.dismissButtonText() }); if (redirectToAfterDisable != null) { dismissFormNode.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "redirectToAfterDisable", redirectToAfterDisable }); } } return result; } private String getAlertLevelName(short level) { if (level <= UserAlert.CRITICAL_ERROR) return "error"; else if (level <= UserAlert.ERROR) return "alert"; else if (level <= UserAlert.WARNING) return "warning"; else if (level <= UserAlert.MINOR) return "minor"; else { Logger.error(this, "Unknown alert level: "+level, new Exception("debug")); return "error"; } } public HTMLNode createSummary() { // This method is called by the toadlets when they want to show // a summary of alerts. With a status bar, we only show full errors here. return createAlerts(true); } static final HTMLNode ALERTS_LINK = new HTMLNode("a", "href", "/alerts/").setReadOnly(); /** * Write the alert summary as HTML to a StringBuilder */ public HTMLNode createSummary(boolean oneLine) { short highestLevel = 99; int numberOfCriticalError = 0; int numberOfError = 0; int numberOfWarning = 0; int numberOfMinor = 0; int totalNumber = 0; for (UserAlert alert: getAlerts()) { if (!alert.isValid()) continue; short level = alert.getPriorityClass(); if (level < highestLevel) highestLevel = level; if (level <= UserAlert.CRITICAL_ERROR) numberOfCriticalError++; else if (level <= UserAlert.ERROR) numberOfError++; else if (level <= UserAlert.WARNING) numberOfWarning++; else if (level <= UserAlert.MINOR) numberOfMinor++; totalNumber++; } if(numberOfMinor == 0 && numberOfWarning == 0 && oneLine) return null; if (totalNumber == 0) return new HTMLNode("#", ""); boolean separatorNeeded = false; String separator = oneLine?", ":" | "; int messageTypes=0; StringBuilder alertSummaryString = new StringBuilder(1024); if (numberOfCriticalError != 0 && !oneLine) { alertSummaryString.append(l10n("criticalErrorCountLabel")).append(' ').append(numberOfCriticalError); separatorNeeded = true; messageTypes++; } if (numberOfError != 0 && !oneLine) { if (separatorNeeded) alertSummaryString.append(separator); alertSummaryString.append(l10n("errorCountLabel")).append(' ').append(numberOfError); separatorNeeded = true; messageTypes++; } if (numberOfWarning != 0) { if (separatorNeeded) alertSummaryString.append(separator); if(oneLine) { alertSummaryString.append(numberOfWarning).append(' ').append(l10n("warningCountLabel").replace(":", "")); } else { alertSummaryString.append(l10n("warningCountLabel")).append(' ').append(numberOfWarning); } separatorNeeded = true; messageTypes++; } if (numberOfMinor != 0) { if (separatorNeeded) alertSummaryString.append(separator); if(oneLine) { alertSummaryString.append(numberOfMinor).append(' ').append(l10n("minorCountLabel").replace(":", "")); } else { alertSummaryString.append(l10n("minorCountLabel")).append(' ').append(numberOfMinor); } separatorNeeded = true; messageTypes++; } if (messageTypes != 1 && !oneLine) { if (separatorNeeded) alertSummaryString.append(separator); alertSummaryString.append(l10n("totalLabel")).append(' ').append(totalNumber); } HTMLNode summaryBox = null; String classes = oneLine?"alerts-line contains-":"infobox infobox-"; if (highestLevel <= UserAlert.CRITICAL_ERROR && !oneLine) summaryBox = new HTMLNode("div", "class", classes + "error"); else if (highestLevel <= UserAlert.ERROR && !oneLine) summaryBox = new HTMLNode("div", "class", classes + "alert"); else if (highestLevel <= UserAlert.WARNING) summaryBox = new HTMLNode("div", "class", classes + "warning"); else if (highestLevel <= UserAlert.MINOR) summaryBox = new HTMLNode("div", "class", classes + "information"); summaryBox.addChild("div", "class", "infobox-header", l10n("alertsTitle")); HTMLNode summaryContent = summaryBox.addChild("div", "class", "infobox-content"); if(!oneLine) { summaryContent.addChild("#", alertSummaryString.toString() + separator + " "); NodeL10n.getBase().addL10nSubstitution(summaryContent, "UserAlertManager.alertsOnAlertsPage", new String[] { "link" }, new HTMLNode[] { ALERTS_LINK }); } else { summaryContent.addChild("a", "href", "/alerts/", NodeL10n.getBase().getString("StatusBar.alerts") + " " + alertSummaryString.toString()); } summaryBox.addAttribute("id", "messages-summary-box"); return summaryBox; } private String l10n(String key) { return NodeL10n.getBase().getString("UserAlertManager."+key); } public void dumpEvents(HashSet<String> toDump) { // An iterator might be faster, but we don't want to call methods on the alert within the lock. for(UserAlert alert: getAlerts()) { if(!alert.isEventNotification()) continue; if(!toDump.contains(alert.anchor())) continue; unregister(alert); alert.onDismiss(); } } public void watch(final FCPConnectionHandler subscriber) { subscribers.add(subscriber); // Run off-thread, because of locking, and because client // callbacks may take some time core.clientContext.mainExecutor.execute(new Runnable() { @Override public void run() { for (UserAlert alert : getAlerts()) if(alert.isValid()) subscriber.outputHandler.queue(alert.getFCPMessage()); } }, "UserAlertManager callback executor"); subscribers.add(subscriber); } public void unwatch(FCPConnectionHandler subscriber) { subscribers.remove(subscriber); } //Formats a Unix timestamp according to RFC 3339 private String formatTime(long time) { final Format format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); String date = format.format(new Date(time)); //Z doesn't include a colon between the hour and the minutes return date.substring(0, 22) + ":" + date.substring(22); } public String getAtom(String startURI) { String messagesURI = startURI + "/alerts/"; String feedURI = startURI + "/feed/"; StringBuilder sb = new StringBuilder(); sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); sb.append("<feed xmlns=\"http://www.w3.org/2005/Atom\">\n"); sb.append("\n"); sb.append(" <title>").append(l10n("feedTitle")).append("</title>\n"); sb.append(" <link href=\"").append(feedURI).append("\" rel=\"self\"/>\n"); sb.append(" <link href=\"").append(startURI).append("\"/>\n"); sb.append(" <updated>").append(formatTime(lastUpdated)).append("</updated>\n"); sb.append(" <id>urn:node:").append(Base64.encode(core.node.getDarknetPubKeyHash())).append("</id>\n"); sb.append(" <logo>").append("/favicon.ico").append("</logo>\n"); UserAlert[] alerts = getAlerts(); for(int i = alerts.length - 1; i >= 0; i--) { UserAlert alert = alerts[i]; if (alert.isValid()) { sb.append("\n"); sb.append(" <entry>\n"); sb.append(" <title>").append(alert.getTitle()).append("</title>\n"); sb.append(" <link href=\"").append(messagesURI).append("#").append(alert.anchor()).append("\"/>\n"); sb.append(" <summary>").append(alert.getShortText()).append("</summary>\n"); sb.append(" <content type=\"text\">").append(alert.getText()).append("</content>\n"); sb.append(" <id>urn:feed:").append(alert.anchor()).append("</id>\n"); sb.append(" <updated>").append(formatTime(alert.getUpdatedTime())).append("</updated>\n"); sb.append(" </entry>\n"); } } sb.append("\n</feed>\n"); return sb.toString(); } }