001    /* 
002     * Copyright 2008-2009 the original author or authors.
003     * The contents of this file are subject to the Mozilla Public License
004     * Version 1.1 (the "License"); you may not use this file except in
005     * compliance with the License. You may obtain a copy of the License at
006     * http://www.mozilla.org/MPL/
007     *
008     * Software distributed under the License is distributed on an "AS IS"
009     * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
010     * License for the specific language governing rights and limitations
011     * under the License.
012     */
013     
014    package com.mtgi.analytics;
015    
016    import java.io.Serializable;
017    import java.util.Date;
018    
019    import org.apache.commons.logging.Log;
020    import org.apache.commons.logging.LogFactory;
021    
022    /**
023     * <i>Applications should generally not have to interact with BehaviorEvent directly.</i>
024     * 
025     * Represents an action taken directly or indirectly by a user.
026     * Each action is given a {@link #getType()}, {@link #getName()},
027     * and {@link #getApplication() application};
028     * has a {@link #getStart() start time} and {@link #getDuration()};
029     * and is associated with a {@link #getUserId() user} and {@link #getSessionId() session id}
030     * representing the user's current authenticated session with the application.
031     * Other data like parameter values can be stored in a semi-structured fashion
032     * in {@link #getData()}.  Generally the structure of the event data and
033     * the meaning of the {@link #getName()} should depend on the {@link #getType() type}.
034     * BehaviorEvents may be composed of smaller child events, which themselves
035     * may be composed of child events, and so on; an event's children are
036     * accessed with {@link #getChildren()}.
037     * 
038     * @see BehaviorEventManager
039     */
040    public class BehaviorEvent implements Serializable {
041    
042            private static final long serialVersionUID = -5341143588240860983L;
043            private static final Log log = LogFactory.getLog(BehaviorEvent.class);
044    
045            private Serializable id;
046            private BehaviorEvent parent;
047            private String type;
048            private String name;
049            private String application;
050            private String userId;
051            private String sessionId;
052            private Date start;
053            private Long duration;
054            private EventDataElement data = DeferredDataElement.INSTANCE;
055            private String error;
056            
057            protected BehaviorEvent(BehaviorEvent parent, String type, String name, String application, String userId, String sessionId) {
058                    this.parent = parent;
059                    this.type = type;
060                    this.name = name;
061                    this.application = application;
062                    this.userId = userId;
063                    this.sessionId = sessionId;
064            }
065    
066            @Override
067            protected void finalize() {
068                    if (isStarted() && !isEnded())
069                            log.warn("Event [" + type + ":" + name + ":" + id + "] was started and then discarded!");
070            }
071            
072            /**
073             * Notification that this event has started.  {@link #getStart()}
074             * will return the time at which the event began.
075             * 
076             * <i>This method should never be called directly by application code.</i>
077             * @see BehaviorTrackingManager#start(BehaviorEvent)
078             * @throws IllegalStateException if this event is already finished or started
079             */
080            protected void start() {
081                    if (isStarted())
082                            throw new IllegalStateException("Event has already started");
083                    start = new Date();
084            }
085            
086            /**
087             * Notification that this event is finished.  {@link #getDuration()}
088             * will return the time elapsed since {@link #start()} was called.
089             * 
090             * <i>This method should never be called directly by application code.</i>
091             * @see BehaviorTrackingManager#stop(BehaviorEvent)
092             * @throws IllegalStateException if this event is already finished or was never started
093             */
094            protected void stop() {
095                    if (!isStarted())
096                            throw new IllegalStateException("Event has not started");
097                    if (isEnded())
098                            throw new IllegalStateException("Event has already ended");
099                    duration = System.currentTimeMillis() - start.getTime();
100            }
101            
102            /** @return true if this event has started already */
103            protected boolean isStarted() {
104                    return start != null;
105            }
106    
107            /** @return true if this event is finished */
108            protected boolean isEnded() {
109                    return duration != null;
110            }
111            
112            /** @return true if this event is not nested in some other event */
113            protected boolean isRoot() {
114                    return parent == null;
115            }
116            
117            /**
118             * Get the name of the application in which this event took place.
119             */
120            public String getApplication() {
121                    return application;
122            }
123    
124            /**
125             * If this event has extra data, it can be accessed here.  Otherwise returns
126             * null.
127             * @see #addData()
128             */
129            public EventDataElement getData() {
130                    return data;
131            }
132            
133            /**
134             * If this event is finished, return its duration.  Otherwise return null.
135             */
136            public Long getDuration() {
137                    return duration;
138            }
139    
140            /** If this event ended in error, return a description of that error (null otherwise) */
141            public String getError() {
142                    return error;
143            }
144            public void setError(Throwable t) {
145                    setError(t.getClass().getName() + ": " + t.getMessage());
146            }
147            
148            public void setError(String error) {
149                    this.error = error;
150            }
151            
152            /** a unique identifier (e.g. primary key) for this event */
153            public Serializable getId() {
154                    return id;
155            }
156            public void setId(Serializable id) {
157                    this.id = id;
158            }
159            
160            /**
161             * Get the name of the event; for example, a method name for
162             * instrumented method calls, or a server request path for
163             * an instrumented servlet.
164             */
165            public String getName() {
166                    return name;
167            }
168            
169            /**
170             * If this event is a child of a larger composite event, 
171             * return the event's parent.  Otherwise returns null.
172             */
173            public BehaviorEvent getParent() {
174                    return parent;
175            }
176            
177            /**
178             * If this event is part of an authenticated session, return an
179             * identifier of that session.
180             */
181            public String getSessionId() {
182                    return sessionId;
183            }
184            
185            /**
186             * If this event has started, return the date at which the event began.
187             * Otherwise return null.
188             */
189            public Date getStart() {
190                    return start;
191            }
192    
193            /**
194             * Get the type of this event.  Many applications will only use
195             * one type of event (e.g. "method"), though some may define several.
196             * A single application should generally have only a smaller number of
197             * event types, corresponding to the layer of the system at which the
198             * event originated (e.g. "method", "rendering", "database").  It is
199             * suggested that the value of "type" should give some meaning to how
200             * the value of {@link #getName()} should be interpreted.  For example,
201             * an event type of "request" could signify that {@link #getName()} returns
202             * an HTTP request URL.  Generally though type/name schemes are up to the application.
203             */
204            public String getType() {
205                    return type;
206            }
207            
208            /**
209             * Get the user on whose behalf this event is executing.
210             */
211            public String getUserId() {
212                    return userId;
213            }
214            
215            /**
216             * Add metadata to this event, if metadata has not already been added.
217             * @return A container for event data.  Calling this method more than once always returns the same instance.
218             */
219            public EventDataElement addData() {
220                    return data.initialize(this);
221            }
222    
223            @Override
224            public String toString() {
225                    StringBuffer buf = new StringBuffer();
226                    
227                    buf.append("behavior-event:").append(" id=\"").append(id).append('"');
228                    if (parent != null)
229                            buf.append(" parent-id=\"").append(parent.getId()).append('"');
230                    buf.append(" type=\"").append(type).append('"')
231                       .append(" name=\"").append(name).append('"')
232                       .append(" application=\"").append(application).append('"')
233                       .append(" start=\"").append(start).append('"')
234                       .append(" duration-ms=\"").append(duration).append('"')
235                       .append(" user-id=\"").append(userId).append('"')
236                       .append(" session-id=\"").append(sessionId).append('"')
237                       .append(" error=\"").append(error).append('"');
238                    
239                    return buf.toString();
240            }
241            
242            /** 
243             * a singleton empty data element, for events that do not need to define any data.
244             * Replaces itself with a new, standard EventDataElement instance on the first call
245             * to {@link #initialize(BehaviorEvent)}.  This somewhat arcane construction is an optimization
246             * for calls to {@link #addData}, so that they do not require a null check.
247             */
248            private static class DeferredDataElement extends ImmutableEventDataElement {
249                    private static final long serialVersionUID = -4571142241188203441L;
250                    private static final DeferredDataElement INSTANCE = new DeferredDataElement();
251                    private DeferredDataElement() {
252                            super("event-data");
253                    }
254                    
255                    @Override
256                    public boolean isNull() {
257                            return true;
258                    }
259    
260                    @Override
261                    protected EventDataElement initialize(BehaviorEvent event) {
262                            return event.data = new EventDataElement("event-data");
263                    }
264            }
265    }