/*==========================================================================*\ | $Id: Message.java,v 1.8 2011/12/25 02:24:54 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2010-2011 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.core.messaging; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jfree.util.Log; import org.webcat.core.Application; import org.webcat.core.MutableDictionary; import org.webcat.core.SentMessage; import org.webcat.core.Subsystem; import org.webcat.core.User; import org.webcat.woextensions.ECAction; import static org.webcat.woextensions.ECAction.run; import org.webcat.woextensions.WCEC; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSTimestamp; //------------------------------------------------------------------------- /** * <p> * The abstract base class for all notification messages in Web-CAT. Subclasses * should localize their registration by providing a static register() method * that calls the {@link #registerMessage(Class, String, boolean, int)} with * the appropriate parameters, and then the subsystem can all these register() * methods in its {@link Subsystem#init()}. * </p><p> * Some methods below are denoted as "only called during the message sending * cycle". This means that during the execution of these methods, the editing * context returned by the {@link #editingContext()} method is valid. * </p> * * @author Tony Allevato * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.8 $, $Date: 2011/12/25 02:24:54 $ */ public abstract class Message { //~ Methods ............................................................... // ---------------------------------------------------------- /** * Registers the specified message type in the messaging system. * * @param messageClass the class of message being registered * @param category a human-readable name of a category for this message, * used for grouping * @param description a human-readable name of the message type, suitable * for display in the Web-CAT user interface * @param isBroadcast true if messages of this type should be broadcast * to any system-wide notification protocols (such as Twitter feeds), * in addition to being sent to the message's list of users * @param accessLevel the lowest access level of users who can receive * this message type (used to control whether the user can view it in * the message/protocol enablement matrix) */ public static void registerMessage( Class<? extends Message> messageClass, String category, String description, boolean isBroadcast, int accessLevel) { MessageDescriptor descriptor = new MessageDescriptor( messageClass.getCanonicalName(), category, description, isBroadcast, accessLevel); synchronized (descriptors) { descriptors.put(messageClass, descriptor); } } // ---------------------------------------------------------- /** * Gets the descriptors for the currently registered direct-to-user * messages. This method only returns those messages that can be sent to * the specified user, based on access level. * * @param user the user * @return an array of message descriptors */ public static NSArray<MessageDescriptor> registeredUserMessages(User user) { NSMutableArray<MessageDescriptor> descs = new NSMutableArray<MessageDescriptor>(); synchronized (descriptors) { for (MessageDescriptor descriptor : descriptors.values()) { if (user.accessLevel() >= descriptor.accessLevel()) { descs.addObject(descriptor); } } } return descs; } // ---------------------------------------------------------- /** * Gets the descriptors for the currently registered broadcast messages. * * @return an array of message descriptors */ public static NSArray<MessageDescriptor> registeredBroadcastMessages() { NSMutableArray<MessageDescriptor> descs = new NSMutableArray<MessageDescriptor>(); synchronized (descriptors) { for (MessageDescriptor descriptor : descriptors.values()) { if (descriptor.isBroadcast()) { descs.addObject(descriptor); } } } return descs; } // ---------------------------------------------------------- /** * A shorthand method for getting the message type name, which is the * canonical name of the message class. * * @return the canonical class name of the message */ public final String messageType() { return getClass().getCanonicalName(); } // ---------------------------------------------------------- /** * Gets the message descriptor for messages of this type. * * @return the message descriptor */ public final MessageDescriptor messageDescriptor() { MessageDescriptor result = null; synchronized (descriptors) { result = descriptors.get(getClass()); } return result; } // ---------------------------------------------------------- /** * Gets the message descriptor for messages of the given type. * * @param messageType the type of the message * @return the message descriptor */ public static MessageDescriptor messageDescriptorForMessageType( String messageType) { try { Class<?> klass = Class.forName(messageType); MessageDescriptor result = null; synchronized (descriptors) { result = descriptors.get(klass); } return result; } catch (ClassNotFoundException e) { return null; } } // ---------------------------------------------------------- /** * Gets a value indicating whether this message should also be sent to the * system administrator e-mail addresses specified in the Web-CAT * installation wizard. The default behavior returns false, except for * messages that extend SysAdminMessage, which returns true. * * @return true if the message should also be sent to system * administrators, false otherwise */ public boolean isSevere() { return false; } // ---------------------------------------------------------- /** * Gets an object that implements {@link IMessageSettings} that contains * the settings to use for sending this message if it is a broadcast * message. The default behavior returns null, which indicates that the * system default settings should be used. Subclasses can override this * and return another object (usually a {@link ProtocolSettings}) to modify * the way the message is broadcast; for example, to post it to a * course-specific Twitter feed instead of the system feed. * * @return an object that implements {@link IMessageSettings}, or null to * use the system settings */ public IMessageSettings broadcastSettings() { return null; } // ---------------------------------------------------------- /** * <p> * Gets the list of users who should receive this message. If the message * is meant to be broadcast system-wide only, then this method may return * null. * </p><p> * This method is only called during the message sending cycle. * </p> * * @return an array of Users who should receive the message, or null if it * should only be broadcast system-wide */ public abstract NSArray<User> users(); // ---------------------------------------------------------- /** * <p> * Gets the title of the message. The message title is used in situations * where it is useful to differentiate a very brief description of the * message from its longer-form content (such as the subject line of an * e-mail). Titles are typically very brief, even shorter than the * {@link #shortBody()} of the message. * </p><p> * This method is only called during the message sending cycle. * </p> * * @return the title of the message */ public abstract String title(); // ---------------------------------------------------------- /** * <p> * Gets the short-form body content of the message. The short form is used * in the pop-up notification list, and is available for messaging * protocols that have constraints on the length of the content that they * can send (such as Twitter and SMS). * </p><p> * Subclasses should attempt to keep their short form content under 140 * characters as a rule of thumb, but no guarantees are made to ensure * this, so specific protocols may need to further elide this content if * necessary. * </p><p> * This method is only called during the message sending cycle. * </p> * * @return the short form of the message body */ public abstract String shortBody(); // ---------------------------------------------------------- /** * <p> * Gets the full-form body content of the message. The full form can be an * arbitrary length and should be used by protocols where there are no * tight constraints on message content length, such as the body of an * e-mail or description of an item in an RSS feed. * </p><p> * This method is only called during the message sending cycle. * </p> * * @return the full form of the message body */ public String fullBody() { return shortBody(); } // ---------------------------------------------------------- /** * <p> * Gets a set of attachments that should be sent along with the message, if * the protocol (such as e-mail) supports attachments. Since many messaging * protocols do not support attachments, there should not be any critical * information included here. This method may return null if there are no * attachments. * </p><p> * This method is only called during the message sending cycle. * </p> * * @return a dictionary of attachments, keyed by the name of the attachment */ public List<File> attachments() { return null; } // ---------------------------------------------------------- /** * <p> * Used by the message dispatcher to load the attachment data into memory. * Clients should never call this method directly. * </p> * * @return a dictionary of attachments, keyed by the name of the attachment */ final public NSDictionary<String, NSData> attachmentData() { List<File> attachmentFiles = attachments(); NSMutableDictionary<String, NSData> attachmentData = new NSMutableDictionary<String, NSData>(); for (File file : attachmentFiles) { try { NSData data = dataFromContentsOfFile(file); attachmentData.setObjectForKey(data, file.getName()); } catch (IOException e) { Log.error("Exception occurred while loading the attachment " + file, e); } } return attachmentData; } // ---------------------------------------------------------- /** * Returns an NSData object with the contents of the specified file. * * @param file the file * @return an NSData object containing the contents of the file * @throws IOException if an I/O error occurred */ private NSData dataFromContentsOfFile(File file) throws IOException { FileInputStream stream = new FileInputStream(file); NSData data = new NSData(stream, 0); stream.close(); return data; } // ---------------------------------------------------------- /** * <p> * Gets a set of URLs that link to relevant content for the message. This * method may return null if there are no links for a particular message. * </p><p> * This method is only called during the message sending cycle. * </p> * * @return a dictionary containing links to relevant content, keyed by * plain-text labels that describe each link */ public NSDictionary<String, String> links() { return null; } // ---------------------------------------------------------- /** * Sends the message, then disposes of it. */ public final synchronized void send() { if (disposed) { throw new IllegalStateException(getClass().getSimpleName() + ": send() called on disposed message. Each message can " + "only be sent once."); } run(new ECAction(editingContext()) { public void action() { // Use the application's registered message dispatcher to send // the message. Application.wcApplication().messageDispatcher() .sendMessage(Message.this); // Store the message in the database. storeMessage(); }}); // Dispose of message and its EC dispose(); } // ---------------------------------------------------------- /** * Stores the contents of this message in the database. * * @param descriptor the message descriptor for this message */ private synchronized void storeMessage() { final MessageDescriptor descriptor = messageDescriptor(); // The editing context is already locked by the caller // (or it will auto-lock). EOEditingContext ec = editingContext(); // Create a representation of the message in the database so that it // can be viewed by users later. SentMessage message = SentMessage.create(ec, false); message.setSentTime(new NSTimestamp()); message.setMessageType(messageType()); message.setTitle(title()); message.setShortBody(shortBody()); message.setIsBroadcast(descriptor.isBroadcast()); if (links() != null) { message.setLinks(new MutableDictionary(links())); } NSArray<User> users = users(); if (users != null) { for (User user : users) { // Sanity check to ensure that messages don't get sent to // users who shouldn't receive them based on their access // level. if (user.accessLevel() >= descriptor.accessLevel()) { message.addToUsersRelationship(user.localInstance(ec)); } } } ec.saveChanges(); } // ---------------------------------------------------------- /** * Gets the editing context used during the sending of this message. * Subclasses can access this editing context during the message sending * cycle if they need to fetch EOs. * * @return an editing context */ public synchronized EOEditingContext editingContext() { if (editingContext == null) { editingContext = WCEC.newAutoLockingEditingContext(); editingContext.setSharedEditingContext(null); } return editingContext; } // ---------------------------------------------------------- /** * Release the resources associated with this message (e.g., its internal * editing context) because this message will not be used again. */ protected synchronized void dispose() { if (disposed) { throw new IllegalStateException(getClass().getSimpleName() + ": dispose() called on disposed message."); } if (editingContext != null) { editingContext.dispose(); editingContext = null; } disposed = true; } //~ Static/instance variables ............................................. private EOEditingContext editingContext; private boolean disposed = false; private static Map<Class<? extends Message>, MessageDescriptor> descriptors = new HashMap<Class<? extends Message>, MessageDescriptor>(); }