/** * $URL: https://source.sakaiproject.org/svn/sitestats/trunk/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/StatsUpdateManagerImpl.java $ * $Id: StatsUpdateManagerImpl.java 116373 2012-11-14 18:40:48Z matthew.buckett@it.ox.ac.uk $ * * Copyright (c) 2006-2009 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sakaiproject.sitestats.impl; import java.sql.SQLException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.Criteria; import org.hibernate.HibernateException; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.criterion.Expression; import org.hibernate.criterion.Order; import org.sakaiproject.alias.api.AliasService; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.entity.api.EntityManager; import org.sakaiproject.event.api.Event; import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.event.api.UsageSession; import org.sakaiproject.event.api.UsageSessionService; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SiteService; import org.sakaiproject.sitestats.api.EventStat; import org.sakaiproject.sitestats.api.JobRun; import org.sakaiproject.sitestats.api.ResourceStat; import org.sakaiproject.sitestats.api.ServerStat; import org.sakaiproject.sitestats.api.SiteActivity; import org.sakaiproject.sitestats.api.SitePresence; import org.sakaiproject.sitestats.api.SiteVisits; import org.sakaiproject.sitestats.api.StatsManager; import org.sakaiproject.sitestats.api.StatsUpdateManager; import org.sakaiproject.sitestats.api.UserStat; import org.sakaiproject.sitestats.api.Util; import org.sakaiproject.sitestats.api.event.EventRegistryService; import org.sakaiproject.sitestats.api.event.ToolInfo; import org.sakaiproject.sitestats.api.parser.EventParserTip; import org.springframework.orm.hibernate3.HibernateCallback; import org.springframework.orm.hibernate3.support.HibernateDaoSupport; /** * @author <a href="mailto:nuno@ufp.pt">Nuno Fernandes</a> */ public class StatsUpdateManagerImpl extends HibernateDaoSupport implements Runnable, StatsUpdateManager, Observer { private Log LOG = LogFactory.getLog(StatsUpdateManagerImpl.class); private final static String PRESENCE_SUFFIX = "-presence"; private final static int PRESENCE_SUFFIX_LENGTH = PRESENCE_SUFFIX.length(); /** Spring bean members */ private boolean collectThreadEnabled = true; public long collectThreadUpdateInterval = 4000L; private boolean collectAdminEvents = false; private boolean collectEventsForSiteWithToolOnly = true; /** Sakai services */ private StatsManager M_sm; private EventRegistryService M_ers; private SiteService M_ss; private AliasService M_as; private EntityManager M_em; private UsageSessionService M_uss; private EventTrackingService M_ets; /** Collect Thread and Semaphore */ private Thread collectThread; private List<Event> collectThreadQueue = new ArrayList<Event>(); private Object collectThreadSemaphore = new Object(); private boolean collectThreadRunning = false; /** Collect thread queue maps */ private Map<String, EventStat> eventStatMap = Collections.synchronizedMap(new HashMap<String, EventStat>()); private Map<String, ResourceStat> resourceStatMap = Collections.synchronizedMap(new HashMap<String, ResourceStat>()); private Map<String, SiteActivity> activityMap = Collections.synchronizedMap(new HashMap<String, SiteActivity>()); private Map<String, SiteVisits> visitsMap = Collections.synchronizedMap(new HashMap<String, SiteVisits>()); private Map<String, SitePresenceConsolidation> presencesMap = Collections.synchronizedMap(new HashMap<String, SitePresenceConsolidation>()); private Map<UniqueVisitsKey, Integer> uniqueVisitsMap = Collections.synchronizedMap(new HashMap<UniqueVisitsKey, Integer>()); private Map<String, ServerStat> serverStatMap = Collections.synchronizedMap(new HashMap<String, ServerStat>()); private Map<String, UserStat> userStatMap = Collections.synchronizedMap(new HashMap<String, UserStat>()); private boolean initialized = false; private final ReentrantLock lock = new ReentrantLock(); /** Metrics */ private boolean isIdle = true; private long totalEventsProcessed = 0; private long totalTimeInEventProcessing = 0; private long resetTime = System.currentTimeMillis(); // ################################################################ // Spring related methods // ################################################################ public void setCollectThreadEnabled(boolean enabled) { this.collectThreadEnabled = enabled; if(initialized) { if(enabled && !collectThreadRunning) { // start update thread startUpdateThread(); // add this as EventInfo observer M_ets.addLocalObserver(this); }else if(!enabled && collectThreadRunning){ // remove this as EventInfo observer M_ets.deleteObserver(this); // stop update thread stopUpdateThread(); } } } public boolean isCollectThreadEnabled() { return collectThreadEnabled; } public void setCollectThreadUpdateInterval(long dbUpdateInterval){ this.collectThreadUpdateInterval = dbUpdateInterval; } public long getCollectThreadUpdateInterval(){ return collectThreadUpdateInterval; } public void setCollectAdminEvents(boolean value){ this.collectAdminEvents = value; } public boolean isCollectAdminEvents(){ return collectAdminEvents; } public void setCollectEventsForSiteWithToolOnly(boolean value){ this.collectEventsForSiteWithToolOnly = value; } public boolean isCollectEventsForSiteWithToolOnly(){ return collectEventsForSiteWithToolOnly; } public void setStatsManager(StatsManager mng){ this.M_sm = mng; } public void setEventRegistryService(EventRegistryService eventRegistryService) { this.M_ers = eventRegistryService; } public void setSiteService(SiteService ss){ this.M_ss = ss; } public void setAliasService(AliasService as) { this.M_as = as; } public void setEntityManager(EntityManager em) { this.M_em = em; } public void setEventTrackingService(EventTrackingService ets){ this.M_ets = ets; } public void setUsageSessionService(UsageSessionService uss){ this.M_uss = uss; } public void init(){ StringBuilder buff = new StringBuilder(); buff.append("init(): collect thread enabled: "); buff.append(collectThreadEnabled); if(collectThreadEnabled) { buff.append(", db update interval: "); buff.append(collectThreadUpdateInterval); buff.append(" ms"); } buff.append(", collect administrator events: " + collectAdminEvents); buff.append(", collect events only for sites with SiteStats: " + collectEventsForSiteWithToolOnly); logger.info(buff.toString()); initialized = true; setCollectThreadEnabled(collectThreadEnabled); } public void destroy(){ if(collectThreadEnabled) { // remove this as EventInfo observer M_ets.deleteObserver(this); // stop update thread stopUpdateThread(); } } // ################################################################ // Public methods // ################################################################ public Event buildEvent(Date date, String event, String ref, String sessionUser, String sessionId) { return new CustomEventImpl(date, event, ref, sessionUser, sessionId); } public Event buildEvent(Date date, String event, String ref, String context, String sessionUser, String sessionId) { return new CustomEventImpl(date, event, ref, context, sessionUser, sessionId); } /* (non-Javadoc) * @see org.sakaiproject.sitestats.api.StatsUpdateManager#collectEvent(org.sakaiproject.event.api.Event) */ public boolean collectEvent(Event e) { if(e != null) { long startTime = System.currentTimeMillis(); isIdle = false; preProcessEvent(e); //long endTime = System.currentTimeMillis(); //LOG.debug("Time spent pre-processing 1 event: " + (endTime-startTime) + " ms"); boolean success = doUpdateConsolidatedEvents(); isIdle = true; totalTimeInEventProcessing += (System.currentTimeMillis() - startTime); return success; } return true; } /* (non-Javadoc) * @see org.sakaiproject.sitestats.api.StatsUpdateManager#collectEvents(java.util.List) */ public boolean collectEvents(List<Event> events) { if(events != null) { int eventCount = events.size(); if(eventCount > 0) { return collectEvents(events.toArray(new Event[eventCount])); } } return true; } /* (non-Javadoc) * @see org.sakaiproject.sitestats.api.StatsUpdateManager#collectEvents(org.sakaiproject.event.api.Event[]) */ public boolean collectEvents(Event[] events) { if(events != null) { int eventCount = events.length; if(eventCount > 0) { long startTime = System.currentTimeMillis(); isIdle = false; for(int i=0; i<events.length; i++){ if(events[i] != null) { preProcessEvent(events[i]); } } //long endTime = System.currentTimeMillis(); //LOG.debug("Time spent pre-processing " + eventCount + " event(s): " + (endTime-startTime) + " ms"); boolean success = doUpdateConsolidatedEvents(); isIdle = true; totalTimeInEventProcessing += (System.currentTimeMillis() - startTime); return success; } } return true; } /* (non-Javadoc) * @see org.sakaiproject.sitestats.api.StatsUpdateManager#collectPastSiteEvents(java.lang.String, java.util.Date, java.util.Date) */ public long collectPastSiteEvents(String siteId, Date initialDate, Date finalDate) { StatsAggregateJobImpl statsAggregateJob = (StatsAggregateJobImpl) ComponentManager.get("org.sakaiproject.sitestats.api.StatsAggregateJob"); return statsAggregateJob.collectPastSiteEvents(siteId, initialDate, finalDate); } /* (non-Javadoc) * @see org.sakaiproject.sitestats.api.StatsUpdateManager#saveJobRun(org.sakaiproject.sitestats.api.JobRun) */ public boolean saveJobRun(final JobRun jobRun){ if(jobRun == null) { return false; } Object r = getHibernateTemplate().execute(new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { Transaction tx = null; try{ tx = session.beginTransaction(); session.saveOrUpdate(jobRun); tx.commit(); }catch(Exception e){ if(tx != null) tx.rollback(); LOG.warn("Unable to commit transaction: ", e); return Boolean.FALSE; } return Boolean.TRUE; } }); return ((Boolean) r).booleanValue(); } /* (non-Javadoc) * @see org.sakaiproject.sitestats.api.StatsUpdateManager#getLatestJobRun() */ public JobRun getLatestJobRun() throws Exception { Object r = getHibernateTemplate().execute(new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { JobRun jobRun = null; Criteria c = session.createCriteria(JobRunImpl.class); c.setMaxResults(1); c.addOrder(Order.desc("id")); List jobs = c.list(); if(jobs != null && jobs.size() > 0){ jobRun = (JobRun) jobs.get(0); } return jobRun; } }); return (JobRun) r; } /* (non-Javadoc) * @see org.sakaiproject.sitestats.api.StatsUpdateManager#getEventDateFromLatestJobRun() */ public Date getEventDateFromLatestJobRun() throws Exception { Object r = getHibernateTemplate().execute(new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { Criteria c = session.createCriteria(JobRunImpl.class); c.add(Expression.isNotNull("lastEventDate")); c.setMaxResults(1); c.addOrder(Order.desc("id")); List jobs = c.list(); if(jobs != null && jobs.size() > 0){ JobRun jobRun = (JobRun) jobs.get(0); return jobRun.getLastEventDate(); } return null; } }); return (Date) r; } // ################################################################ // Metrics related methods // ################################################################ public int getQueueSize() { return collectThreadQueue.size(); } public boolean isIdle() { return this.isIdle && getQueueSize() == 0; } public void resetMetrics() { totalEventsProcessed = 0; totalTimeInEventProcessing = 0; resetTime = System.currentTimeMillis(); } public long getNumberOfEventsProcessed() { return totalEventsProcessed; } public long getTotalTimeInEventProcessing() { return totalTimeInEventProcessing; } public long getResetTime() { return resetTime; } public long getTotalTimeEllapsedSinceReset() { return System.currentTimeMillis() - resetTime; } public double getNumberOfEventsProcessedPerSec() { if(totalTimeInEventProcessing > 0) { return Util.round((double)totalEventsProcessed / ((double)totalTimeInEventProcessing/1000), 3); }else{ return Util.round((double)totalEventsProcessed / 0.001, 3); // => will assume 1ms instead of 0ms } } public double getNumberOfEventsGeneratedPerSec() { double ellapsed = (double) getTotalTimeEllapsedSinceReset(); if(ellapsed > 0) { return Util.round((double)totalEventsProcessed / (ellapsed/1000), 3); }else{ return Util.round((double)totalEventsProcessed / 0.001, 3); // => will assume 1ms instead of 0ms } } public long getAverageTimeInEventProcessingPerEvent() { if(totalEventsProcessed > 0) { return totalTimeInEventProcessing / totalEventsProcessed; }else{ return 0; } } public String getMetricsSummary(boolean compact) { StringBuilder sb = new StringBuilder(); if(!compact) { sb.append("SiteStats Event aggregation metrics summary:\n"); sb.append("\t\tNumber of total events processed: ").append(getNumberOfEventsProcessed()).append("\n"); sb.append("\t\tReset/init time: ").append(new Date(getResetTime())).append("\n"); sb.append("\t\tTotal time ellapsed since reset: ").append(getTotalTimeEllapsedSinceReset()).append(" ms\n"); sb.append("\t\tTotal time spent processing events: ").append(getTotalTimeInEventProcessing()).append(" ms\n"); sb.append("\t\tNumber of events processed per sec: ").append(getNumberOfEventsProcessedPerSec()).append("\n"); sb.append("\t\tNumber of events genereated in Sakai per sec: ").append(getNumberOfEventsGeneratedPerSec()).append("\n"); sb.append("\t\tAverage time spent in event processing per event: ").append(getAverageTimeInEventProcessingPerEvent()).append(" ms\n"); sb.append("\t\tEvent queue size: ").append(getQueueSize()).append("\n"); sb.append("\t\tIdle: ").append(isIdle()); }else{ sb.append("#Events processed: ").append(getNumberOfEventsProcessed()).append(", "); sb.append("Time ellapsed since reset: ").append(getTotalTimeEllapsedSinceReset()).append(" ms, "); sb.append("Time spent processing events: ").append(getTotalTimeInEventProcessing()).append(" ms, "); sb.append("#Events processed/sec: ").append(getNumberOfEventsProcessedPerSec()).append(", "); sb.append("Avg. Time/event: ").append(getAverageTimeInEventProcessingPerEvent()).append(" ms, "); sb.append("Event queue size: ").append(getQueueSize()).append(", "); sb.append("Idle: ").append(isIdle()); } return sb.toString(); } // ################################################################ // Update thread related methods // ################################################################ /** Method called whenever an new event is generated from EventTrackingService: do not call this method! */ public void update(Observable obs, Object o) { // At the moment this isn't threadsafe, but as sakai event handling is single threaded this shoudn't be a problem, // but it's not a formal contract. if(o instanceof Event){ Event e = (Event) o; Event eventWithPreciseDate = buildEvent(getToday(), e.getEvent(), e.getResource(), e.getContext(), e.getUserId(), e.getSessionId()); collectThreadQueue.add(eventWithPreciseDate); } } /** Update thread: do not call this method! */ public void run(){ try{ LOG.debug("Started statistics update thread"); while(collectThreadRunning){ // do update job isIdle = false; long startTime = System.currentTimeMillis(); int eventCount = collectThreadQueue.size(); if(eventCount > 0) { //long startTime2 = System.currentTimeMillis(); while(collectThreadQueue.size() > 0){ preProcessEvent(collectThreadQueue.remove(0)); } //long endTime2 = System.currentTimeMillis(); //LOG.debug("Time spent pre-processing " + eventCount + " event(s): " + (endTime2-startTime2) + " ms"); } doUpdateConsolidatedEvents(); isIdle = true; totalTimeInEventProcessing += (System.currentTimeMillis() - startTime); // sleep if no work to do if(!collectThreadRunning) break; try{ synchronized (collectThreadSemaphore){ collectThreadSemaphore.wait(collectThreadUpdateInterval); } }catch(InterruptedException e){ LOG.warn("Failed to sleep statistics update thread",e); } } }catch(Throwable t){ LOG.debug("Failed to execute statistics update thread",t); }finally{ if(collectThreadRunning){ // thread was stopped by an unknown error: restart LOG.debug("Statistics update thread was stoped by an unknown error: restarting..."); startUpdateThread(); }else LOG.debug("Finished statistics update thread"); } } /** Start the update thread */ private void startUpdateThread(){ collectThreadRunning = true; collectThread = null; collectThread = new Thread(this, "org.sakaiproject.sitestats.impl.StatsUpdateManagerImpl"); collectThread.start(); } /** Stop the update thread */ private void stopUpdateThread(){ collectThreadRunning = false; synchronized (collectThreadSemaphore){ collectThreadSemaphore.notifyAll(); } } // ################################################################ // Event process methods // ################################################################ private void preProcessEvent(Event e) { totalEventsProcessed++; String userId = e.getUserId(); e = fixMalFormedEvents(e); if(getRegisteredEvents().contains(e.getEvent()) && isValidEvent(e)){ // site check String siteId = parseSiteId(e); if(siteId == null || M_ss.isUserSite(siteId) || M_ss.isSpecialSite(siteId)){ return; } Site site = getSite(siteId); if(site == null) { return; } if(isCollectEventsForSiteWithToolOnly() && site.getToolForCommonId(StatsManager.SITESTATS_TOOLID) == null) { return; } // user check if(userId == null) { UsageSession session = M_uss.getSession(e.getSessionId()); if(session != null) { userId = session.getUserId(); } } if(!isCollectAdminEvents() && ("admin").equals(userId)){ return; }if(!M_sm.isShowAnonymousAccessEvents() && ("?").equals(userId)){ return; } // consolidate event Date date = null; if(e instanceof CustomEventImpl){ date = ((CustomEventImpl) e).getDate(); }else{ date = getToday(); } String eventId = e.getEvent(); String resourceRef = e.getResource(); if(userId == null || eventId == null || resourceRef == null) return; consolidateEvent(date, eventId, resourceRef, userId, siteId); } else if(getServerEvents().contains(e.getEvent())){ //it's a server event if(LOG.isDebugEnabled()) { LOG.debug("Server event: "+e.toString()); } String eventId = e.getEvent(); if(eventId == null) { return; } Date date = new Date(); consolidateServerEvent(date, eventId); } //we do this separately as we want individual login stats as well as totals from the server stats section if (isUserLoginEvent(e)) { //it's a user event if(LOG.isDebugEnabled()) { LOG.debug("User event: "+e.toString()); } // user check if(userId == null) { UsageSession session = M_uss.getSession(e.getSessionId()); if(session != null) { userId = session.getUserId(); } } if(userId == null) { return; } Date date = new Date(); consolidateUserEvent(date, userId); } //else LOG.debug("EventInfo ignored: '"+e.toString()+"' ("+e.toString()+") USER_ID: "+userId); } private void consolidateEvent(Date dateTime, String eventId, String resourceRef, String userId, String siteId) { if(eventId == null) return; Date date = getTruncatedDate(dateTime); // update if(getRegisteredEvents().contains(eventId) && !StatsManager.SITEVISITEND_EVENTID.equals(eventId)){ // add to eventStatMap String key = userId+siteId+eventId+date; synchronized(eventStatMap){ EventStat e1 = eventStatMap.get(key); if(e1 == null){ e1 = new EventStatImpl(); e1.setUserId(userId); e1.setSiteId(siteId); e1.setEventId(eventId); e1.setDate(date); } e1.setCount(e1.getCount() + 1); eventStatMap.put(key, e1); } if(!StatsManager.SITEVISIT_EVENTID.equals(eventId)){ // add to activityMap String key2 = siteId+date+eventId; synchronized(activityMap){ SiteActivity e2 = activityMap.get(key2); if(e2 == null){ e2 = new SiteActivityImpl(); e2.setSiteId(siteId); e2.setDate(date); e2.setEventId(eventId); } e2.setCount(e2.getCount() + 1); activityMap.put(key2, e2); } } } if(eventId.startsWith(StatsManager.RESOURCE_EVENTID_PREFIX)){ // add to resourceStatMap String resourceAction = null; try{ resourceAction = eventId.split("\\.")[1]; }catch(ArrayIndexOutOfBoundsException ex){ resourceAction = eventId; } String key = userId+siteId+resourceRef+resourceAction+date; synchronized(resourceStatMap){ ResourceStat e1 = resourceStatMap.get(key); if(e1 == null){ e1 = new ResourceStatImpl(); e1.setUserId(userId); e1.setSiteId(siteId); e1.setResourceRef(resourceRef); e1.setResourceAction(resourceAction); e1.setDate(date); } e1.setCount(e1.getCount() + 1); resourceStatMap.put(key, e1); } }else if(StatsManager.SITEVISIT_EVENTID.equals(eventId)){ // add to visitsMap String key = siteId+date; lock.lock(); try{ SiteVisits e1 = visitsMap.get(key); if(e1 == null){ e1 = new SiteVisitsImpl(); e1.setSiteId(siteId); e1.setDate(date); } e1.setTotalVisits(e1.getTotalVisits() + 1); // unique visits are determined when updating to db: // --> e1.setTotalUnique(totalUnique); visitsMap.put(key, e1); // place entry on map so we can update unique visits later UniqueVisitsKey keyUniqueVisits = new UniqueVisitsKey(siteId, date); uniqueVisitsMap.put(keyUniqueVisits, Integer.valueOf(1)); // site presence started if(M_sm.isEnableSitePresences()) { String pKey = siteId+userId+date; SitePresenceConsolidation spc = presencesMap.get(pKey); if(spc == null) { SitePresence sp = new SitePresenceImpl(); sp.setSiteId(siteId); sp.setUserId(userId); sp.setDate(date); spc = new SitePresenceConsolidation(sp); } spc.sitePresence.setLastVisitStartTime(dateTime); presencesMap.put(pKey, spc); } }finally{ lock.unlock(); } }else if(StatsManager.SITEVISITEND_EVENTID.equals(eventId)){ // site presence ended if(M_sm.isEnableSitePresences()) { String pKey = siteId+userId+date; lock.lock(); try{ SitePresenceConsolidation spc = presencesMap.get(pKey); if(spc == null) { SitePresence sp = new SitePresenceImpl(); sp.setSiteId(siteId); sp.setUserId(userId); sp.setDate(date); sp.setLastVisitStartTime(null); spc = new SitePresenceConsolidation(sp, dateTime); } if(spc.sitePresence.getLastVisitStartTime() != null) { long existingDuration = spc.sitePresence.getDuration(); long start = spc.sitePresence.getLastVisitStartTime().getTime(); long thisEventTime = dateTime.getTime(); long additionalDuration = thisEventTime - start; if(additionalDuration > 4*60*60*1000) { LOG.warn("A site presence is longer than 4h!: duration="+(additionalDuration/1000/60)+" min (SITE:"+siteId+", USER:"+userId+", DATE:"+date+")"); } spc.sitePresence.setDuration(existingDuration + additionalDuration); spc.sitePresence.setLastVisitStartTime(null); } presencesMap.put(pKey, spc); }finally{ lock.unlock(); } } } } //STAT-299 consolidate a server event private void consolidateServerEvent(Date dateTime, String eventId) { Date date = getTruncatedDate(dateTime); // add to serverStatMap String key = eventId+date; synchronized(serverStatMap){ ServerStat s = serverStatMap.get(key); if(s == null){ s = new ServerStatImpl(); s.setEventId(eventId); s.setDate(date); } s.setCount(s.getCount() + 1); serverStatMap.put(key, s); } } //STAT-299 consolidate a user event private void consolidateUserEvent(Date dateTime, String userId) { Date date = getTruncatedDate(dateTime); // add to userStatMap String key = userId+date; synchronized(userStatMap){ UserStat s = userStatMap.get(key); if(s == null){ s = new UserStatImpl(); s.setUserId(userId); s.setDate(date); } s.setCount(s.getCount() + 1); userStatMap.put(key, s); } } // ################################################################ // Db update methods // ################################################################ @SuppressWarnings("unchecked") private synchronized boolean doUpdateConsolidatedEvents() { long startTime = System.currentTimeMillis(); if(eventStatMap.size() > 0 || resourceStatMap.size() > 0 || activityMap.size() > 0 || uniqueVisitsMap.size() > 0 || visitsMap.size() > 0 || presencesMap.size() > 0 || serverStatMap.size() > 0 || userStatMap.size() > 0) { Object r = getHibernateTemplate().execute(new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { Transaction tx = null; try{ tx = session.beginTransaction(); // do: EventStat if(eventStatMap.size() > 0) { Collection<EventStat> tmp1 = null; synchronized(eventStatMap){ tmp1 = eventStatMap.values(); eventStatMap = Collections.synchronizedMap(new HashMap<String, EventStat>()); } doUpdateEventStatObjects(session, tmp1); } // do: ResourceStat if(resourceStatMap.size() > 0) { Collection<ResourceStat> tmp2 = null; synchronized(resourceStatMap){ tmp2 = resourceStatMap.values(); resourceStatMap = Collections.synchronizedMap(new HashMap<String, ResourceStat>()); } doUpdateResourceStatObjects(session, tmp2); } // do: SiteActivity if(activityMap.size() > 0) { Collection<SiteActivity> tmp3 = null; synchronized(activityMap){ tmp3 = activityMap.values(); activityMap = Collections.synchronizedMap(new HashMap<String, SiteActivity>()); } doUpdateSiteActivityObjects(session, tmp3); } // do: SiteVisits if(uniqueVisitsMap.size() > 0 || visitsMap.size() > 0) { // determine unique visits for event related sites Map<UniqueVisitsKey, Integer> tmp4; synchronized(uniqueVisitsMap){ tmp4 = uniqueVisitsMap; uniqueVisitsMap = Collections.synchronizedMap(new HashMap<UniqueVisitsKey, Integer>()); } tmp4 = doGetSiteUniqueVisits(session, tmp4); // do: SiteVisits if(visitsMap.size() > 0) { Collection<SiteVisits> tmp5 = null; synchronized(visitsMap){ tmp5 = visitsMap.values(); visitsMap = Collections.synchronizedMap(new HashMap<String, SiteVisits>()); } doUpdateSiteVisitsObjects(session, tmp5, tmp4); } } // do: SitePresences if(presencesMap.size() > 0) { Collection<SitePresenceConsolidation> tmp6 = null; synchronized(presencesMap){ tmp6 = presencesMap.values(); presencesMap = Collections.synchronizedMap(new HashMap<String, SitePresenceConsolidation>()); } doUpdateSitePresencesObjects(session, tmp6); } // do: ServerStats if(serverStatMap.size() > 0) { Collection<ServerStat> tmp7 = null; synchronized(serverStatMap){ tmp7 = serverStatMap.values(); serverStatMap = Collections.synchronizedMap(new HashMap<String, ServerStat>()); } doUpdateServerStatObjects(session, tmp7); } // do: UserStats if(userStatMap.size() > 0) { Collection<UserStat> tmp8 = null; synchronized(userStatMap){ tmp8 = userStatMap.values(); userStatMap = Collections.synchronizedMap(new HashMap<String, UserStat>()); } doUpdateUserStatObjects(session, tmp8); } // commit ALL tx.commit(); }catch(Exception e){ if(tx != null) tx.rollback(); LOG.warn("Unable to commit transaction: ", e); return Boolean.FALSE; } return Boolean.TRUE; } }); long endTime = System.currentTimeMillis(); LOG.debug("Time spent in doUpdateConsolidatedEvents(): " + (endTime-startTime) + " ms"); return ((Boolean) r).booleanValue(); }else{ return true; } } private void doUpdateEventStatObjects(Session session, Collection<EventStat> objects) { if(objects == null) return; Iterator<EventStat> i = objects.iterator(); while(i.hasNext()){ EventStat eUpdate = i.next(); String eExistingSiteId = null; EventStat eExisting = null; try{ Criteria c = session.createCriteria(EventStatImpl.class); c.add(Expression.eq("siteId", eUpdate.getSiteId())); c.add(Expression.eq("eventId", eUpdate.getEventId())); c.add(Expression.eq("userId", eUpdate.getUserId())); c.add(Expression.eq("date", eUpdate.getDate())); try{ eExisting = (EventStat) c.uniqueResult(); }catch(HibernateException ex){ try{ List events = c.list(); if ((events!=null) && (events.size()>0)){ LOG.debug("More than 1 result when unique result expected.", ex); eExisting = (EventStat) c.list().get(0); }else{ LOG.debug("No result found", ex); eExisting = null; } }catch(Exception ex3){ eExisting = null; } }catch(Exception ex2){ LOG.debug("Probably ddbb error when loading data at java object", ex2); System.out.println("Probably ddbb error when loading data at java object!!!!!!!!"); } if(eExisting == null) eExisting = eUpdate; else eExisting.setCount(eExisting.getCount() + eUpdate.getCount()); eExistingSiteId = eExisting.getSiteId(); }catch(Exception ex){ //If something happens, skip the event processing ex.printStackTrace(); } if ((eExistingSiteId!=null) && (eExistingSiteId.trim().length()>0)) session.saveOrUpdate(eExisting); } } private void doUpdateResourceStatObjects(Session session, Collection<ResourceStat> objects) { if(objects == null) return; Iterator<ResourceStat> i = objects.iterator(); while(i.hasNext()){ ResourceStat eUpdate = i.next(); ResourceStat eExisting = null; String eExistingSiteId = null; try{ Criteria c = session.createCriteria(ResourceStatImpl.class); c.add(Expression.eq("siteId", eUpdate.getSiteId())); c.add(Expression.eq("resourceRef", eUpdate.getResourceRef())); c.add(Expression.eq("resourceAction", eUpdate.getResourceAction())); c.add(Expression.eq("userId", eUpdate.getUserId())); c.add(Expression.eq("date", eUpdate.getDate())); try{ eExisting = (ResourceStat) c.uniqueResult(); }catch(HibernateException ex){ try{ List events = c.list(); if ((events!=null) && (events.size()>0)){ LOG.debug("More than 1 result when unique result expected.", ex); eExisting = (ResourceStat) c.list().get(0); }else{ LOG.debug("No result found", ex); eExisting = null; } }catch(Exception ex3){ eExisting = null; } }catch(Exception ex2){ LOG.debug("Probably ddbb error when loading data at java object", ex2); System.out.println("Probably ddbb error when loading data at java object!!!!!!!!"); } if(eExisting == null) eExisting = eUpdate; else eExisting.setCount(eExisting.getCount() + eUpdate.getCount()); eExistingSiteId = eExisting.getSiteId(); }catch(Exception e){ e.printStackTrace(); } if ((eExistingSiteId!=null) && (eExistingSiteId.trim().length()>0)) session.saveOrUpdate(eExisting); } } private void doUpdateSiteActivityObjects(Session session, Collection<SiteActivity> objects) { if(objects == null) return; Iterator<SiteActivity> i = objects.iterator(); while(i.hasNext()){ SiteActivity eUpdate = i.next(); SiteActivity eExisting = null; String eExistingSiteId = null; try{ Criteria c = session.createCriteria(SiteActivityImpl.class); c.add(Expression.eq("siteId", eUpdate.getSiteId())); c.add(Expression.eq("eventId", eUpdate.getEventId())); c.add(Expression.eq("date", eUpdate.getDate())); try{ eExisting = (SiteActivity) c.uniqueResult(); }catch(HibernateException ex){ try{ List events = c.list(); if ((events!=null) && (events.size()>0)){ LOG.debug("More than 1 result when unique result expected.", ex); eExisting = (SiteActivity) c.list().get(0); }else{ LOG.debug("No result found", ex); eExisting = null; } }catch(Exception ex3){ eExisting = null; } }catch(Exception ex2){ LOG.debug("Probably ddbb error when loading data at java object", ex2); System.out.println("Probably ddbb error when loading data at java object!!!!!!!!"); } if(eExisting == null) eExisting = eUpdate; else eExisting.setCount(eExisting.getCount() + eUpdate.getCount()); eExistingSiteId = eExisting.getSiteId(); }catch(Exception e){ e.printStackTrace(); } if ((eExistingSiteId!=null) && (eExistingSiteId.trim().length()>0)) session.saveOrUpdate(eExisting); } } private void doUpdateSiteVisitsObjects(Session session, Collection<SiteVisits> objects, Map<UniqueVisitsKey, Integer> map) { if(objects == null) return; Iterator<SiteVisits> i = objects.iterator(); while(i.hasNext()){ SiteVisits eUpdate = i.next(); SiteVisits eExisting = null; String eExistingSiteId = null; try{ Criteria c = session.createCriteria(SiteVisitsImpl.class); c.add(Expression.eq("siteId", eUpdate.getSiteId())); c.add(Expression.eq("date", eUpdate.getDate())); try{ eExisting = (SiteVisits) c.uniqueResult(); }catch(HibernateException ex){ try{ List events = c.list(); if ((events!=null) && (events.size()>0)){ LOG.debug("More than 1 result when unique result expected.", ex); eExisting = (SiteVisits) c.list().get(0); }else{ LOG.debug("No result found", ex); eExisting = null; } }catch(Exception ex3){ eExisting = null; } }catch(Exception ex2){ LOG.debug("Probably ddbb error when loading data at java object", ex2); System.out.println("Probably ddbb error when loading data at java object!!!!!!!!"); } if(eExisting == null){ eExisting = eUpdate; }else{ eExisting.setTotalVisits(eExisting.getTotalVisits() + eUpdate.getTotalVisits()); } Integer mapUV = map.get(new UniqueVisitsKey(eExisting.getSiteId(), eExisting.getDate())); eExisting.setTotalUnique(mapUV == null? 1 : mapUV.longValue()); eExistingSiteId = eExisting.getSiteId(); }catch(Exception e){ e.printStackTrace(); } if ((eExistingSiteId!=null) && (eExistingSiteId.trim().length()>0)) session.saveOrUpdate(eExisting); } } private void doUpdateSiteVisitTimeObjects(Session session, Collection<SitePresence> objects) { if(objects == null) return; Iterator<SitePresence> i = objects.iterator(); while(i.hasNext()){ SitePresence eUpdate = i.next(); SitePresence eExisting = null; String eExistingSiteId = null; try{ Criteria c = session.createCriteria(SitePresenceImpl.class); c.add(Expression.eq("siteId", eUpdate.getSiteId())); c.add(Expression.eq("userId", eUpdate.getUserId())); c.add(Expression.eq("date", eUpdate.getDate())); try{ eExisting = (SitePresence) c.uniqueResult(); }catch(HibernateException ex){ try{ List<SitePresence> events = (List<SitePresence>) c.list(); if ((events!=null) && (events.size()>0)){ LOG.debug("More than 1 result when unique result expected.", ex); eExisting = (SitePresence) c.list().get(0); }else{ LOG.debug("No result found", ex); eExisting = null; } }catch(Exception ex3){ eExisting = null; } }catch(Exception ex2){ LOG.debug("Probably ddbb error when loading data at java object", ex2); System.out.println("Probably ddbb error when loading data at java object!!!!!!!!"); } if(eExisting == null){ eExisting = eUpdate; }else{ eExisting.setDuration(eExisting.getDuration() + eUpdate.getDuration()); } eExistingSiteId = eExisting.getSiteId(); }catch(Exception e){ e.printStackTrace(); } if ((eExistingSiteId!=null) && (eExistingSiteId.trim().length()>0)) session.saveOrUpdate(eExisting); } } private void doUpdateServerStatObjects(Session session, Collection<ServerStat> objects) { if(objects == null) return; Iterator<ServerStat> i = objects.iterator(); while(i.hasNext()){ ServerStat eUpdate = i.next(); ServerStat eExisting = null; try{ Criteria c = session.createCriteria(ServerStatImpl.class); c.add(Expression.eq("eventId", eUpdate.getEventId())); c.add(Expression.eq("date", eUpdate.getDate())); try{ eExisting = (ServerStat) c.uniqueResult(); }catch(HibernateException ex){ try{ List events = c.list(); if ((events!=null) && (events.size()>0)){ LOG.debug("More than 1 result when unique result expected.", ex); eExisting = (ServerStat) c.list().get(0); }else{ LOG.debug("No result found", ex); eExisting = null; } }catch(Exception ex3){ eExisting = null; } }catch(Exception ex2){ LOG.debug("Probably ddbb error when loading data at java object", ex2); System.out.println("Probably ddbb error when loading data at java object!!!!!!!!"); } if(eExisting == null) { eExisting = eUpdate; }else{ eExisting.setCount(eExisting.getCount() + eUpdate.getCount()); } }catch(Exception e){ e.printStackTrace(); } session.saveOrUpdate(eExisting); } } private void doUpdateUserStatObjects(Session session, Collection<UserStat> objects) { if(objects == null) return; Iterator<UserStat> i = objects.iterator(); while(i.hasNext()){ UserStat eUpdate = i.next(); UserStat eExisting = null; String eExistingUserId = null; try{ Criteria c = session.createCriteria(UserStatImpl.class); c.add(Expression.eq("userId", eUpdate.getUserId())); c.add(Expression.eq("date", eUpdate.getDate())); try{ eExisting = (UserStat) c.uniqueResult(); }catch(HibernateException ex){ try{ List events = c.list(); if ((events!=null) && (events.size()>0)){ LOG.debug("More than 1 result when unique result expected.", ex); eExisting = (UserStat) c.list().get(0); }else{ LOG.debug("No result found", ex); eExisting = null; } }catch(Exception ex3){ eExisting = null; } }catch(Exception ex2){ LOG.debug("Probably ddbb error when loading data at java object", ex2); System.out.println("Probably ddbb error when loading data at java object!!!!!!!!"); } if(eExisting == null) { eExisting = eUpdate; }else{ eExisting.setCount(eExisting.getCount() + eUpdate.getCount()); } eExistingUserId = eExisting.getUserId(); }catch(Exception e){ e.printStackTrace(); } if(StringUtils.isNotBlank(eExistingUserId)) { session.saveOrUpdate(eExisting); } } } private Map<UniqueVisitsKey, Integer> doGetSiteUniqueVisits(Session session, Map<UniqueVisitsKey, Integer> map) { Iterator<UniqueVisitsKey> i = map.keySet().iterator(); while(i.hasNext()){ UniqueVisitsKey key = i.next(); Query q = session.createQuery("select count(distinct s.userId) " + "from EventStatImpl as s " + "where s.siteId = :siteid " + "and s.eventId = 'pres.begin' " + "and s.date = :idate"); q.setString("siteid", key.siteId); q.setDate("idate", key.date); Integer uv = 1; try{ uv = (Integer) q.uniqueResult(); }catch(ClassCastException ex){ uv = (int) ((Long) q.uniqueResult()).longValue(); }catch(HibernateException ex){ try{ List visits = q.list(); if ((visits!=null) && (visits.size()>0)){ LOG.debug("More than 1 result when unique result expected.", ex); uv = (Integer) q.list().get(0); }else{ LOG.debug("No result found", ex); uv = 1; } }catch (Exception e3){ uv = 1; } }catch(Exception ex2){ LOG.debug("Probably ddbb error when loading data at java object", ex2); } int uniqueVisits = uv == null? 1 : uv.intValue(); map.put(key, Integer.valueOf((int)uniqueVisits)); } return map; } private void doUpdateSitePresencesObjects(Session session, Collection<SitePresenceConsolidation> objects) { if(objects == null) return; Iterator<SitePresenceConsolidation> i = objects.iterator(); while(i.hasNext()){ try{ SitePresenceConsolidation spc = i.next(); SitePresence sp = spc.sitePresence; SitePresence spExisting = doGetSitePresence(session, sp.getSiteId(), sp.getUserId(), sp.getDate()); if(spExisting == null) { session.save(sp); }else{ long previousTotalPresence = spExisting.getDuration(); long previousPresence = 0; long newTotalPresence = 0; if(spc.firstEventIsPresEnd) { if(spExisting.getLastVisitStartTime() != null) previousPresence = spc.firstPresEndDate.getTime() - spExisting.getLastVisitStartTime().getTime(); else throw new Exception("No initial visit start time found - skipping"); } newTotalPresence = previousTotalPresence + previousPresence + sp.getDuration(); spExisting.setDuration(newTotalPresence); spExisting.setLastVisitStartTime(sp.getLastVisitStartTime()); session.update(spExisting); } }catch(HibernateException e){ LOG.debug("Probably ddbb error when loading data at java object", e); }catch(Exception e){ LOG.debug("Unknow error while consolidating presence events", e); } } } // ################################################################ // Special site presence methods (visit time tracking) // ################################################################ @SuppressWarnings("unchecked") private SitePresence doGetSitePresence(Session session, String siteId, String userId, Date date) { SitePresence eDb = null; Criteria c = session.createCriteria(SitePresenceImpl.class); c.add(Expression.eq("siteId", siteId)); c.add(Expression.eq("userId", userId)); c.add(Expression.eq("date", date)); try{ eDb = (SitePresence) c.uniqueResult(); }catch(HibernateException ex){ try{ List es = c.list(); if(es != null && es.size() > 0){ LOG.debug("More than 1 result when unique result expected.", ex); eDb = (SitePresence) es.get(0); }else{ eDb = null; } }catch (Exception e3){ LOG.debug("Probably ddbb error when loading data at java object", e3); eDb = null; } }catch(Exception ex2){ LOG.debug("Probably ddbb error when loading data at java object", ex2); } return eDb; } // ################################################################ // Utility methods // ################################################################ private synchronized boolean isValidEvent(Event e) { if(e.getEvent().startsWith(StatsManager.RESOURCE_EVENTID_PREFIX)){ String ref = e.getResource(); if(ref.trim().equals("")) return false; try{ String parts[] = ref.split("\\/"); if(parts[2].equals("user")){ // workspace (ignore) return false; }else if(parts[2].equals("attachment") && parts.length < 6){ // ignore mail attachments (no reference to site) return false; }else if(parts[2].equals("group")){ // resources if(parts.length <= 4) return false; }else if(parts[2].equals("group-user")){ // drop-box if(parts.length <= 5) return false; }else if ((parts.length >= 3) && (parts[2].equals("private"))) { // discard LOG.debug("Discarding content event in private area."); return false; } }catch(Exception ex){ return false; } } return true; } private Event fixMalFormedEvents(Event e){ String event = e.getEvent(); String resource = e.getResource(); // OBSOLETE: fix bad reference (resource) format // => Use <eventParserTip> instead //if(!resource.startsWith("/")) // resource = '/' + resource; // MessageCenter (OLD) CASE: Handle old MessageCenter events */ if (event!=null){ if(event.startsWith(StatsManager.RESOURCE_EVENTID_PREFIX) && resource.startsWith("MessageCenter")) { resource = resource.replaceFirst("MessageCenter::", "/MessageCenter/site/"); resource = resource.replaceAll("::", "/"); return M_ets.newEvent( event.replaceFirst("content.", "msgcntr."), resource, false); }else{ return e; } }else{ return M_ets.newEvent("garbage.", resource, false); } } private String parseSiteId(Event e){ String eventId = e.getEvent(); // get contextId (siteId) from new Event.getContext() method, if available if(M_sm.isEventContextSupported()) { String contextId = null; try{ contextId = (String) e.getClass().getMethod("getContext", null).invoke(e, null); // STAT-150 fix: String sitePrefix = "/site/"; if(contextId != null && contextId.startsWith(sitePrefix)) { contextId = contextId.substring(sitePrefix.length()); } LOG.debug("Context read from Event.getContext() for event: " + eventId + " - context: " + contextId); }catch(Exception ex){ LOG.warn("Unable to get Event.getContext() for event: " + eventId, ex); } if(contextId != null) return contextId; } // get contextId (siteId) from event reference String eventRef = e.getResource(); if(eventRef != null){ try{ if(StatsManager.SITEVISIT_EVENTID.equals(eventId) || StatsManager.SITEVISITEND_EVENTID.equals(eventId)){ // presence (site visit) syntax (/presence/SITE_ID-presence) String[] parts = eventRef.split("/"); if(parts.length > 2 && parts[2].endsWith(PRESENCE_SUFFIX)) { return parts[2].substring(0, parts[2].length() - PRESENCE_SUFFIX_LENGTH); } }else{ // use <eventParserTip> ToolInfo toolInfo = getEventIdToolMap().get(eventId); EventParserTip parserTip = toolInfo.getEventParserTip(); if(parserTip != null && parserTip.getFor().equals(StatsManager.PARSERTIP_FOR_CONTEXTID)){ int index = Integer.parseInt(parserTip.getIndex()); return eventRef.split(parserTip.getSeparator())[index]; }else if(M_sm.isEventContextSupported()) { LOG.info("Context information unavailable for event: " + eventId + " (ignoring)"); }else{ LOG.info("<eventParserTip> is mandatory when Event.getContext() is unsupported! Ignoring event: " + eventId); // try with most common syntax (/abc/cde/SITE_ID/...) // return eventRef.split("/")[3]; } } }catch(Exception ex){ LOG.warn("Unable to parse contextId from event: " + eventId + " | " + eventRef, ex); } } return null; } private Site getSite(String siteId) { Site site = null; try{ // is it a site id? site = M_ss.getSite(siteId); }catch(IdUnusedException e1){ // is it an alias? try{ String alias = siteId; String target = M_as.getTarget(alias); if(target != null) { String newSiteId = M_em.newReference(target).getId(); LOG.debug(alias + " is an alias targetting site id: "+newSiteId); site = M_ss.getSite(newSiteId); }else{ throw new IdUnusedException(siteId); } }catch(IdUnusedException e2){ // not a valid site LOG.debug(siteId + " is not a valid site.", e2); } }catch(Exception ex) { // not a valid site LOG.debug(siteId + " is not a valid site.", ex); } return site; } /** Get all registered events */ private Collection<String> getRegisteredEvents() { return M_ers.getEventIds(); } /** Get all server events **/ private Collection<String> getServerEvents() { return M_ers.getServerEventIds(); } /** Get eventId -> ToolInfo map */ private Map<String, ToolInfo> getEventIdToolMap() { return M_ers.getEventIdToolMap(); } /** * Get the date for today (time of 00:00:00). * This is used when we are grouping event by day. */ private Date getToday() { return new Date(); } private boolean isUserLoginEvent(Event e) { return StringUtils.equals(StatsManager.LOGIN_EVENTID, e.getEvent()); } private Date getTruncatedDate(Date date) { if(date == null) return null; Calendar c = Calendar.getInstance(); c.setTime(date); c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); return c.getTime(); } private static class UniqueVisitsKey { public String siteId; public Date date; public UniqueVisitsKey(String siteId, Date date){ this.siteId = siteId; this.date = resetToDay(date); } @Override public boolean equals(Object o) { if(o instanceof UniqueVisitsKey) { UniqueVisitsKey u = (UniqueVisitsKey) o; return siteId.equals(u.siteId) && date.equals(u.date); } return false; } @Override public int hashCode() { return siteId.hashCode() + date.hashCode(); } private Date resetToDay(Date date){ Calendar c = Calendar.getInstance(); c.setTime(date); c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); return c.getTime(); } } private static class SitePresenceConsolidation { public boolean firstEventIsPresEnd; public Date firstPresEndDate; public SitePresence sitePresence; public SitePresenceConsolidation(SitePresence sitePresence) { this(sitePresence, null); } public SitePresenceConsolidation(SitePresence sitePresence, Date firstPresEndDate) { this.sitePresence = sitePresence; if(firstPresEndDate == null) { this.firstEventIsPresEnd = false; this.firstPresEndDate = null; }else{ this.firstEventIsPresEnd = true; this.firstPresEndDate = firstPresEndDate; } } @Override public boolean equals(Object o) { if(o instanceof SitePresenceConsolidation) { SitePresenceConsolidation u = (SitePresenceConsolidation) o; return firstEventIsPresEnd == u.firstEventIsPresEnd && ( (firstPresEndDate == null && u.firstPresEndDate == null) || (firstPresEndDate != null && firstPresEndDate.equals(u.firstPresEndDate)) ) && ( (sitePresence == null && u.sitePresence == null) || (sitePresence != null && sitePresence.equals(u.sitePresence)) ); } return false; } @Override public int hashCode() { return Boolean.valueOf(firstEventIsPresEnd).hashCode() + firstPresEndDate.hashCode() + sitePresence.hashCode(); } @Override public String toString() { StringBuilder buff = new StringBuilder(); buff.append("firstPresEndDate: "); buff.append(firstPresEndDate); buff.append(" | sitePresence => "); if(sitePresence != null) buff.append(sitePresence.toString()); else buff.append("null"); return buff.toString(); } } }