/* * Event.java * * Version: $Revision: 3762 $ * * Date: $Date: 2009-05-07 04:36:47 +0000 (Thu, 07 May 2009) $ * * Copyright (c) 2002-2009, The DSpace Foundation. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of the DSpace Foundation nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ package org.dspace.event; import java.io.Serializable; import java.sql.SQLException; import java.util.BitSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.log4j.Logger; import org.dspace.content.DSpaceObject; import org.dspace.core.Constants; import org.dspace.core.Context; /** * An Event object represents a single action that changed one object in the * DSpace data model. An "atomic" action at the application or business-logic * API level may spawn many of these events. * <p> * This class includes tools to help set and use the contents of the event. Note * that it describes DSpace data object types in two ways: by the type * identifiers in the Constants class, and also by an Event-specific bitmask * (used by its internal filters). All public API calls use the Constants * version of the data model types. * <p> * Note that the type of the event itself is actually descriptive of the * <em>action</em> it performs: ADD, MODIFY, etc. The most significant * elements of the event are: * <p> * <br> - (Action) Type <br> - Subject -- DSpace object to which the action * applies, e.g. the Collection to which an ADD adds a member. <br> - Object -- * optional, when present it is the other object effected by an action, e.g. the * Item ADDed to a Collection by an ADD. <br> - detail -- a textual summary of * what changed, content and its significance varies by the combination of * action and subject type. <br> - timestamp -- exact millisecond timestamp at * which event was logged. * * @version $Revision: 3762 $ */ public class Event implements Serializable { /** ---------- Constants ------------- * */ /** Event (Action) types */ public static final int CREATE = 1 << 0; // create new object public static final int MODIFY = 1 << 1; // modify object public static final int MODIFY_METADATA = 1 << 2; // modify object public static final int ADD = 1 << 3; // add content to container public static final int REMOVE = 1 << 4; // remove content from container public static final int DELETE = 1 << 5; // destroy object /** Index of filter parts in their array: */ public static final int SUBJECT_MASK = 0; // mask of subject types public static final int EVENT_MASK = 1; // mask of event type // XXX NOTE: keep this up to date with any changes to event (action) types. private static final String eventTypeText[] = { "CREATE", "MODIFY", "MODIFY_METADATA", "ADD", "REMOVE", "DELETE" }; /** XXX NOTE: These constants must be kept synchronized * */ /** XXX NOTE: with ALL_OBJECTS_MASK *AND* objTypeToMask hash * */ private static final int NONE = 0; private static final int BITSTREAM = 1 << Constants.BITSTREAM; // 0 private static final int BUNDLE = 1 << Constants.BUNDLE; // 1 private static final int ITEM = 1 << Constants.ITEM; // 2 private static final int COLLECTION = 1 << Constants.COLLECTION; // 3 private static final int COMMUNITY = 1 << Constants.COMMUNITY; // 4 private static final int SITE = 1 << Constants.SITE; // 5 private static final int GROUP = 1 << Constants.GROUP; // 6 private static final int EPERSON = 1 << Constants.EPERSON; // 7 private static final int ALL_OBJECTS_MASK = BITSTREAM | BUNDLE | ITEM | COLLECTION | COMMUNITY | SITE | GROUP | EPERSON; private static Map<Integer, Integer> objTypeToMask = new HashMap<Integer, Integer>(); private static Map<Integer, Integer> objMaskToType = new HashMap<Integer, Integer>(); static { objTypeToMask.put(new Integer(Constants.BITSTREAM), new Integer( BITSTREAM)); objMaskToType.put(new Integer(BITSTREAM), new Integer( Constants.BITSTREAM)); objTypeToMask.put(new Integer(Constants.BUNDLE), new Integer(BUNDLE)); objMaskToType.put(new Integer(BUNDLE), new Integer(Constants.BUNDLE)); objTypeToMask.put(new Integer(Constants.ITEM), new Integer(ITEM)); objMaskToType.put(new Integer(ITEM), new Integer(Constants.ITEM)); objTypeToMask.put(new Integer(Constants.COLLECTION), new Integer( COLLECTION)); objMaskToType.put(new Integer(COLLECTION), new Integer( Constants.COLLECTION)); objTypeToMask.put(new Integer(Constants.COMMUNITY), new Integer( COMMUNITY)); objMaskToType.put(new Integer(COMMUNITY), new Integer( Constants.COMMUNITY)); objTypeToMask.put(new Integer(Constants.SITE), new Integer(SITE)); objMaskToType.put(new Integer(SITE), new Integer(Constants.SITE)); objTypeToMask.put(new Integer(Constants.GROUP), new Integer(GROUP)); objMaskToType.put(new Integer(GROUP), new Integer(Constants.GROUP)); objTypeToMask.put(new Integer(Constants.EPERSON), new Integer(EPERSON)); objMaskToType.put(new Integer(EPERSON), new Integer(Constants.EPERSON)); } /** ---------- Event Fields ------------- * */ /** identifier of Dispatcher that created this event (hash of its name) */ private int dispatcher; /** event (action) type - above enumeration */ private int eventType; /** object-type of SUBJECT - see above enumeration */ private int subjectType; /** content model identifier */ private int subjectID; /** object-type of SUBJECT - see above enumeration */ private int objectType = NONE; /** content model identifier */ private int objectID = -1; /** timestamp */ private long timeStamp; /** "detail" - arbitrary field for relevant detail, */ /** e.g. former handle for DELETE event since obj is no longer available. */ /** * FIXME This field is not a complete view of the DSpaceObject that was * modified. Providing these objects to the consumer (e.g. by storing * lifecycle versions of the changed objects in the context) would provide * for more complex consumer abilities that are beyond our purview. */ private String detail; /** unique key to bind together events from one context's transaction */ private String transactionID; /** identity of authenticated user, i.e. context.getCurrentUser() */ /** only needed in the event for marshalling for asynch event messages */ private int currentUser = -1; /** copy of context's "extraLogInfo" filed, used only for */ /** marshalling for asynch event messages */ private String extraLogInfo = null; private BitSet consumedBy = new BitSet(); /** log4j category */ private static Logger log = Logger.getLogger(Event.class); /** * Constructor. * * @param eventType * action type, e.g. Event.ADD * @param subjectType * DSpace Object Type of subject e.g. Constants.ITEM. * @param subjectID * database ID of subject instance. * @param detail * detail information that depends on context. */ public Event(int eventType, int subjectType, int subjectID, String detail) { this.eventType = eventType; this.subjectType = coreTypeToMask(subjectType); this.subjectID = subjectID; timeStamp = System.currentTimeMillis(); this.detail = detail; } /** * Constructor. * * @param eventType * action type, e.g. Event.ADD * @param subjectType * DSpace Object Type of subject e.g. Constants.ITEM. * @param subjectID * database ID of subject instance. * @param objectType * DSpace Object Type of object e.g. Constants.BUNDLE. * @param objectID * database ID of object instance. * @param detail * detail information that depends on context. * @param */ public Event(int eventType, int subjectType, int subjectID, int objectType, int objectID, String detail) { this.eventType = eventType; this.subjectType = coreTypeToMask(subjectType); this.subjectID = subjectID; this.objectType = coreTypeToMask(objectType); this.objectID = objectID; timeStamp = System.currentTimeMillis(); this.detail = detail; } /** * Compare two events. Ignore any difference in the timestamps. Also ignore * transactionID since that is not always set initially. * * @param other * the event to compare this one to * @returns true if events are "equal", false otherwise. */ public boolean equals(Event other) { return (this.detail == null ? other.detail == null : this.detail .equals(other.detail)) && this.eventType == other.eventType && this.subjectType == other.subjectType && this.subjectID == other.subjectID && this.objectType == other.objectType && this.objectID == other.objectID; } /** * Set the identifier of the dispatcher that first processed this event. * * @param id * the unique (hash code) value characteristic of the dispatcher. */ public void setDispatcher(int id) { dispatcher = id; } // translate a "core.Constants" object type value to local bitmask value. private static int coreTypeToMask(int core) { Integer mask = (Integer) objTypeToMask.get(new Integer(core)); if (mask == null) return -1; else return mask.intValue(); } // translate bitmask object-type to "core.Constants" object type. private static int maskTypeToCore(int mask) { Integer core = (Integer) objMaskToType.get(new Integer(mask)); if (core == null) return -1; else return core.intValue(); } /** * Get the DSpace object which is the "object" of an event. * * @returns DSpaceObject or null if none can be found or no object was set. */ public DSpaceObject getObject(Context context) throws SQLException { int type = getObjectType(); int id = getObjectID(); if (type < 0 || id < 0) return null; else return DSpaceObject.find(context, type, id); } /** * Syntactic sugar to get the DSpace object which is the "subject" of an * event. * * @returns DSpaceObject or null if none can be found. */ public DSpaceObject getSubject(Context context) throws SQLException { return DSpaceObject.find(context, getSubjectType(), getSubjectID()); } /** * @returns database ID of subject of this event. */ public int getSubjectID() { return subjectID; } /** * @returns database ID of object of this event, or -1 if none was set. */ public int getObjectID() { return objectID; } /** * @returns type number (e.g. Constants.ITEM) of subject of this event. */ public int getSubjectType() { return maskTypeToCore(subjectType); } /** * @returns type number (e.g. Constants.ITEM) of object of this event, or -1 * if none was set. */ public int getObjectType() { return maskTypeToCore(objectType); } /** * @returns type of subject of this event as a String, e.g. for logging. */ public String getSubjectTypeAsString() { int i = log2(subjectType); if (i >= 0 && i < Constants.typeText.length) return Constants.typeText[i]; else return "(Unknown)"; } /** * @returns type of object of this event as a String, e.g. for logging. */ public String getObjectTypeAsString() { int i = log2(objectType); if (i >= 0 && i < Constants.typeText.length) return Constants.typeText[i]; else return "(Unknown)"; } /** * Translate a textual DSpace Object type name into an event subject-type * mask. NOTE: This returns a BIT-MASK, not a numeric type value; the mask * is only used within the event system. * * @param s * text name of object type. * @returns numeric value of object type or 0 for error. */ public static int parseObjectType(String s) { if ("*".equals(s) || "all".equalsIgnoreCase(s)) return ALL_OBJECTS_MASK; else { int id = Constants.getTypeID(s.toUpperCase()); if (id >= 0) return 1 << id; } return 0; } /** * @returns event-type (i.e. action) this event, one of the masks like * Event.ADD defined above. */ public int getEventType() { return eventType; } /** * Get the text name of event (action) type. * * @returns event-type (i.e. action) this event as a String, e.g. for * logging. */ public String getEventTypeAsString() { int i = log2(eventType); if (i >= 0 && i < eventTypeText.length) return eventTypeText[i]; else return "(Unknown)"; } /** * Interpret named event type. * * @param text * name of event type. * @returns numeric value of event type or 0 for error. */ public static int parseEventType(String s) { if ("*".equals(s) || "all".equalsIgnoreCase(s)) { int result = 0; for (int i = 0; i < eventTypeText.length; ++i) result |= (1 << i); return result; } for (int i = 0; i < eventTypeText.length; ++i) if (eventTypeText[i].equalsIgnoreCase(s)) return 1 << i; return 0; } /** * @returns timestamp at which event occurred, as a count of milliseconds * since the epoch (standard Java format). */ public long getTimeStamp() { return timeStamp; } /** * @returns hashcode identifier of name of Dispatcher which first dispatched * this event. (Needed by asynch dispatch code.) */ public int getDispatcher() { return dispatcher; } /** * @returns value of detail element of the event. */ public String getDetail() { return detail; } /** * @returns value of transactionID element of the event. */ public String getTransactionID() { return transactionID; } /** * Sets value of transactionID element of the event. * * @param tid * new value of transactionID. */ public void setTransactionID(String tid) { transactionID = tid; } public void setCurrentUser(int uid) { currentUser = uid; } public int getCurrentUser() { return currentUser; } public void setExtraLogInfo(String info) { extraLogInfo = info; } public String getExtraLogInfo() { return extraLogInfo; } /** * @param filters * list of filter masks; each one is an Array of two ints. * @returns true if this event would be passed through the given filter * list. */ public boolean pass(List filters) { boolean result = false; for (Iterator fi = filters.iterator(); fi.hasNext();) { int filter[] = (int[]) fi.next(); if ((subjectType & filter[SUBJECT_MASK]) != 0 && (eventType & filter[EVENT_MASK]) != 0) result = true; } if (log.isDebugEnabled()) log.debug("Filtering event: " + "eventType=" + String.valueOf(eventType) + ", subjectType=" + String.valueOf(subjectType) + ", result=" + String.valueOf(result)); return result; } // dumb integer "log base 2", returns -1 if there are no 1's in number. private static int log2(int n) { for (int i = 0; i < 32; ++i) if (n == 1) return i; else n = n >> 1; return -1; } /** * Keeps track of which consumers the event has been consumed by. Should be * called by a dispatcher when calling consume(Context ctx, String name, * Event event) on an event. * * @param consumerName */ public void setBitSet(String consumerName) { consumedBy.set(EventManager.getConsumerIndex(consumerName)); } public BitSet getBitSet() { return consumedBy; } /** * @returns Detailed string representation of contents of this event, to * help in logging and debugging. */ public String toString() { return "org.dspace.event.Event(eventType=" + this.getEventTypeAsString() + ", SubjectType=" + this.getSubjectTypeAsString() + ", SubjectID=" + String.valueOf(subjectID) + ", ObjectType=" + this.getObjectTypeAsString() + ", ObjectID=" + String.valueOf(objectID) + ", TimeStamp=" + String.valueOf(timeStamp) + ", dispatcher=" + String.valueOf(dispatcher) + ", detail=" + (detail == null ? "[null]" : "\"" + detail + "\"") + ", transactionID=" + (transactionID == null ? "[null]" : "\"" + transactionID + "\"") + ")"; } }